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:
@@ -101,6 +101,7 @@ regression around the RED-first gate.
|
||||
```
|
||||
THE LOOP NEVER COMMITS. CODE EDITS, TESTS, AND THE STATS FILE LIVE IN THE WORKING TREE AS UNSTAGED CHANGES UNTIL THE ORCHESTRATOR COMMITS THEM.
|
||||
WITHIN THIS LOOP, MAIN HEAD IS SACROSANCT — NO RESET, NO REVERT; THE LOOP ONLY EVER WRITES UNSTAGED CHANGES. MAIN MOVES FORWARD ONLY VIA ORCHESTRATOR COMMITS. (THE `/boss` ROLLBACK SANDBOX WINDS BACK ALREADY-COMMITTED AUTONOMOUS WORK — A BOSS-LEVEL RECOVERY, NOT A STEP IN THIS LOOP.)
|
||||
NO IN-LOOP AGENT EVER DISCARDS WORKING-TREE STATE VIA `git checkout` / `git restore` — WHOLE-FILE OR `--patch`: NOTHING COMMITS BETWEEN TASKS, SO A SHARED FILE CARRIES EARLIER TASKS' UNCOMMITTED DONE WORK, AND A CHECKOUT SILENTLY DESTROYS IT (issue #23). RECOVERY IS FORWARD — EDIT BACK, OR REPORT `BLOCKED`. (THE ORCHESTRATOR'S OWN END-OF-ITERATION `git checkout -- .` DISPOSITION HAPPENS OUTSIDE THE LOOP.)
|
||||
PER-TASK PHASES ARE SEPARATE AGENT CALLS IN THE WORKFLOW SCRIPT — implementer, then spec-compliance, then quality. SPEC COMPLIANCE IS GATED BEFORE QUALITY.
|
||||
TASKS RUN SEQUENTIALLY; A BLOCKED TASK STOPS THE LOOP — NO SKIP-AHEAD (TASK ORDERING DEPENDENCIES ARE UNKNOWN).
|
||||
NEVER PUSH PAST `BLOCKED` BY HAND.
|
||||
@@ -155,6 +156,32 @@ ground truth, never a self-report** — a self-report must never be able to
|
||||
The E2E phase's `status` is also read now (not just its fixtures): a
|
||||
non-`DONE` E2E status or a zero-fixture run surfaces as a concern.
|
||||
|
||||
**Cross-task discard guard** (issue #23). In a multi-task run nothing
|
||||
commits between tasks, so one task's file-level `git checkout`/`git
|
||||
restore` can silently destroy an earlier task's DONE work in a shared
|
||||
file — invisible to the reviewers (scoped to the current task's
|
||||
footprint) and to the tree-count gate (the surviving changes keep the
|
||||
count nonzero). The loop therefore records a snapshot boundary after
|
||||
every task (`git stash create` — a dangling commit; HEAD, index, and
|
||||
working tree untouched) and hard-`BLOCK`s when a path that was
|
||||
HEAD-modified at one boundary is HEAD-identical at the next: the
|
||||
end-report names the lost paths, the boundary, and the recovery
|
||||
snapshot sha the discarded content can be read from
|
||||
(`git show <sha>:<path>`). Coarse by design, in both directions: a
|
||||
checkout followed by re-edits of the same file, or a `--patch`-level
|
||||
restore of a single hunk, escapes the comparison (the path never
|
||||
leaves the diff) — the snapshot still keeps any loss diagnosable and
|
||||
recoverable; conversely a task that legitimately edits a shared file
|
||||
back to byte-identical HEAD content trips the guard, so a tripped
|
||||
verdict means *adjudicate against the snapshot*, not proven malice.
|
||||
Renames do not trip it (the boundary diff runs `--no-renames`, so a
|
||||
renamed file's old path stays listed as a deletion). Untracked files
|
||||
sit outside the threat model (`git checkout -- <path>` cannot discard
|
||||
them). Cost: one extra sonnet/medium snapshot call per task of a
|
||||
multi-task run; single-task and mini runs skip the guard. The
|
||||
detection details live in `workflows/implement-loop.js`, the single
|
||||
source.
|
||||
|
||||
## The Process — orchestrator side
|
||||
|
||||
### Step 1 — Run the `implement-loop` workflow
|
||||
|
||||
Reference in New Issue
Block a user