Skip to content

fix(roadmap): a step whose agent moved HEAD is reset, not stopped #3575

Description

@dan-petty

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 runs git 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_moved only 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 --soft to 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 archive writes the spec deltas of the change into openspec/specs/roadmap-implement-step/spec.md
  • THEN the requirement states the reset, and the stop with head_moved only 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_failure do today when the head differs from the head that the keeper expects?: It returns a failure with the class head_moved before any check runs, so the step stops and keeper.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 later git add -A stages 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.
  • Where does the reset run?: In _exit_failure, at the head check and before keeper.fold(), so it also runs after an extra turn. (best practice) [sources: src/devops_cli/roadmap/implement_step.py]
    • Chosen: In _exit_failure at the head check. Every exit check runs it, also the one after an extra turn, and it runs before the keeper folds.
    • Rejected: In _turn after the run. It misses the extra turn, which also commits, and the head check of _exit_failure would still stop.
  • 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 in ACTION_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.
  • What happens to the wip: commits of the keeper when the step resets the head?: The reset moves the head to keeper.expected_head, which is the tip after the last wip: commit. Those commits stay, and keeper.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. The wip: commits of the step stay, and the keeper folds them into the commit of the step.
    • Rejected: Reset to keeper.base. It discards the wip: commits that the step kept, which loses verified work.
  • How does the test prove the repair on a real git repository?: A test creates a repository at tmp_path with git init and 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 of git reset --soft and of the keeper on the history.
    • Rejected: A fake git through the git callback of the keeper. A fake shows the call and not the state of the history, and _exit_failure calls git itself, so a fake never reaches the reset.
  • Does the change add a failure class or an action?: No. head_moved keeps its row retry in ACTION_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

  1. added
    type/bugDefect, regression, or incorrect runtime behavior
    scope/roadmapThe roadmap jobs: intake, refine, stage, implement and the Service that runs them
    status/newStatus New: the item is not ready yet
    effort/lowEffort Low: little work
    on Nov 5, 2026
  2. dan-petty commented on Nov 5, 2026

    @dan-petty
    OwnerAuthor

    Intake placed this item to the backlog: intake puts every new item in the backlog. A person plans the milestones.

    • Type: type/bug. The fix prefix of the title gives the type.
    • Scope: scope/roadmap. The (roadmap) scope of the title gives the scope.
    • Priority: P2-Medium.
    • Value: Medium.
    • Effort: Low.
  3. added
    priority/p2-mediumStandard priority feature work and architectural improvements
    on Nov 5, 2026
  4. added
    status/readyStatus Ready: problem, acceptance criteria and an answer to each key question
    and removed
    status/newStatus New: the item is not ready yet
    on Nov 6, 2026
  5. devops-cli-service commented on Nov 6, 2026

    @devops-cli-service
    Contributor

    Status set to Ready at 29309f0 (see proposed design in issue body).

  6. added
    owner/agentOwner Agent: an agentic coder works on the item
    status/in-progressActively being designed or implemented by developer or agent
    and removed
    status/readyStatus Ready: problem, acceptance criteria and an answer to each key question
    on Nov 6, 2026
  7. removed
    status/in-progressActively being designed or implemented by developer or agent
    owner/agentOwner Agent: an agentic coder works on the item
    on Nov 7, 2026
  8. added a commit that references this issue on Nov 7, 2026
  9. devops-cli-service commented on Nov 7, 2026

    @devops-cli-service
    Contributor
  10. devops-cli-service commented on Nov 7, 2026

    @devops-cli-service
    Contributor

    Live since 2026-11-07 05:06 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/lowEffort Low: little workpriority/p2-mediumStandard priority feature work and architectural improvementsscope/roadmapThe roadmap jobs: intake, refine, stage, implement and the Service that runs themsource/agentFiled by an agenttype/bugDefect, regression, or incorrect runtime behaviorvalue/mediumValue Medium

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions