Incident
After the issue #102 advance (PR #103, 2026-07-09), main's checkout was left dirty: the PR's harness changes were staged in the MAIN checkout's index (git status showed A/MM/AD entries for loop-daemon.sh, loop-event.sh, skills/setup files, templates), while the working tree was mostly back at HEAD. Reconstruction: the implementer at some point ran git add (and likely a shared-branch checkout) in the main checkout instead of its worktree — the run's own execution note mentions the docs phase "had to remove the stale harness worktree to check out the shared branch". The staged content was fully contained in PR #103, so recovery was git reset + restore, but this is the second time main has been found dirty after loop activity.
Why it matters
merge-ready.sh's local_sync only fast-forwards a clean main — a dirty checkout silently stops local syncing, so the owner's terminal drifts behind merged reality (exactly the "main is not clean, this keeps happening" experience).
- A dirty index in main risks a later owner commit accidentally including leaked worker content.
- Module boundaries are enforced for FILE edits, but nothing stops a worker from mutating the main checkout's GIT state (index, HEAD, branch).
Proposal
- Guard hook (mechanism, not discipline): extend
guard-git-add.py (or add a sibling PreToolUse hook) to block git state mutations — add, rm, mv, restore --staged, reset, checkout <branch>, switch — when the command's repo toplevel is the MAIN checkout rather than a .claude/worktrees/* path, for worker sessions. Worker detection is the design question: e.g. the orchestrator/implementer agent defs export a marker env var the hook checks, defaulting to allow for owner sessions. Same best-effort spirit as the existing hook (keyed off sandbox/settings, never blocks non-hardened repos).
- Prompt reinforcement: implementer + orchestrator agent definitions get an explicit hard rule: ALL git operations happen inside the worker's own worktree; a worker that needs a shared branch checks it out IN ITS WORKTREE (
git worktree add semantics already guarantee exclusivity — surface the "branch already checked out elsewhere" failure as a re-scope signal to the orchestrator, not a license to use the main checkout).
- Census hygiene line (visibility):
loop-census.sh emits a main_dirty=yes|no (porcelain non-empty, excluding known sandbox mask paths) line so every tick surfaces contamination instead of it being discovered by the owner days later.
Acceptance criteria
Incident
After the issue #102 advance (PR #103, 2026-07-09),
main's checkout was left dirty: the PR's harness changes were staged in the MAIN checkout's index (git statusshowedA/MM/ADentries forloop-daemon.sh,loop-event.sh, skills/setup files, templates), while the working tree was mostly back at HEAD. Reconstruction: the implementer at some point rangit add(and likely a shared-branch checkout) in the main checkout instead of its worktree — the run's own execution note mentions the docs phase "had to remove the stale harness worktree to check out the shared branch". The staged content was fully contained in PR #103, so recovery wasgit reset+ restore, but this is the second timemainhas been found dirty after loop activity.Why it matters
merge-ready.sh'slocal_synconly fast-forwards a cleanmain— a dirty checkout silently stops local syncing, so the owner's terminal drifts behind merged reality (exactly the "main is not clean, this keeps happening" experience).Proposal
guard-git-add.py(or add a sibling PreToolUse hook) to block git state mutations —add,rm,mv,restore --staged,reset,checkout <branch>,switch— when the command's repo toplevel is the MAIN checkout rather than a.claude/worktrees/*path, for worker sessions. Worker detection is the design question: e.g. the orchestrator/implementer agent defs export a marker env var the hook checks, defaulting to allow for owner sessions. Same best-effort spirit as the existing hook (keyed off sandbox/settings, never blocks non-hardened repos).git worktree addsemantics already guarantee exclusivity — surface the "branch already checked out elsewhere" failure as a re-scope signal to the orchestrator, not a license to use the main checkout).loop-census.shemits amain_dirty=yes|no(porcelain non-empty, excluding known sandbox mask paths) line so every tick surfaces contamination instead of it being discovered by the owner days later.Acceptance criteria
git add/checkout/switch/resetwith toplevel == main checkout is blocked by the hook with a message pointing it at its worktree; owner sessions are unaffected.loop-census.shreportsmain_dirty=each tick, excluding sandbox mask phantom paths (see docs/HARDENING.md → Caveats).loop-census.test.sh.