feat(review-loop): auto-force-merge when MAX_ROUNDS cap is hit #137
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!137
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/force-merge-on-max-rounds"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Behaviour change
When
guardAuthorDispatchseesreviewerhas submitted ≥ MAX_ROUNDS (3) REQUEST_CHANGES reviews on a PR, the round cap no longer halts and pages the operator. It now dispatches a force-merge to boss instead.What "force" means
dispatchMerge(repo, pr, { force: true })prepends an override block to the task text: skip the "latest review state is APPROVED" check in step 2 ofskills/merge.md.mergeable: true(no conflicts), CI green. If any of those fail, boss stops and posts an explanatory comment — no hard bypass.Wiring
src/review-loop.tsGuardDecisiongrowsforceMerge?: boolean.guardAuthorDispatchsets it + rewrites the PR comment to "review loop capped — auto-merging". No import of webhook-ci — decision returned, handler dispatches.src/webhook-ci.tsdispatchMerge(repo, pr, opts: { force?: boolean }). Force prepends a 9-line override block.src/webhook-handlers.tshandleChangesRequestedroutescap.forceMerge→dispatchMerge(repo, pr, { force: true }).src/review-loop.test.ts${MAX_ROUNDS} roundstest assertsforceMerge: true,reason.includes("force-merging"), comment body contains "auto-merging".Comment on PR (before the merge dispatch)
Revert path
If auto-merge turns out wrong in practice (e.g. reviewer catches a real bug on round 3 that should have blocked), the old operator-handoff behaviour is a 3-line revert in
guardAuthorDispatch+handleChangesRequested— thepostForceMergeCommentbody can swap back to options-list copy, and theforceMergeflag becomes a no-op.Tests
bun test src/review-loop.test.ts— 17 pass.bun test src/webhook-handlers.test.ts— 26 pass.happy-domtsc from #121,DELETE wipe=trueenv-dep, 3 sweeper-JSONL tests from a just-landed PR — all reproduce onmain).Closes nothing — this is a behaviour change requested by operator.
Instead of halting the address-review dispatch and paging the operator with a handoff comment, the round cap now triggers a force-merge via boss. The existing `dispatchMerge` grew an opt-in `force: true` flag that prepends an override block to the task text; every other guard in `skills/merge.md` (PR open, mergeable, CI green) still applies — only the "latest review state is APPROVED" check is waived, which is exactly the state the cap fires in. Wiring: - `guardAuthorDispatch` sets `forceMerge: true` on the `GuardDecision` and renames its PR comment to "review loop capped — auto-merging" (drops the operator-options block, keeps the rationale + cap pointer). - `handleChangesRequested` routes `cap.forceMerge` into `dispatchMerge(repo, pr, { force: true })` in lieu of the skipped address-review. - `review-loop.ts` stays free of the webhook-ci import chain — the decision is returned, the handler does the dispatch. Tests updated for the new shape (`forceMerge: true`, comment copy); skills/merge.md unchanged — the override is task-text-scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>64136f2d87a1f4248876pending
✅ APPROVED
CI: Green on head
9a2ae95(run #1715, 3m11s). ✅What I checked
src/review-loop.ts—guardAuthorDispatchcorrectly trips atrounds >= MAX_ROUNDS, posts the "auto-merging" comment, and returns{ skip: true, forceMerge: true }. The module stays pure: it returns a decision, the handler dispatches. SwallowingcreateIssueCommenterrors is documented and correct — the decision is the important effect, not the comment.src/webhook-ci.ts—dispatchMerge(repo, pr, { force: true })prepends a clear 9-line override block instructing boss to skip the approval check. The three remaining guards (PR open,mergeable: true, CI green) are still enforced by the boss merge skill — no hard bypass.src/webhook-handlers.ts—handleChangesRequestedroutescap.forceMerge → dispatchMerge(…, { force: true }). Thereturn nullbelow it is the documented revert path (dead code today, not a bug). PR shape passed todispatchMergehas all requiredPrForCifields includinghead?.ref.src/review-loop.test.ts— Test atMAX_ROUNDS roundscorrectly assertsforceMerge: true,reason.includes("force-merging"), and comment body contains "auto-merging" / reviewer login / author login / round count.Minor notes (non-blocking)
webhook-handlers.test.tsfor the force-merge routing path, but the integration is a trivial 3-line if-block and the guard layer is thoroughly covered inreview-loop.test.ts.All acceptance criteria are met. Implementation is correct, safe, and the module boundaries are clean.