Repository navigation
Description
Problem or Motivation
The stop of #2636 reads "The agent moved HEAD", after 7,166,584 input tokens. The text falls to class head_moved with action retry, and the failure gets no repair turn. A wider refusal of git commands in the shell is not the answer: refused commands are the largest stop class, and #2695 records the sandbox decision. Split from #3053.
Proposed Solution & Architectural Design
- When the head differs from the head that the keeper expects (
WipKeeper.expected_head), the job runsgit reset --soft <expected head>and keeps the changes in the worktree. The checks then judge the result. - Acceptance: a stub agent that commits during a step ends with the changes in the worktree and the checks run on them; the step stops with
head_movedonly when the reset fails. - Nobody tested this repair on a real run, so the premise needs a test with a real git repository first.
Target Subsystem
src/devops_cli/roadmap/implement_step.py (_exit_failure).
Existing Tool Considered
git covers the reset. The project adds the call in the step: about 20000 tokens to build, 1000 each quarter to keep, 5000 to fix one miss, about 1 hour.
Source: #3053
Proposed design
Problem statement
A stop of the implement job reports head_moved when the agent makes a commit during a step. The step then stops, discards the work and gets no repair turn. The change resets the head to the commit that the keeper expects and lets the exit checks judge the work. The step stops with head_moved only when the reset fails.
Acceptance criteria
Requirement: A step resets a head that the agent moved
When the head of the checkout differs from the expected head of the keeper, the step SHALL move the head back with git reset --soft and keep the worktree as it is.
Scenario: A stub agent commits during a step
- WHEN a real git repository holds the checkout and the stub agent makes a commit during a step
- THEN the step ends done and its commit holds the change that the agent committed.
Requirement: A failed reset stops the step with head_moved
The step MUST stop with the class head_moved only when the reset to the expected head exits with a status other than 0.
Scenario: The reset fails
- WHEN
git reset --softto the expected head exits with a status other than 0 - THEN the step ends stopped with the class
head_moved, and the reason names the reset command and the output of git.
Requirement: The reset keeps the wip: commits of the keeper
The step SHALL reset the head to the expected head of the keeper, so the wip: commits of the step stay in the history.
Scenario: The agent commits after a wip: commit
- WHEN the keeper made a
wip:commit and the agent then makes its own commit in the step - THEN the history still holds the
wip:commit, and the commit of the step folds it in.
Requirement: The reset shows in the Job log
The step SHALL print one muted line that names the moved head and the expected head.
Scenario: The step resets a moved head
- WHEN the step resets a head that the agent moved
- THEN the Job log holds one line that names the moved head and the expected head.
Requirement: The spec states the reset of a moved head
The change SHALL modify the requirement A step makes the only commit of roadmap-implement-step to state the reset of a head that the agent moved.
Scenario: The change is archived
- WHEN
openspec archivewrites the spec deltas of the change intoopenspec/specs/roadmap-implement-step/spec.md - THEN the requirement states the reset, and the stop with
head_movedonly when the reset fails.
Existing Tool Considered
No check has run. git covers the reset with git reset --soft, and it is open, maintained and installed in the runner. The project adds one call in the step. The written cost is about 20000 tokens to build, 1000 each quarter to keep, 5000 to fix one miss, and about 1 hour.
Key questions
- What does
_exit_failuredo today when the head differs from the head that the keeper expects?: It returns a failure with the classhead_movedbefore any check runs, so the step stops andkeeper.discard()resets the checkout, which discards the work. [sources: src/devops_cli/roadmap/implement_step.py, docs/ROADMAP_WORKFLOW.md:157, docs/ROADMAP_WORKFLOW.md:175] - Which git reset mode does the step use?:
git reset --soft <expected head>. It moves HEAD only, so the index and the worktree keep the work of the agent. (best practice) [sources: src/devops_cli/roadmap/implement_step.py]- Chosen:
git reset --soft. It moves HEAD only. The index and the worktree keep the work of the agent, and the latergit add -Astages it. - Rejected:
git reset --mixed. It also resets the index, so the work survives only as unstaged changes and the staged checks need a second add. - Rejected:
git reset --hard. It deletes the work of the agent, which is the defect that this issue fixes.
- Chosen:
- Where does the reset run?: In
_exit_failure, at the head check and beforekeeper.fold(), so it also runs after an extra turn. (best practice) [sources: src/devops_cli/roadmap/implement_step.py]- Chosen: In
_exit_failureat the head check. Every exit check runs it, also the one after an extra turn, and it runs before the keeper folds. - Rejected: In
_turnafter the run. It misses the extra turn, which also commits, and the head check of_exit_failurewould still stop.
- Chosen: In
- What does the step do when the reset fails?: It stops with the class
head_moved, and its reason names the reset command and the output of git. (best practice) [sources: src/devops_cli/roadmap/failure.py, docs/ROADMAP_WORKFLOW.md:175]- Chosen: Stop with
head_moved. The class keeps its row inACTION_BY_CLASS, and the stop comment names the cause and the output of git. - Rejected: Retry the reset once. A second reset fails for the same reason, and it adds a path that no check covers.
- Chosen: Stop with
- What happens to the
wip:commits of the keeper when the step resets the head?: The reset moves the head tokeeper.expected_head, which is the tip after the lastwip:commit. Those commits stay, andkeeper.fold()folds them into the commit of the step. (best practice) [sources: src/devops_cli/roadmap/implement_wip.py, src/devops_cli/roadmap/implement_step.py]- Chosen: Reset to
keeper.expected_head. Thewip:commits of the step stay, and the keeper folds them into the commit of the step. - Rejected: Reset to
keeper.base. It discards thewip:commits that the step kept, which loses verified work.
- Chosen: Reset to
- How does the test prove the repair on a real git repository?: A test creates a repository at
tmp_pathwithgit initand a first commit, runs a stub agent that commits during the step, and asserts that the step ends done with the change in the worktree. (best practice) [sources: tests/roadmap/test_roadmap_implement_step.py, src/devops_cli/roadmap/implement_step.py]- Chosen: A real git repository at
tmp_path. It shows the real behaviour ofgit reset --softand of the keeper on the history. - Rejected: A fake git through the
gitcallback of the keeper. A fake shows the call and not the state of the history, and_exit_failurecalls git itself, so a fake never reaches the reset.
- Chosen: A real git repository at
- Does the change add a failure class or an action?: No.
head_movedkeeps its rowretryinACTION_BY_CLASS, and the reset makes the stop rare. [sources: src/devops_cli/roadmap/failure.py, docs/ROADMAP_WORKFLOW.md:175]
Fit
Fits in one pull request.
Suspected block
None.
Dependencies
None.
Activity
dan-petty commentedon Nov 5, 2026
Intake placed this item to the backlog: intake puts every new item in the backlog. A person plans the milestones.
fixprefix of the title gives the type.(roadmap)scope of the title gives the scope.feat(roadmap): a step stops at its budgets and records attempts (#3580)
devops-cli-service commentedon Nov 6, 2026
Status set to Ready at 29309f0 (see proposed design in issue body).
feat(roadmap): ask for a smaller edit after a parser failure (#3737)
chore(main): release 0.77.0 (#3888)
devops-cli-service commentedon Nov 7, 2026
Released in v0.77.0: https://github.com/dan-petty/devops-cli/releases/tag/v0.77.0
devops-cli-service commentedon Nov 7, 2026
Live since 2026-11-07 05:06 UTC