Follow-up from #109 (split out of PR #122, which fixes the root cause for the Claude adapter).
Problem
When a dev session dies early — before writing any code — the working tree's only divergence from the attempt baseline is often the spec file's own 2-line frontmatter flip (status: ready-for-dev → in-progress), written by the skill as its first act. On the retry decision with the default scm.rollback_on_failure = false + isolation = "none", that flip counts as attempt dirtiness, so _rollback_or_pause pauses the run with the manual-recovery notice instead of retrying.
For the operator this reads as "the loop keeps needing manual rollback / hitting git trouble" when nothing of value would be lost by resetting: the residue is a file whose lifecycle the orchestrator itself owns, and the retry re-drives it from ready-for-dev anyway.
Where it lives
Engine._rollback_or_pause (engine.py): the dirty check excludes the orchestrator-owned artifact folders (_protected_relpaths() — output/implementation/planning) only on the resolved-redrive path:
protected = self._protected_relpaths() if redrive else ()
if task.baseline_commit and not verify.attempt_dirty(..., exclude=protected):
self.journal.append("rollback-skipped-clean", ...)
return
A plain stopped/crashed attempt passes exclude=(), so the spec flip alone is enough to trigger the pause.
Design notes / cautions
- This deliberately interacts with rearm/baseline semantics (the re-arm paths strip stale
## Auto Run Result sections; the resolved-redrive carve-out exists precisely so a corrected spec doesn't read as a failed attempt). Any relaxation needs to keep those invariants.
- Scope question: exclude the full protected folders on the plain path too, or only the dispatched story's own spec file? The narrow version (spec-file-only diff → auto-restore +
rollback-skipped-clean-style journal entry) is the safer shape: a session that wrote real code into an artifact folder should still pause.
- Stories mode: the spec folder (
[stories] spec_folder) is user-chosen and not necessarily inside _protected_relpaths() — the carve-out needs to cover the id-keyed story spec path as well.
- Whatever resets/keeps the flip must leave the retried session's read-back deterministic: the next attempt's
synthesize_result must not mistake leftover state for this session's output (mtime launch-floor already guards the read side).
Follow-up from #109 (split out of PR #122, which fixes the root cause for the Claude adapter).
Problem
When a dev session dies early — before writing any code — the working tree's only divergence from the attempt baseline is often the spec file's own 2-line frontmatter flip (
status: ready-for-dev → in-progress), written by the skill as its first act. On the retry decision with the defaultscm.rollback_on_failure = false+isolation = "none", that flip counts as attempt dirtiness, so_rollback_or_pausepauses the run with the manual-recovery notice instead of retrying.For the operator this reads as "the loop keeps needing manual rollback / hitting git trouble" when nothing of value would be lost by resetting: the residue is a file whose lifecycle the orchestrator itself owns, and the retry re-drives it from
ready-for-devanyway.Where it lives
Engine._rollback_or_pause(engine.py): the dirty check excludes the orchestrator-owned artifact folders (_protected_relpaths()— output/implementation/planning) only on the resolved-redrive path:A plain stopped/crashed attempt passes
exclude=(), so the spec flip alone is enough to trigger the pause.Design notes / cautions
## Auto Run Resultsections; the resolved-redrive carve-out exists precisely so a corrected spec doesn't read as a failed attempt). Any relaxation needs to keep those invariants.rollback-skipped-clean-style journal entry) is the safer shape: a session that wrote real code into an artifact folder should still pause.[stories] spec_folder) is user-chosen and not necessarily inside_protected_relpaths()— the carve-out needs to cover the id-keyed story spec path as well.synthesize_resultmust not mistake leftover state for this session's output (mtime launch-floor already guards the read side).