feat(merge): verify once per batch via skipVerify + verify_merges

Per-merge verify made an N-task batch pay N x ~7 min, each run testing
the same moving main. review_task/merge_task/continue_merge now take
skipVerify: the merge lands but the gate AND the Done transition are
deferred (status merged_verify_pending, task stays WaitingForReview).
The new verify_merges tool runs the list verify command once for the
explicitly listed tasks and promotes them to Done on success - explicit
ids so the handler own submitted task and parked verify_failed tasks
are never swept up; an Active worktree is refused per entry.

PlanningMergeOrchestrator threads the flag through the unit merge (no
more per-child verify) and skips FinalizeParentDoneAsync when verifies
were deferred. The merge-helper Merge prompt approves with
skipVerify=true and calls verify_merges once after the last merge. UI
approve is unchanged; the no-Done-without-green-verify invariant stays
server-enforced.
This commit is contained in:
mika kuns
2026-08-26 15:29:20 +02:00
parent c4928b4def
commit 168ab1cd2f
8 changed files with 491 additions and 41 deletions
+10 -4
View File
@@ -508,9 +508,8 @@ public static class PromptFiles
1. Never read a task's diff yourself. Instead start a sonnet subagent (Task tool) per task to inspect get_task_diff and sanity-check it against the task's title and description; only its short verdict (clean / risky + reason) comes back into your own context. If preview_merge_set flagged this task in an overlap, tell the subagent to also skim the diff of the other task(s) sharing that file.
2. Before approving, cross-check changedFileCount from preview_merge_set (or the length of get_task_diff's files list the diff tool has no changedFileCount field). 0 changed files means an empty branch, not a clean success do not approve it; treat it like a risky diff instead.
3. If the subagent's verdict is risky, or the change looks wrong or incomplete, STOP and ask the user before merging offer reject_rerun (with feedback) or skip.
4. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true).
- Clean merge, verify gate passes (or no verify command is set) the task is Done; move on.
- Clean merge, verify gate fails the merge stands, but the Done transition is deliberately withheld. Report this task as merged-but-not-Done and move on do NOT try to fix the verify failure yourself; that is separate follow-up work.
4. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true, skipVerify=true). skipVerify defers the list's verify command to ONE verify_merges call after the last merge (below) instead of paying it per merge.
- Clean merge mergeStatus "merged_verify_pending" (expected: the task deliberately stays WaitingForReview until verify_merges passes) or "merged" (list has no verify command; the task is Done). Either way, move on.
- Conflict (markers left in the working tree, repoPath returned) resolve it below.
After each merge, main has moved a clean preview_merge_set result from before this merge is now stale for the remaining tasks. If you are unsure whether an earlier overlap warning still matters, call preview_merge for the next task again before merging it.
@@ -518,12 +517,19 @@ public static class PromptFiles
Every branch in this run forked from the same base, so conflicts between them are the NORMAL case, not a failure. Resolve them and keep going; do not abandon the run because a merge conflicted.
Resolving a conflict do this yourself, never via a subagent, since every subagent would share this session's same main checkout:
- Open each conflicted file under repoPath (Read/Edit) and resolve the <<<<<<< ======= >>>>>>> markers, guided by BOTH sides' intent. Then call continue_merge(taskId). If markers remain it tells you fix and call again. Use abort_merge(taskId) to cancel a merge you cannot safely resolve.
- Open each conflicted file under repoPath (Read/Edit) and resolve the <<<<<<< ======= >>>>>>> markers, guided by BOTH sides' intent. Then call continue_merge(taskId, skipVerify=true). If markers remain it tells you fix and call again. Use abort_merge(taskId) to cancel a merge you cannot safely resolve.
- For a task WITH children (a unit merge), pass the PARENT task id to continue_merge / abort_merge.
- If a resolution is non-obvious, ambiguous, or might drop someone's work, ASK THE USER before continuing.
- Prefer the MCP tools whenever they apply. Only if the MCP tools cannot reach an in-progress merge may you finish it by hand: resolve the markers, then `git add -- <the resolved paths>` and `git commit` NEVER `git add -A` or a bare commit, because the checkout is shared with other sessions.
- Never use raw `git merge`, `git reset`, or `git checkout` to force a merge. Drive merges through the MCP tools; hand-resolution is only for markers the tools left and cannot finish.
## Verify once after the last merge
When every merge above is done (including conflict resolutions), call verify_merges with the ids of ALL tasks that reported merged_verify_pending. It runs the list's verify command a single time and promotes them all to Done on success that is the per-merge gate you skipped, paid once.
- verify_passed check each entry's promoted flag; report any skipped entry with its reason.
- verify_failed the merges stay in place, but every task deliberately stays out of Done. Report them as merged-but-not-Done together with the output tail do NOT try to fix the verify failure yourself; that is separate follow-up work.
Skip the call only when nothing reported merged_verify_pending.
## Failed tasks only after every merge above is done
For each task that landed in Failed, read failureReason (returned by get_task/batch_get_tasks) before deciding anything Failed is a catch-all covering an account/usage limit, a max-turns cutoff, and a genuine crash, and each deserves a different response. Never restart one blind: