fix(implement-loop): guard against a task discarding sibling tasks' uncommitted work

During a multi-task run nothing commits between tasks, so one task's
file-level `git checkout -- <file>` / `git restore <file>` silently
destroys an earlier task's uncommitted DONE work in a shared file. The
loss was structurally invisible: spec- and quality-reviewers are scoped
to the current task's footprint, diff_fingerprint only detects
same-task repair cycles, and the end-of-iteration tree gate counts
files (the surviving changes keep it nonzero). Observed in the wild as
a PARTIAL run whose DONE reports did not match the tree. Two layers,
both plugin-side (harness-level prevention is ruled out — decision log
on the issue):

Prose guard — implementer.md Step 8 no longer sanctions file checkout
for scope curation: over-reach is undone by editing back, a broken
intermediate state is repaired forward or reported BLOCKED (new fourth
BLOCKED bucket in the status protocol). A matching Iron Law line in
implement/SKILL.md covers all in-loop agents, whole-file and --patch;
docs/conventions.md now marks the checkout discard idiom as the
orchestrator's, between iterations.

Mechanical guard — after every task of a multi-task run a snapshot
agent records `git stash create` (a dangling commit; HEAD, index, and
tree untouched — semantics verified empirically in a scratch repo) plus
the `git diff HEAD --name-only --no-renames` path set. A path that was
HEAD-modified at one boundary and gone at the next trips a hard
BLOCKED naming the lost paths, the boundary, and the recovery snapshot
sha (`git show <sha>:<path>`); the discard verdict outranks per-task
outcomes in blocked_detail since the reports and the tree have
diverged. Coarse by design, in both directions, and documented as
such: a checkout-then-re-edit or a --patch hunk restore escapes the
comparison (the snapshot keeps it diagnosable); a legitimate
back-to-HEAD edit trips it (the verdict says adjudicate against the
snapshot). --no-renames keeps a staged rename from reading as a loss;
untracked files are outside the threat model (checkout cannot discard
them). Cost: one sonnet/medium call per task, multi-task runs only;
single-task and mini runs are unchanged.

Verified: node --check on the async-wrapped script and a stub-agent
harness — discard trips the hard BLOCKED and stops the loop; an
accumulating happy path, a plan-intended deletion, a single-task run,
and a dead snapshot agent all pass without a false positive.

closes #23
This commit is contained in:
2026-07-09 11:27:51 +02:00
parent 22aafe892a
commit e7009bc304
5 changed files with 172 additions and 12 deletions
+2 -2
View File
@@ -159,8 +159,8 @@ pin exists to remove. Effort follows the model split:
Workflow scripts pin a third tier inline: schema-bound
extraction/verification stages that author no code (preflight,
plan-index, plan-extract, mini-verify, tree-check, finalize, build/suite verify)
run `medium` via the `agent()` call's `effort:` option. As with
plan-index, plan-extract, snapshot, mini-verify, tree-check, finalize,
build/suite verify) run `medium` via the `agent()` call's `effort:` option. As with
`model:`, every `agent()` call passes `effort:` explicitly — whether
frontmatter effort propagates through an `agentType` dispatch is
undocumented, so the scripts do not rely on it (see the policy header
+8 -2
View File
@@ -130,8 +130,14 @@ too.
- **main HEAD is sacrosanct — below the session anchor.** Nobody runs
`git reset` / `git revert` on user-ratified main history (or any
protected branch). main moves forward only via orchestrator commits; a
wrong agent diff is discarded with `git checkout -- <paths>` /
`git stash`, never by rewinding ratified history.
wrong agent diff is discarded — by the orchestrator, between
iterations — with `git checkout -- <paths>` / `git stash`, never by
rewinding ratified history. That discard idiom is the orchestrator's
alone: an agent running INSIDE a multi-task `implement` iteration
never checkouts/restores a file — nothing commits between tasks, so
it would silently destroy sibling tasks' uncommitted work; in-loop
recovery is forward-editing or `BLOCKED` (`implement/SKILL.md` Iron
Law, issue #23).
**One narrow `/boss` exception — the rollback sandbox:** inside an
autonomous run the orchestrator anchors on the `main` HEAD it started
from (the last user-ratified state) and MAY `git reset --hard` its