feat(pipeline): M19-6 stall detection, round counter, force-merge polish #195
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!195
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/179"
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?
Summary
M19-6 surfaces pipeline risk at a glance so operators catch stuck stages before the force-merge escape hatch fires.
Server
config/agents.json::pipeline(
ci_threshold_ms15 min,review_threshold_ms10 min,implement_threshold_ms30 min,default_stall_ms10 min). Legacystall_threshold_msstill honoured as the review-stage fallback —pre-M19-6 configs keep working unchanged.
pipeline.max_review_rounds(default 3, mirrorsreview-loop::MAX_ROUNDS) is surfaced on theIssuePipelineResponseenvelope so the UI and server read one cap.
updated_atwhen theaggregate state is
running. Implement / design stall reads the liveworker registry via the new
Worker.currentTaskStartedAt+ alistRunningTasksForIssuepipeline dep (wired inmain.ts).POST /pipeline/bounce-review— DELETE +re-POSTs every
requested_revieweron the PR so Forgejo fires afresh
pull_request_review_requestevent. SameguardMutatingrail as the other M18-8 endpoints.
UI
stalled_sinceanchor and the right remedy per stage ("clickBounce" for review, "open the Actions run" for CI — no
auto-retrigger, safety rail).
round >= max - 1; the↺N ⚠pattern carries through compactmode alongside the gold
★badge.to
?risk=1in the URL so shareable links land on the samefilter.
<BounceButton>renders next to a stalled review pill that has alinked PR; posts to
/pipeline/bounce-reviewand surfacessuccess / failure via the toast store.
Tests
apps/server/src/pipeline-stall.test.ts— 8 tests covering CIstall (stalled / fresh / completed), implement stall via live
worker (stalled / fresh / fallback / designer variant),
max_review_roundson the envelope.apps/web/src/components/pipeline-stall.test.tsx— amber pill,tooltip hint, CI "open Actions" hint, Bounce button presence /
absence, clicking Bounce POSTs the right body.
apps/web/src/components/pipeline-round-counter.test.tsx— roletransitions at round 0, 1, 2 (warning), N-1 (warning), and
force-merge.
pipeline-list.test.tsxwith theriskOnlyfilter +isAtRiskhelper tests.Docs
Label routing and updates the API table + pipeline module entry.
the Pipeline monitor section, and the
↺N ⚠row lands in thepalette table.
Closes #179
Test plan
bun x turbo run typecheck— passes across server, web, sharedbun x biome check .— cleanbun x biome format .— cleanbun x turbo run test— 769 server tests + 61 web tests green,including the new
pipeline-stall/pipeline-round-counterfiles
review request on a real PR to confirm the webhook chain fires
exactly as it would on the original request
Surfaces pipeline risk at a glance so operators catch stuck stages before the force-merge escape hatch fires: - Per-stage stall thresholds in config/agents.json::pipeline (ci_threshold_ms, review_threshold_ms, implement_threshold_ms, default_stall_ms). Legacy stall_threshold_ms still honoured as the review-stage fallback so pre-M19-6 configs keep working. - Review pill flips red at round >= max_review_rounds - 1 ("about to force-merge"); configurable via pipeline.max_review_rounds (default 3), mirrored from review-loop::MAX_ROUNDS and threaded through the response envelope so the UI and server share one cap. - Implement / design stall detection consults the live worker registry via new Worker.currentTaskStartedAt + a listRunningTasksForIssue pipeline dep. - CI stall detection anchors on the newest workflow run's updated_at. - ⚠ Only stalled / at-risk filter chip on the monitor; persists as ?risk=1 on the URL. - Bounce quick-action next to a stalled review pill: POST /pipeline/bounce-review (operator-gated) DELETE + re-POSTs every requested_reviewer so Forgejo fires a fresh review-request event. - ★ force-merge badge survives compact mode alongside the round counter (polished). Tests: pipeline-stall.test.ts (server CI/implement/review stall), pipeline-stall.test.tsx + pipeline-round-counter.test.tsx (UI). Closes #179 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>17e42c165f7fe6efa60cReview — M19-6 stall detection, round counter, force-merge polish
CI ✅ green on head
d0c2d0b.The server-side stall detection (CI anchoring on newest run's
updated_at, implement stall via live worker registry, review stall viasubmitted_atfallback to PR'supdated_at) is well-designed and the tests cover the key branches cleanly. ThelistRunningTasksForIssuedependency-injection pattern makes the stall tests deterministic without mocking the worker registry directly. Config loading, threshold clamping, and the legacystall_threshold_msback-compat path all look correct. The bounce-review endpoint andBounceReviewResponseshape are clean.One bug blocks approval — the purple "round" intermediate state is unreachable with the default
maxRounds=3, and the implementation silently deviates from the acceptance criterion that says↺ Nshould appear atround > 0.Bug: purple "round" state is unreachable with default
maxRounds—stage-pill.tsxFiles:
apps/web/src/components/stage-pill.tsxandapps/web/src/components/pipeline-round-counter.test.tsxThree conditions in
stage-pill.tsxall useround > 1as the threshold for showing the ↺ counter:Combined with the
colorRolewarning check (round >= maxRounds - 1 && round > 0), the effective palette transitions with defaultmaxRounds=3are:colorRoleresultround-warning(red)The purple
roundrole is only reachable whenmaxRounds > 3(non-default) — with the default config the pill jumps directly from "no indicator at all" to "red warning", skipping the informational purple state entirely.The issue acceptance criterion says:
And this is confirmed by the test's own internal comment, which then contradicts the test name:
The test name says "mid-loop, below warning, purple" but the assertion is
round-warning(red failure palette). The comment inside correctly identifies it's at the warning threshold — contradicting the test name.Fix:
In
colorRole, separate the purple and red zones properly:In
labelTextandshowRoundBadge, change the threshold fromround > 1toround > 0:With this fix and
maxRounds=3, the palette is:colorRoleresultround(purple)round-warning(red)The test for round=2 should then be renamed to reflect it's at the warning threshold with
maxRounds=3, and a test for round=1 assertingdata-role="round"and↺1should be added.Everything else — CI stall anchoring, implement stall via
currentTaskStartedAt, review stall fallback, config parsing with per-stage defaults,max_review_roundson the response envelope, theBounceButton/postBounceReviewwiring, and the?risk=1filter — is correct and ready to ship once the above is addressed.The ↺ round counter threshold was `round > 1` in labelText / showRoundBadge and the purple `round` color role — with default maxRounds=3 the pill jumped directly from the base state (round=1, no indicator) to the red `round-warning` palette (round=2), skipping the purple informational state entirely. The acceptance criterion ("↺ N when round > 0") and the round=2 test's internal comment both contradicted the implementation. - stage-pill: collapse the colorRole review branch so the purple `round` role fires at round > 0 (below warning) and the red `round-warning` role fires at round >= maxRounds - 1. Drop the dead `round > 0` guard on round-warning (already implied by the outer > 0 check). Align labelText and showRoundBadge with the same > 0 threshold. - Rename the "round=2 below warning" test to reflect it's at the warning threshold with maxRounds=3, add a round=2 + maxRounds=4 test that actually exercises the purple branch, and update the round=1 test to assert the now-reachable purple palette. - Stage-pill test comment: "round >1" → "round > 0" for consistency.Thanks — caught a real gap. Fixed in
ea345c3:stage-pill.tsx:colorRolereview branch rewritten soround > 0→round(purple) andround >= maxRounds - 1→round-warning(red).labelTextandshowRoundBadgethresholds lowered toround > 0to match. With the defaultmaxRounds=3the palette is now:data-rolerunninground(purple)round-warning(red)pipeline-round-counter.test.tsx: the misnamed "round=2 below warning" case renamed to "at warning threshold, maxRounds=3"; added around=2+maxRounds=4case that actually exercises the purple branch; theround=1test now asserts the reachable purple palette per the acceptance criterion.round=1also asserts no⚠to pin the warning boundary.stage-pill.test.tsx: the "round >1" test label corrected toround > 0.All 111 web tests green,
bun x biome check ./bun x biome format .clean.Round 2 — follow-up on the purple
roundpalette fixCI ✅ green on head
ea345c3(run #1812, 3m32s).The sole blocking finding from round 1 is fully resolved. All three
round > 1guards have been corrected toround > 0:colorRole()—entry.round > 0branches into purple first, red warning only when>= maxRounds - 1. Purple is now reachable at round=1 with the defaultmaxRounds=3.labelText()— threshold changed toround > 0; label shows↺1on the first re-review.showRoundBadge— same threshold; compact mode exposes the badge from round=1.Test suite updated correctly:
round=1test added, assertingdata-role="round"(purple) and↺1with no⚠— exactly the acceptance criterion.round=2 (maxRounds=4)test confirms the purple zone is reachable when the cap is higher.Everything from the first review is addressed. Approving.
ea345c37321d23688907