Dashboard: surface force-merge events distinctly from normal approval merges #141

Closed
opened 2026-04-20 13:01:49 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As the operator, I want force-merged PRs (the MAX_ROUNDS auto-merge path from #137) to be visually distinct on the dashboard so that I can spot at a glance which merges landed without a clean reviewer approval and decide whether to follow up.

Context

After #137, hitting MAX_ROUNDS=3 REQUEST_CHANGES flips the normal "author fixes findings, reviewer approves, boss merges" chain into "boss squash-merges despite the open REQUEST_CHANGES". The two paths produce the same squash commit from outside, so the dashboard's task history can't distinguish them. That's fine for the majority case (force-merge should be rare), but when it does fire:

  • The operator should see that it fired (not just "merged — cost $X.XX, 42 turns").
  • Ideally the row should link to the cap comment so the reviewer's last findings are one click away.
  • Post-hoc trend: "how often does the loop cap fire" is a quality signal for the reviewer agent — too often means the reviewer is nitpicking.

Acceptance criteria

Event stream

  • The force-merge dispatch in src/webhook-handlers.ts::handleChangesRequested currently calls dispatchMerge(repo, pr, { force: true }). Extend the call-site (or dispatchMerge itself) to emit a force_merge_dispatched event onto the SSE stream alongside the normal dispatch event. Payload: { repo, pr_number, reviewer, rounds }.
  • TaskRecord for the boss merge task gains an optional flag — e.g. force_merge: true — so the history surface can render it without re-fetching the PR. Wire it through worker.enqueue metadata if the simplest path lets us.

UI

  • Dashboard history row for a force-merged task shows a distinct badge — e.g. ⚠ force-merged next to the task title. Colour: --warning token (same as the disconnect banner's accent).
  • Hovering the badge shows a tooltip: "Merged after reviewer's REQUEST_CHANGES; MAX_ROUNDS=3 policy." Clicking navigates to the PR's cap comment if possible (permalink anchor), else just the PR.
  • Stats panel's by_day strip: force-merged tasks count as cancelled for success-rate purposes? No — force-merge is a successful operator-approved terminator. Render them as successes but with a tiny warning dot overlay. Or, cleaner: add a force_merged int field to the /stats by-day/by-agent aggregates and show it as a small subtitle ("23 tasks · 2 force-merged") on agent rows.

Tests

  • dashboard-browser.test.ts: seed a task with force_merge: true, assert the badge renders + tooltip has the right copy.
  • dashboard-smoke.test.ts: structural check for the badge element.
  • stats.test.ts: if the stats aggregates grow a force_merged count, one fixture task with the flag set, assert the aggregate increments.

Out of scope

  • Alerting / email when force-merge fires (operator dashboard is enough for now).
  • Overriding the MAX_ROUNDS cap per-repo or per-reviewer (separate ticket if needed).
  • Retroactively backfilling force-merge flags on already-merged PRs — only forward-going merges get flagged.

References

  • Force-merge wiring: PR #137 (src/review-loop.ts, src/webhook-ci.ts, src/webhook-handlers.ts).
  • Task event stream: src/main.ts::broadcastSSE, logSDKMessage.
  • Stats endpoint: src/main.ts::handleStats, src/task-store.ts.

Dependencies

  • Blocked by: nothing.
  • Blocks: nothing.
  • Branch off: main.
## User story As the **operator**, I want force-merged PRs (the MAX_ROUNDS auto-merge path from #137) to be visually distinct on the dashboard so that I can spot at a glance which merges landed without a clean reviewer approval and decide whether to follow up. ## Context After #137, hitting `MAX_ROUNDS=3` REQUEST_CHANGES flips the normal "author fixes findings, reviewer approves, boss merges" chain into "boss squash-merges despite the open REQUEST_CHANGES". The two paths produce the same squash commit from outside, so the dashboard's task history can't distinguish them. That's fine for the majority case (force-merge should be rare), but when it does fire: - The operator should see *that* it fired (not just "merged — cost $X.XX, 42 turns"). - Ideally the row should link to the cap comment so the reviewer's last findings are one click away. - Post-hoc trend: "how often does the loop cap fire" is a quality signal for the reviewer agent — too often means the reviewer is nitpicking. ## Acceptance criteria ### Event stream - [ ] The force-merge dispatch in `src/webhook-handlers.ts::handleChangesRequested` currently calls `dispatchMerge(repo, pr, { force: true })`. Extend the call-site (or `dispatchMerge` itself) to emit a `force_merge_dispatched` event onto the SSE stream alongside the normal dispatch event. Payload: `{ repo, pr_number, reviewer, rounds }`. - [ ] `TaskRecord` for the boss merge task gains an optional flag — e.g. `force_merge: true` — so the history surface can render it without re-fetching the PR. Wire it through `worker.enqueue` metadata if the simplest path lets us. ### UI - [ ] Dashboard history row for a force-merged task shows a distinct badge — e.g. `⚠ force-merged` next to the task title. Colour: `--warning` token (same as the disconnect banner's accent). - [ ] Hovering the badge shows a tooltip: "Merged after reviewer's REQUEST_CHANGES; MAX_ROUNDS=3 policy." Clicking navigates to the PR's cap comment if possible (permalink anchor), else just the PR. - [ ] **Stats** panel's `by_day` strip: force-merged tasks count as `cancelled` for success-rate purposes? **No** — force-merge is a successful operator-approved terminator. Render them as successes but with a tiny warning dot overlay. Or, cleaner: add a `force_merged` int field to the `/stats` by-day/by-agent aggregates and show it as a small subtitle ("23 tasks · 2 force-merged") on agent rows. ### Tests - [ ] `dashboard-browser.test.ts`: seed a task with `force_merge: true`, assert the badge renders + tooltip has the right copy. - [ ] `dashboard-smoke.test.ts`: structural check for the badge element. - [ ] `stats.test.ts`: if the stats aggregates grow a `force_merged` count, one fixture task with the flag set, assert the aggregate increments. ## Out of scope - Alerting / email when force-merge fires (operator dashboard is enough for now). - Overriding the MAX_ROUNDS cap per-repo or per-reviewer (separate ticket if needed). - Retroactively backfilling force-merge flags on already-merged PRs — only forward-going merges get flagged. ## References - Force-merge wiring: PR #137 (`src/review-loop.ts`, `src/webhook-ci.ts`, `src/webhook-handlers.ts`). - Task event stream: `src/main.ts::broadcastSSE`, `logSDKMessage`. - Stats endpoint: `src/main.ts::handleStats`, `src/task-store.ts`. ## Dependencies - **Blocked by:** nothing. - **Blocks:** nothing. - **Branch off:** `main`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#141
No description provided.