feat: session mode — agentic-git run (#1)#3
Merged
Merged
Conversation
…g/hooks (#1) Absorbs the minimal orchestrator role for standalone use: argv[0] dispatch (Δ1) splits shim mode (basename(argv[0]) == "git") from a new CLI surface (`run`, `version`) in `crates/agentic-git/src/cli.rs`, so shim-mode's existing body is untouched (byte-identical, only renamed to `shim_main`). `run` provisions, per the design's INVARIANT (same disk contract the agend daemon writes): home dirs + an atomically first-writer-wins integrity key (Δ2, hard-link no-replace), a validated `--agent` identity (Δ3 v3, lowercase + Windows-device-name-safe), a worktree + signed binding.json (schema v1), worktree-scoped hooks (`extensions.worktreeConfig` shared + `core.hooksPath --worktree`, so the source checkout keeps its own hooks), and the `<home>/bin/git` shim symlink — then spawns the agent inside it, propagating its exit code and printing a session summary. Binding reuse follows Δ4's complete-match-or-hard-error predicate (branch + worktree + canonicalized source_repo + gitdir ownership) — never a silent rebind. Adds `getrandom` as the only new dependency (key + default agent-name randomness). CI gets a `legacy-env-adoption` job (Δ5) that explicitly unsets every `AGENTIC_GIT_*` var and count-asserts the legacy-only route/deny/ bypass suite actually ran. Deviations from a fully mechanical implementation (both explicit, both git-diff-revertable): - Hoisted the #1504 recursion-guard check to run once, unconditionally, before mode dispatch (shim_main's own inline copy is untouched) — a cross-cutting recursion safety net, not shim-specific logic. - Two pre-existing shim_phase2.rs tests (`shim_denies_agent_bypass_canonical_provisioning_2234`, `self_referential_real_git_env_ignored_review1`) invoked the compiled binary directly via `Command::new(CARGO_BIN_EXE_agentic-git)` without forcing argv[0]; under literal Δ1 that basename ("agentic-git") is the issue's own worked CLI-mode example, so their shim-mode assertions would otherwise break. Added `.arg0("git")` (unix) to both — invocation shape only, zero assertion changes — so they keep exercising shim mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ew blocker) fs::write + set_permissions left a umask-dependent window where the 32-byte HMAC key existed group/world-readable before chmod; another local uid watching $AGENTIC_GIT_HOME could read it and forge binding sidecars. Extracted open_new_0600 (create_new + mode(0o600) at open time) so the key file is private from birth; create_new also refuses a pre-planted tmp path. Regression test stats the file while the handle is open — reverting to write-then-chmod turns it RED. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
suzuke
added a commit
that referenced
this pull request
Jul 4, 2026
* fix: make agentic-git publishable + close 3 run bugs + smoke tests
Adversarial code review (fugu) of main found 3 real defects; all fixed with
regression coverage, plus a CI publish gate and end-to-end smoke tests.
1. **agentic-git crate was not publishable** (the real cause of the failed
release run). `cli.rs`/`shim_phase1.rs` did `include_str!("../../../assets/
hooks/…")` — workspace-ROOT assets that `cargo package` does not put in the
crate tarball, so the verify build failed. Move `assets/hooks/` INTO the
crate (`crates/agentic-git/assets/`), fix the 6 include paths. `cargo
package -p agentic-git` now verifies clean.
2. **`run` could launch UNGUARDED.** `spawn_agent`'s child PATH used
`join_paths(...).unwrap_or(path_env)`; a home path containing ':' made
join fail → silent fallback to the real PATH → the agent's git bypassed the
shim entirely. Now a hard EX_CONFIG refusal.
3. **`.agend-managed` defeated skip-when-clean.** The provisioned marker is an
untracked file, so every fresh session's tree looked dirty and clean-tree
destructive ops still snapshotted. `is_clean` now treats a tree whose only
untracked entry is the marker as clean.
Also: **smoke tests** (`tests/smoke.rs`) drive the shipped binary through the
real `agentic-git run` flow (routing→trailer→snapshot→recover→deny) + pin #2/#3;
**CI `cargo package` gate** (both crates, verify build) so #1 can't regress;
**release.yml `set +e`** so `bash -e` no longer aborts the idempotent publish
on an already-uploaded crate.
cargo test --workspace: 150 green (+3 smoke); clippy clean; both crates package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: allow the documented snapshot restore through the shim (fugu hunt #4)
fugu verified the 3 packaging/run fixes, then found a real usability
blocker: the recovery layer's OWN documented restore —
`git checkout <snapshot-ref> -- .` — was DENIED by the shim. The
cross-branch guard reads args[1] as a branch target, so a snapshot ref
tripped 'cross-branch — cannot switch to refs/agentic-git/snapshots/...',
leaving snapshots un-restorable unless the user knew to set
AGENTIC_GIT_BYPASS=1. The recovery feature was effectively broken for a
docs-following user.
Fix: a `checkout <tree-ish> -- <pathspec>` is a working-tree path restore,
not a branch switch (the bound branch is unchanged), so the cross-branch
guard must not deny it. Recognize the `--` pathspec form and skip the
cross-branch check for checkout.
+smoke test run_session_documented_restore_works_without_bypass_smoke: full
real-session round-trip (dirty+untracked → reset --hard snapshot → delete
untracked → `git checkout <snap> -- .` through the shim, NO bypass → both
recovered).
Also harden legacy_env_adoption's bypass assertion: check the deny SIGNATURE
('cross-branch'), not the loose word 'denied' (the #2158 bypass audit dumps
process ancestry to stderr, which a parent command's text could false-trip).
cargo test --workspace: green; clippy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(snapshot): force a self-contained identity in commit-tree (Linux CI)
The new smoke tests + Linux CI caught a real cross-platform bug invisible on
macOS: on a runner/container/fresh machine with NO git user.name/user.email
(ubuntu-latest has none; macOS derives a default), the snapshot's
`git commit-tree` — which REQUIRES an author+committer — silently failed,
so a real session's destructive op created NO snapshot (SNAPS=0, fail-open),
i.e. the recovery layer didn't work exactly where it's supposed to. And the
agent's own `git commit` failed (TRAILER=no).
Fix: the snapshot forces a fixed GIT_AUTHOR/COMMITTER_NAME+EMAIL
('agentic-git <agentic-git@localhost>') alongside the dates it already
forces, making snapshots environment-independent. The smoke session also
sets an identity so the agent's own commits succeed on bare runners.
Verified by reproducing the failure on macOS with GIT_CONFIG_GLOBAL=/dev/null
+ empty HOME: smoke now passes (previously SNAP=NONE, as on ubuntu CI).
cargo test --workspace: green; smoke green under a no-git-identity env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
suzuke
added a commit
that referenced
this pull request
Jul 5, 2026
* feat(snapshots): one-command `restore` recovery (kills the git-native onboarding drop-off) `git checkout <snapshot-ref> -- .` was the only documented recovery — a git-native step that a product trier who doesn't understand snapshot refs abandons on (adversarial review, fugu drop-off #3). Replace it with: agentic-git snapshots restore [<ref>] [--repo <path>] [--yes] [--staged] Design settled against an adversarial design pass: - Non-destructive: writes the snapshot's paths back; NEVER deletes files created after it (this is a *recovery* tool, not a tree rollback). - Lands UNSTAGED by default (a user recovering files does not expect them silently staged into the next commit); `--staged` opts into the classic `checkout -- .` index side effect. - Fail-CLOSED pre-restore snapshot: restore overwrites the working tree, so it snapshots current state first (undo target) and ABORTS if that fails — unlike `maybe_snapshot`, which fails open because it must never block the git op it protects. This is our own command; no reason to overwrite unsaved work with the net down. - No-ref resolution refuses to guess: exactly one snapshot → use it; several → exit 2 and list candidates unless `--yes` (then the genuinely-newest, tie-broken by ref `<seq>` since dates are only second-granular). - Only restores from `refs/agentic-git/snapshots/…` — never an arbitrary branch/tag. Paths resolved via `diff --name-status -z` (spaces/newlines safe; `A` entries = newer files, excluded). 6 end-to-end tests (real shim snapshot + real CLI restore): unstaged recovery, `--staged`, ambiguous+`--yes`-newest, bad-ref reject, no-snapshots, and a non-destructive undo round-trip. README updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(snapshots): restore feeds pathspecs via stdin, not argv (fixes E2BIG on large snapshots) fugu's adversarial review (PR #10) repro'd a real blocker: a `clean -fd` snapshot of 60,000 long filenames restored ZERO files — `git checkout` received every changed path as argv and died with `Argument list too long` (E2BIG) before touching the tree. One-command recovery was unusable for exactly the case it's for: large generated trees / vendored dirs. Fix: feed pathspecs to checkout AND reset over stdin via `--pathspec-from-file=- --pathspec-file-nul`. No argv limit; one invocation handles any count. Verified against git 2.39. Bonus: paths now stay RAW BYTES end-to-end (diff `-z` → `Vec<u8>` → NUL-joined stdin), dropping the lossy `String::from_utf8_lossy` conversion — a non-UTF-8 filename round-trips unchanged (fugu's secondary concern). Regression test R7: 5000 files with 230-char names (>1 MB of pathspec, past macOS ARG_MAX) lost and fully recovered in one command. Full workspace suite green (23 snapshot tests), clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(snapshots): restore pathspecs literally (recover files named like git magic) fugu re-review (PR #10): a legal filename that begins with git's pathspec sigil — `:(glob)literal.txt`, `:/foo`, `:x` — was snapshotted faithfully but could NOT be recovered: `git checkout` parsed the fed pathspec as a magic expression (`pathspec ... did not match`) instead of a literal path. The recovery promise broke for an entire class of legal names. Fix: set `GIT_LITERAL_PATHSPECS=1` on the pathspec-stdin checkout AND reset, so every fed path is treated literally, never as magic. Verified against git 2.39 (repro recovers `:(glob)literal.txt` byte-for-byte). This closes the weird-filename class comprehensively: bulk/E2BIG (stdin), spaces+newlines (-z/NUL), non-UTF-8 bytes (raw Vec<u8>), and now magic sigils (literal pathspecs). Regression test R8: a `:(glob)…`-named file lost and recovered. 24 snapshot tests green, clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
suzuke
added a commit
that referenced
this pull request
Jul 5, 2026
* docs: run-first onboarding + a runnable recovery demo (the three productization items) Adversarial product discussion with fugu surfaced three onboarding gaps for a stranger who has never heard of agend. This closes them: 1. Killer demo — demo/recovery-demo.sh: a REAL guarded session where an agent erases uncommitted + untracked work with `reset --hard`, then one `agentic-git snapshots restore` brings it back (unstaged). Single story, no git internals. It asserts every step, so it doubles as a cold-start acceptance check (AGENTIC_GIT_BIN=$(command -v agentic-git) to test an install). Robust to a wrapped `git` on PATH; runs the guard un-bypassed. 2. Cold-start path — README "Trying it" was STALE: it claimed `agentic-git run` was "the top roadmap item" (it shipped in PR #3) and only showed the manual symlink dance. Rewrote it as a run-first Quickstart (`cargo install` → `agentic-git run -- <agent>` → `snapshots restore`), demoting the manual wiring to an "embedding it yourself" subsection. 3. Framing — promoted **recovers** into the top capability list and added a `reset --hard`/`clean -fd` row to the Why table, per fugu's reframe that recovery ("plain git has no undo for that") is a first-class reason to adopt, not a footnote. Honest positioning (seatbelt-not-cage) already present, left intact. No src/ changes. Demo verified end-to-end (DEMO PASSED); shellcheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(demo): tell the truth about `git status` in a session worktree fugu review (PR #11): the demo printed `git status → (clean)` and had the agent say "git status is clean" — but in a real `agentic-git run` worktree, plain `git status --porcelain` is NOT clean: the session's `.agend-managed` marker remains untracked (`?? .agend-managed`). A first-time user running the advertised command would see output the demo denied, and the step was never even asserted. Now the demo shows the REAL output honestly — `?? .agend-managed (only the session marker — none of your work)` — and actually runs + asserts that, filtering the marker, git has no record of the erased work to restore from. The agent's line no longer claims "clean". Demo re-verified end-to-end (DEMO PASSED); shellcheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
suzuke
added a commit
that referenced
this pull request
Jul 8, 2026
…embedder P1a) (#24) * feat(core): consolidate signing/verify primitives + scheme envelope (embedder P1a) P1a moves the security contract into agentic-git-core so signer and verifier share ONE source (no drift). Isolated to the agentic-git repo — the agend-terminal daemon wiring + daemon signer swap is P1b. - Move `ensure_key` (lock-free first-writer-wins `hard_link` provisioning) + its `open_new_0600` helper verbatim from the run CLI into core as the single key primitive; core gains a `getrandom` dep. The run CLI now calls `core::ensure_key` / `core::sign_binding` (its local copies removed). - Add `sign_binding` = ensure_key + sign — closes the reviewer4 hole where the low-level `sign` panicked on a fresh home. - `verify` becomes a typed `Result<(), VerifyError{MissingKey | MalformedTag | MacMismatch | UnsupportedScheme{..}}>` (was `bool`). It parses a self-describing tag ENVELOPE (`ag-hmac-sha256:v<algo>:<key-format>:<hex>`) BEFORE the MAC check, so an HMAC scheme skew is diagnosable — the shim maps `UnsupportedScheme` to a LOUD deny (`verify_sidecar`) instead of a silent "unbound". - Add `BINDING_FORMAT_VERSION` / `HMAC_ALGO_VERSION` consts and `envelope_tag` (the "can produce" capability). ORDERING INVARIANT (fleet-safety — the emphasized landmine): the DEFAULT emit stays the BARE hex tag (implicit scheme v1), byte-identical to the pre-P1a signer, so an unswapped shim and every on-disk sidecar keep verifying. The envelope is a capability core can parse + produce, NOT the default output (that flips in P2). RED-first proven: making `sign_binding` emit the envelope fails the byte-identical test against an independent python-HMAC oracle. reviewer4 acceptance conditions (RED-first): #2 a malformed `ag-hmac-sha256:` prefixed tag NEVER falls back to bare-hex legacy (extra colon / non-`v` algo / non-hex / unknown key-format -> MalformedTag/UnsupportedScheme); #3 a newer scheme or any tamper NEVER authenticates; #4 bare-hex legacy accepted (the migration window). (#1 lockfile-single-source is P1b — agend-terminal.) Regression: the run CLI's concurrent first-writer-wins is preserved — the existing session_mode race tests (driving the real `run` binary) stay green through the core reroute, plus a direct core-level N-thread race test. cargo test (workspace) 0-failed; clippy --workspace --all-targets clean; cargo package -p agentic-git-core green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Huang ChiaCheng (黃家政) <1557604+suzuke@users.noreply.github.com> * fix(release): bump agentic-git-core to 0.2.0 + package interdependent crates together (reviewer4 D1) P1a added new public API (`VerifyError`, `ensure_key`, `sign_binding`) + a breaking `verify` bool->Result change to agentic-git-core WITHOUT bumping its version, so the version/package contract was incoherent: `cargo package -p agentic-git` resolves `agentic-git-core = "0.1.0"` from crates.io (the OLD published API) and fails to compile the new call sites — the PR's ubuntu CI failure. A released binary depending on published core 0.1.0 could not build with the new code. - Bump the workspace (core) version 0.1.0 -> 0.2.0 — a 0.x minor bump for the new + breaking API, exactly what the workspace comment prescribes ("core bumps when its API changes"). The shim keeps its own independent 0.2.2. - Update the binary's dep requirement to `agentic-git-core { path, version = "0.2.0" }`. - CI: package the two interdependent crates in ONE `cargo package -p agentic-git-core -p agentic-git --allow-dirty` so the binary's verify build resolves core from the just-packaged sibling (build-from-workspace), NOT the older published registry version. No crates.io publish happens (the release-time publish stays manual / operator-gated). dev3 VERIFIED the crypto surface and reviewer4 confirmed no crypto fail-open — this is purely the package/version contract fix; no core logic changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Huang ChiaCheng (黃家政) <1557604+suzuke@users.noreply.github.com> --------- Signed-off-by: Huang ChiaCheng (黃家政) <1557604+suzuke@users.noreply.github.com> Co-authored-by: Huang ChiaCheng (黃家政) <1557604+suzuke@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements issue #1's design (v2+v3 — survived 3 rounds of adversarial design review).
is_git_invocation(basename, .exe-stripped, case rule per platform); shim flow byte-identical (main→shim_main); CLI mode in newcli.rs; unknown CLI subcommand → exit 2, never silently shims.run: home/key provisioning (Δ2 hard-link no-replace,getrandom= only new dep), Δ3 v3 agent-name validator, worktree + binding (signed via core crate public API only), amended hooks scoping (extensions.worktreeConfig+--worktree), shim wiring, signal-forwarding spawn, keep-teardown + session summary.legacy-env-adoptionrequired CI job (explicit unset + count-assert)..arg0("git")only (invocation shape; assertions unchanged) — structurally required by Δ1, disclosed in commit.Design: #1 · Implementation by delegated agent, reviewed by claude-fb2461 · adversarial diff review: fugu (dispatched)
🤖 Generated with Claude Code