feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) - #152
Conversation
… (issue #106) Prevent workers (implementer/orchestrator sessions) from mutating the shared MAIN checkout's git state, and give the loop census a way to detect it if it ever happens anyway. - guard-git-add.py (PreToolUse hook): adds a second check alongside the existing blanket-`git add -A`/`commit -a` guard. Blocks add/rm/mv/reset/switch/checkout <branch>/restore --staged run by a worker session when the command's effective git toplevel (honoring `cd`/`git -C` overrides) resolves to the MAIN checkout rather than the worker's own `.claude/worktrees/<name>` worktree. Worker detection: primary signal is the RECODE_WORKER=1 marker env var (documented in the implementer/orchestrator agent defs), corroborated by the session cwd already sitting under a worktree path. Owner sessions (neither signal present) are unaffected — default allow, same best-effort spirit as the existing guard (never blocks non-hardened repos). - loop-census.sh: two new per-tick telemetry lines. main_dirty=yes|no — is the MAIN checkout dirty, excluding sandbox device-node masks and the read-only-mounted .claude/agents/ and .claude/skills/setup/templates/ trees (which can legitimately lag behind HEAD in sandboxed sessions). main_head=<branch>|detached — the MAIN checkout's current HEAD. Motivated by the 2026-07-16 incident: a driver's `git checkout` failed mid-operation against a read-only-mounted agent file and left main in a DETACHED HEAD on an unmerged commit for ~12h, with a CLEAN working tree throughout — main_dirty alone would have missed it entirely. - implementer.md / orchestrator.md: explicit hard rule that ALL git operations happen inside the worker's own worktree, never the main checkout; a shared-branch need is a re-scope signal to the orchestrator, never a license to reach into the main checkout. - Tests: guard-git-add.test.sh (19 cases — owner/worker x main/worktree, the `-C`/`cd` escape-hatch cases, and the corroboration-only path) and loop-census.test.sh (+9 cases covering main_dirty/main_head, the mask and read-only-mount exclusions, and the detached-HEAD signal). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
9 similar comments
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
|
feat(orchestrator): worker git-state guard + dirty-main census signal (issue #106) (not yet reviewed) |
Closes #106
Problem
Workers (implementer/orchestrator sessions) had no guard against running
git state-mutating commands against the shared MAIN checkout instead of
their own isolated worktree. On 2026-07-16 a driver's
git checkoutinthe main checkout failed mid-operation on a read-only-mounted agent file
and left
mainin a DETACHED HEAD on an unmerged commit for ~12h — witha clean working tree throughout, so nothing in the existing telemetry
would have surfaced it.
Guard hook (
guard-git-add.py, PreToolUse)Extends the existing blanket-
&&` and `git -C ` overrides in the command string — resolves to the MAIN checkout rather than the session's own `.claude/worktrees/` worktree, for **worker** sessions only.git add -A/commit -aguard with a secondcheck: blocks
add/rm/mv/reset/switch/checkout <branch>/restore --stagedwhen the command's effective git toplevel — honoring `cdWorker detection is two-signal, either sufficient on its own:
RECODE_WORKER=1marker env var (documented as a hardrule in
.claude/agents/implementer.mdand.claude/agents/orchestrator.md— set via each agent's own session
envso the guard also protects anorchestrator that runs directly in the main checkout by cwd, not just an
implementer that's always cwd-isolated).
.claude/worktrees/<name>/....Owner sessions (neither signal present) are unaffected — default allow,
same best-effort spirit as the pre-existing guard: it only engages when
the sandbox is hardened, and never blocks non-hardened downstream
consumers.
Census signal (
loop-census.sh)Two new per-tick telemetry lines:
main_dirty=yes|no— is the MAIN checkout dirty, after excluding (a)sandbox
/dev/nulldevice-node masks and (b) the read-only-mounted.claude/agents/and.claude/skills/setup/templates/trees, which canlegitimately lag behind HEAD in sandboxed sessions.
main_head=<branch>|detached— the MAIN checkout's current HEAD.Companion to
main_dirty: the 2026-07-16 incident leftmaindetachedwith a clean working tree the whole time, so
main_dirtyalone wouldnever have caught it — this line closes that gap.
Prompt reinforcement
implementer.md/orchestrator.mdeach get an explicit "Git state (hardrule)" section: ALL git operations happen inside the worker's own
worktree; a shared-branch need (or a branch "already checked out
elsewhere") is a re-scope signal to the orchestrator, never a license to
cd/git -Cinto the main checkout.Tests
guard-git-add.test.sh(19 cases): owner-vs-worker × main-checkout-vs-own-worktree for every mutating subcommand, the non-mutating forms that
must stay allowed, the
-C/cdescape-hatch cases, and thecorroboration-only (no marker) path.
loop-census.test.sh(+9 cases): clean/dirty/detached fixtures, thesandbox-mask exclusion, and the
.claude/agents//.claude/skills/setup/ templates/lag exclusion.Both pass locally, along with the self-hosted gates
(
GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh build|lint|test).🤖 Generated with Claude Code