Recovering a Worktree Left Dirty by an Interrupted Chunk
When gantry dies mid-chunk, resume discards the unverified work and rebuilds from the last committed green state.
Gantry builds a plan chunk by chunk, and it commits each chunk only once that chunk's work has passed the test suite. A finished chunk is a commit; an in-flight chunk is uncommitted edits sitting in the run's working tree. That rule is what makes a crash cheap: whatever was already finished is safely committed, so resuming is just picking up the next unfinished chunk.
But there was a gap. If gantry's owner process died in the middle of a chunk, the last finished chunk was committed while the interrupted chunk's half-written edits were left behind in the working tree. On resume, gantry ran its opening health check against that tree, found it broken, and stopped — the motivating incident was a resume that halted on a wall of compile errors and could only be rescued by hand. The uncommitted edits were unverified and unwanted, but nothing threw them away.
This job closes that gap. On resume, before anything else runs, gantry checks whether the working tree is dirty; if it is, it discards every uncommitted change, returning the tree to its last committed state, and lets the normal resume re-derive the interrupted chunk as still pending and rebuild it from that clean, known-good starting point. No human, no manual cleanup. And because throwing work away silently would be its own hazard, the recovery leaves durable records in the run's history, so it reads as a deliberate step rather than a mystery retry.
Build
The work split into a git mechanic, the decision of when to invoke it, and the records that make it auditable — three pieces sequenced so each was testable before the next relied on it.
The decomposition put the raw git operation first, on its own. One sprint added a working-tree method that hard-resets to the last commit and reports the short SHA it landed on, with tests over a throwaway repository that dirty the tree three ways — a modified tracked file, a staged-but-uncommitted file, an untracked file in a new directory — and prove all three are gone afterward while committed history is untouched. Isolating the mechanic here meant the next sprint could reason purely about *when* to reset, never about *how*, and could lean on a primitive already proven in isolation.
The second sprint placed the decision: a single run-level check, after the working tree is prepared but before any driver or its health check runs, keyed on dirtiness alone rather than on ledger forensics. The brief argues that boundary explicitly — an unnecessary reset costs at most one chunk of re-work, while trusting unverified intermediate edits risks landing a subtly wrong commit, so keying on the tree simply being dirty is both simpler and biased toward safety. The third sprint made the recovery legible: a record naming the commit reset to, and a terminal record that closes out the execute stage the crash left dangling, both riding the journal-and-replay machinery every other stage event already uses.
Every gate in this run passed on the first attempt. The one place the cut moved was a review at the end of the second sprint, which forced a re-plan of the remaining work. The tension that re-plan sat on is visible in the third sprint's brief: the record that proves an interruption happened lives in the run's own history journal — the very uncommitted tail a blanket reset would erase. So the orphaned stage has to be detected *before* the reset and its closing record written *after*, into the rebuilt journal. That ordering constraint, between throwing the tree away and keeping the run's own audit trail, is where the difficulty actually lived, and the re-plan is where the boundary between making recovery happen and making it auditable got redrawn.
Feature
Resume no longer hard-stops on a working tree left dirty by a mid-chunk crash; it discards the unverified work, rebuilds from the last commit, and records the recovery — a design that still stands, folded into a bookkeeping-preserving form.
Before this job, a resume after a mid-chunk death met a broken working tree and stopped, and the only way forward was a manual clean. The design that answers it is a single run-level check on resume: if the tree is dirty and the driver is one that commits per chunk, gantry resets to the last commit, re-derives the interrupted chunk as pending, and rebuilds it — while a clean tree is left exactly as before. Two durable records make the recovery auditable: one naming the commit reset to, one closing the execute stage the crash left open so `gantry status` reflects the interruption without anyone reading raw logs.
Standing in the tree today, the whole design is present and load-bearing. The resume site in the build engine still performs the dirty-check-and-reset, the history journal still carries the reset and interrupted-stage records with round-trip tests, and the working-tree module still holds the reset primitives. Most of the introduced code survives, and none of the touched files have been removed.
Two things have since been refined rather than undone. The exclusion the observability sprint wrote for the map driver — whose completed-but-uncommitted unit artifacts are its own resume ground truth, so its dirty tree must be left alone — is no longer a special case in the reset code; it is now a declarative capability each driver carries, with the commit-per-chunk drivers opting in and map opting out. And the reset actually wired into resume now preserves gantry's own orchestration directory instead of wiping it, so the history journal that proves the interruption survives the reset directly. The blanket whole-tree reset primitive built first still exists but is retained unused, superseded by the preserving variant — the same feature, reshaped by exactly the audit-trail tension the run's re-plan surfaced.