implement-loop: a task's git checkout -- <file> recovery silently discards earlier tasks' uncommitted edits to that file #23

Closed
opened 2026-07-07 16:30:06 +02:00 by Brummel · 2 comments
Owner

Filed at the user's request during a /boss run on the aura project. This
records an observed workflow behaviour only; whether any change follows is for a
later session to decide (the instruction was to describe what happened, not to
prescribe a fix).

What happened

During an implement-loop run (standard mode, a 5-task plan for a large deletion
cut — swap three call sites, rewrite/delete tests, then delete a function and its
transitive closure within one crate), the workflow returned PARTIAL: Tasks 1–3
reported DONE, Task 4 BLOCKED.

The final working tree did not match those per-task reports. The edits Task 1
and Task 2 had made to crates/aura-cli/src/main.rs (the call-site swaps and the
test rewrites/deletions) were absent from the tree, although both tasks
reported DONE. Edits made by Task 3 to other files (a separate integration-test
file, two deleted fixtures, three more test files) were present. Task 4 then
blocked because the call sites Task 1 was supposed to have swapped were still in
their original form, so the function being deleted still had live callers — a
report-versus-tree inconsistency that only surfaced on the orchestrator's own
post-run diff inspection.

Evidence

The run's agent transcript contains a working-tree file checkout issued by an
agent:

git checkout -- crates/aura-cli/src/main.rs && grep -n …

git checkout -- <file> (equivalently git restore <file>) discards all
uncommitted edits to that file, not only the issuing task's own. Because every
task in this run wrote to the same main.rs and the loop never commits between
tasks, that one command threw away the main.rs edits accumulated by the earlier
tasks in the same run. Files other than main.rs were untouched by the checkout,
which is why Task 3's cross-file edits survived while Task 1/2's same-file edits
did not — producing both the tree/report mismatch and the downstream block. The
most likely trigger is an implementer recovering from an intermediate
non-compiling state during the atomic deletion by resetting the file it was
editing.

Where in the plugin

  • implement/SKILL.md — the Iron Law forbids git reset / git revert on
    main and states the loop "only ever writes unstaged changes", but does not
    address a working-tree git checkout -- <file> / git restore <file> that
    discards a sibling task's uncommitted work in a file several tasks share.
  • agents/implementer.md — the per-task implementer agent that ran the checkout.

The report claimed the two earlier tasks were DONE; only a manual diff of the
working tree against the pre-run commit revealed their edits were gone.

> Filed at the user's request during a `/boss` run on the **aura** project. This > records an observed workflow behaviour only; whether any change follows is for a > later session to decide (the instruction was to describe what happened, not to > prescribe a fix). ## What happened During an `implement-loop` run (standard mode, a 5-task plan for a large deletion cut — swap three call sites, rewrite/delete tests, then delete a function and its transitive closure within one crate), the workflow returned PARTIAL: Tasks 1–3 reported DONE, Task 4 BLOCKED. The final working tree did **not** match those per-task reports. The edits Task 1 and Task 2 had made to `crates/aura-cli/src/main.rs` (the call-site swaps and the test rewrites/deletions) were **absent** from the tree, although both tasks reported DONE. Edits made by Task 3 to *other* files (a separate integration-test file, two deleted fixtures, three more test files) were present. Task 4 then blocked because the call sites Task 1 was supposed to have swapped were still in their original form, so the function being deleted still had live callers — a report-versus-tree inconsistency that only surfaced on the orchestrator's own post-run diff inspection. ## Evidence The run's agent transcript contains a working-tree file checkout issued by an agent: ``` git checkout -- crates/aura-cli/src/main.rs && grep -n … ``` `git checkout -- <file>` (equivalently `git restore <file>`) discards **all** uncommitted edits to that file, not only the issuing task's own. Because every task in this run wrote to the same `main.rs` and the loop never commits between tasks, that one command threw away the `main.rs` edits accumulated by the earlier tasks in the same run. Files other than `main.rs` were untouched by the checkout, which is why Task 3's cross-file edits survived while Task 1/2's same-file edits did not — producing both the tree/report mismatch and the downstream block. The most likely trigger is an implementer recovering from an intermediate non-compiling state during the atomic deletion by resetting the file it was editing. ## Where in the plugin - `implement/SKILL.md` — the Iron Law forbids `git reset` / `git revert` on `main` and states the loop "only ever writes unstaged changes", but does not address a working-tree `git checkout -- <file>` / `git restore <file>` that discards a **sibling task's** uncommitted work in a file several tasks share. - `agents/implementer.md` — the per-task implementer agent that ran the checkout. The report claimed the two earlier tasks were DONE; only a manual diff of the working tree against the pre-run commit revealed their edits were gone.
Brummel added the bug label 2026-07-07 16:30:06 +02:00
Author
Owner

Triage verification at commit dacadba. The observed behaviour is consistent with the current plugin text, with one sharpening correction: the implementer is handed the discard tool explicitly.

Confirmed — no guard exists at the failure's granularity. The Iron Law (implement/SKILL.md:100-101) covers reset/revert and the no-commit discipline only; the file's other git checkout -- . mentions (implement/SKILL.md:256, :297) are orchestrator-level whole-tree disposition after the loop ends. implement/workflows/implement-loop.js has no per-task snapshot or post-task presence check: diff_fingerprint (schema at implement-loop.js:201-208, loop check at :528-537) hashes the full git diff HEAD but only detects a same-task no-progress cycle, and the end-of-iteration tree gate counts changed files — a file whose earlier-task content was wiped still counts as changed, so the wipe is invisible to it.

Correction to "contains no guidance mentioning checkout". implement/agents/implementer.md:163-166 (Step 8, self-review) sanctions git checkout -- <path> for undoing the agent's own out-of-task-text edits — with no caution that the loop never commits between tasks, so a shared file may hold earlier tasks' already-reviewed DONE work. The in-loop reviewers cannot catch the loss either: both are scoped to the current task's footprint, with chunks outside it declared not their concern (implement/agents/spec-reviewer.md:147-150, implement/agents/quality-reviewer.md:67 and :141-142). The failure class is therefore not merely un-forbidden but structurally invisible to every in-loop gate; only a post-run tree-versus-report diff surfaces it, which matches how this instance was found.

Two fix layers separate cleanly:

  • (a) prose guard — a caution in implementer.md Step 8 plus an Iron-Law line naming the shared-file discard risk (undo own over-reach hunk-by-hunk, never by whole-file checkout; escalate to BLOCKED instead of discarding). Follows directly from this report.
  • (b) mechanical guard — a per-file content snapshot after each DONE task with a presence re-check before the next task starts. Granularity and cost are unsettled; this report deliberately prescribes nothing, so (b) is an open design decision.
Triage verification at commit dacadba. The observed behaviour is consistent with the current plugin text, with one sharpening correction: the implementer is handed the discard tool explicitly. **Confirmed — no guard exists at the failure's granularity.** The Iron Law (`implement/SKILL.md:100-101`) covers reset/revert and the no-commit discipline only; the file's other `git checkout -- .` mentions (`implement/SKILL.md:256`, `:297`) are orchestrator-level whole-tree disposition after the loop ends. `implement/workflows/implement-loop.js` has no per-task snapshot or post-task presence check: `diff_fingerprint` (schema at `implement-loop.js:201-208`, loop check at `:528-537`) hashes the full `git diff HEAD` but only detects a same-task no-progress cycle, and the end-of-iteration tree gate counts changed files — a file whose earlier-task content was wiped still counts as changed, so the wipe is invisible to it. **Correction to "contains no guidance mentioning checkout".** `implement/agents/implementer.md:163-166` (Step 8, self-review) sanctions `git checkout -- <path>` for undoing the agent's own out-of-task-text edits — with no caution that the loop never commits between tasks, so a shared file may hold earlier tasks' already-reviewed DONE work. The in-loop reviewers cannot catch the loss either: both are scoped to the current task's footprint, with chunks outside it declared not their concern (`implement/agents/spec-reviewer.md:147-150`, `implement/agents/quality-reviewer.md:67` and `:141-142`). The failure class is therefore not merely un-forbidden but structurally invisible to every in-loop gate; only a post-run tree-versus-report diff surfaces it, which matches how this instance was found. Two fix layers separate cleanly: - (a) prose guard — a caution in implementer.md Step 8 plus an Iron-Law line naming the shared-file discard risk (undo own over-reach hunk-by-hunk, never by whole-file checkout; escalate to BLOCKED instead of discarding). Follows directly from this report. - (b) mechanical guard — a per-file content snapshot after each DONE task with a presence re-check before the next task starts. Granularity and cost are unsettled; this report deliberately prescribes nothing, so (b) is an open design decision.
Author
Owner

Design resolution for the mechanical-guard layer left open in the triage (fix layer (b) — the prose layer (a) followed directly from the report and needed no decision).

  • Fork: where the guard lives — options: a harness-level prevention (a hook blocking git checkout --/git restore for loop sessions) vs. a guard inside the plugin's own loop script. → Plugin-side only. Basis: the change must stay within the skills project; a global harness manipulation is ruled out — user decision, 2026-07-09.
  • Fork: guard character (prevent / detect / recover) — → snapshot + coarse detector in implement/workflows/implement-loop.js. Basis: derived — with prevention excluded per the fork above, detection needs a recovery anchor to be worth its cost: git stash create yields a dangling commit per task boundary without touching HEAD, index, or working tree (Iron-Law-compatible: nothing commits, nothing moves), so any loss is both detectable and reconstructable (git show <sha>:<path>).
  • Fork: detection semantics — options: hunk-hash persistence with per-task ownership vs. a coarse boundary comparison. → Coarse: a path HEAD-modified at boundary N that is absent from git diff HEAD --name-only at boundary N+1 trips the guard. Basis: derived — later tasks legitimately rewrite earlier tasks' edits in shared files (the observed run's plan did exactly that), so hunk-persistence semantics false-positive on plan-intended rewrites; the recovery snapshot lowers the cost of the coarse detector's known false negative (a checkout followed by re-edits of the same file escapes the comparison but stays diagnosable post-hoc). Untracked files sit outside the threat model: git checkout -- <path> cannot discard them.
  • Fork: reaction on detection — options: annotate the end-report / invalidate and re-queue / hard stop. → Hard BLOCKED, loop stops at the tripped boundary. Basis: derived — a tripped guard means the per-task DONE reports and the tree have diverged, so neither DONE nor PARTIAL is a trustworthy commit signal; the end-report names the lost paths, the boundary, and the recovery snapshot sha.
Design resolution for the mechanical-guard layer left open in the triage ([fix layer (b)](issues/23#issuecomment-3021) — the prose layer (a) followed directly from the report and needed no decision). - **Fork: where the guard lives** — options: a harness-level prevention (a hook blocking `git checkout --`/`git restore` for loop sessions) vs. a guard inside the plugin's own loop script. → **Plugin-side only.** Basis: the change must stay within the skills project; a global harness manipulation is ruled out — user decision, 2026-07-09. - **Fork: guard character (prevent / detect / recover)** — → **snapshot + coarse detector in `implement/workflows/implement-loop.js`.** Basis: derived — with prevention excluded per the fork above, detection needs a recovery anchor to be worth its cost: `git stash create` yields a dangling commit per task boundary without touching HEAD, index, or working tree (Iron-Law-compatible: nothing commits, nothing moves), so any loss is both detectable and reconstructable (`git show <sha>:<path>`). - **Fork: detection semantics** — options: hunk-hash persistence with per-task ownership vs. a coarse boundary comparison. → **Coarse: a path HEAD-modified at boundary N that is absent from `git diff HEAD --name-only` at boundary N+1 trips the guard.** Basis: derived — later tasks legitimately rewrite earlier tasks' edits in shared files (the observed run's plan did exactly that), so hunk-persistence semantics false-positive on plan-intended rewrites; the recovery snapshot lowers the cost of the coarse detector's known false negative (a checkout followed by re-edits of the same file escapes the comparison but stays diagnosable post-hoc). Untracked files sit outside the threat model: `git checkout -- <path>` cannot discard them. - **Fork: reaction on detection** — options: annotate the end-report / invalidate and re-queue / hard stop. → **Hard `BLOCKED`, loop stops at the tripped boundary.** Basis: derived — a tripped guard means the per-task DONE reports and the tree have diverged, so neither DONE nor PARTIAL is a trustworthy commit signal; the end-report names the lost paths, the boundary, and the recovery snapshot sha.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Skills#23