Skip to content

A task reopened by task_verify failed is redispatched onto its old, already-merged branch with no prompt pointer to the verification evidence; the worker re-submits the merged work - #1976

Merged
ryanrborn merged 5 commits into
mainfrom
bugfix/1959-task-reopened-by-task-verify-failed
Sep 22, 2026

Conversation

@ryanrborn

@ryanrborn ryanrborn commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

A task reopened by task_verify failed was redispatched onto its old,
already-merged branch with no new commits, which produced an empty PR
(#1957 for bd-96mn8i) instead of new work fixing the reported defect.

  • Dispatch.maybe_provision_worktree/2 now runs Worktree.reset_if_merged/4
    before create/3. When the task's last verification outcome was :failed,
    it forces a reset even though this repo's default GitHub merge method
    (squash) means the old branch tip is never a git-ancestor of main — plain
    merge-base ancestry alone never catches that case.
  • The "stale branch" check is git merge-tree --write-tree <ref> <branch>
    producing the same tree as <ref> itself — i.e. merging the branch in
    would change nothing. This correctly fires right after a squash lands
    (any number of squashed commits), keeps firing even after unrelated PRs
    land on main afterward, and stops firing the moment new work is
    committed on the branch (so a later redispatch in the same re-work round,
    while verification_outcome is still :failed, doesn't throw away that
    new work).
  • A resumed dispatch (opts[:resume]) skips the reset entirely, and a live
    worktree with uncommitted changes is never hard-reset even under
    force: true — both guard against arb resume losing in-progress work.
  • The work prompt now includes a verification_evidence section right
    after Acceptance, under a heading stating the merged fix failed in
    production, whenever verification_outcome == :failed.

Went through 3 rounds of internal review; round 3's finding was that the
prior tree-equality check (git diff --quiet ref branch) only matched
immediately after the squash landed — once anything else merged into
main, the trees diverged and the stale branch was wrongly kept again.
Switched to git merge-tree --write-tree, which is robust to main
advancing.

Test plan

  • mix test on worktree_test.exs, dispatch_test.exs,
    prompt_builder_test.exs — 249 tests, 0 failures.
  • mix format --check-formatted on touched files — clean.
  • mix credo on touched lib files — no issues.
  • Full mix test (umbrella mix precommit) — failures are pre-existing,
    unrelated ProvisioningTest (hardcoded worktree path assertions) and
    arbiter_cli (live dev workspace not named "default") failures,
    reproduced independently of this change.
  • New test reproduces the round-3 scenario directly: squash-merge a
    2-commit branch onto main, then push an unrelated commit to main
    before redispatching — force: true still resets.
  • Existing test proves a round-2 commit on a force-reset branch survives
    a later redispatch while verification_outcome is still :failed.

References

bd-8ssxap

Closes #1959

…ilures in the prompt (bd-8ssxap)

A task reopened by `task_verify failed` had its worktree already torn down
by `:await_verification`'s CleanupWorktree hook, leaving only a branch ref
whose commits were already merged into main. Redispatch reused that stale
branch as-is (Worktree.create's "already exists" fallback just checks it
out), so the worker had nothing new to add and could submit an empty PR.

Worktree.reset_if_merged/3 now checks both a live worktree directory and a
surviving branch ref: if the branch's tip is already an ancestor of the
current origin/<base>, it force-resets (or force-moves) it to that tip
before Dispatch.create/3 runs, so redispatch always starts from current
upstream. Dispatch calls this before provisioning. PromptBuilder also now
puts verification_outcome/verification_evidence front and center in the
work prompt when the prior round's merged fix failed verification, so the
worker knows to do new work rather than resubmit what already landed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ryanrborn and others added 4 commits September 22, 2026 14:00
…t a resumed/dirty worktree (bd-8ssxap)

reset_if_merged/3 only trusted `merge-base --is-ancestor`, which never
holds after a squash merge (this repo's default GitHub merge method) —
the old branch tip is never an ancestor of the new squash commit on
main, so the exact bd-96mn8i redispatch-onto-stale-branch bug still
reproduced. Add an explicit `force: true` option, driven from
`task.verification_outcome == :failed` in Dispatch, so a squash-merged
branch is caught even though ancestry says otherwise.

Guard the destructive side: `reset --hard` now refuses to run on a
worktree with uncommitted changes (has_uncommitted?/1), and Dispatch
skips the reset entirely on any `resume` — resume exists to preserve a
stopped worker's committed and uncommitted state, and force-driven
resets must never destroy that.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…credo disable placement (bd-8ssxap)

verification_outcome stays :failed for the whole re-work round (it only
clears once the next PR merges), so a naive `force? or ancestor?` reset
in reset_if_merged/4 would also wipe round-2 commits made after the
first, legitimate reset. force: true now only resets when the branch's
tree content is still identical to the base tip (git diff --quiet), not
merely whenever the task's verification_outcome is :failed.

Also move resuming?/1 and its comment above the
credo:disable-for-next-line so it again sits directly on
maybe_provision_worktree, restoring the cyclomatic-complexity suppression
credo needs there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… tree equality (bd-8ssxap)

git diff --quiet ref branch only matches right after the squash lands;
any later unrelated merge into base makes the trees diverge even though
the branch still contributes nothing, so the stale branch was wrongly
kept — reintroducing the incident on a busy fleet. git merge-tree
--write-tree ref branch compares what merging would produce against
base's own tree, which stays a match regardless of how much base has
since moved on, while a genuine new commit on the branch still diverges
and is kept.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ryanrborn
ryanrborn merged commit 4e04501 into main Sep 22, 2026
5 of 6 checks passed
@ryanrborn
ryanrborn deleted the bugfix/1959-task-reopened-by-task-verify-failed branch September 22, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant