feat(flows): NF-6 Phase 4E — pr-changes-requested baked-in flow #384

Merged
charles merged 2 commits from feat/flows-pr-changes-requested into main 2026-04-26 14:59:45 +00:00
Collaborator

Summary

Ports webhook-handlers.ts::handleChangesRequested (lines 751-817) + domain/workflow/review-loop.ts::guardAuthorDispatch into a baked-in node-flow graph. Ships pr-changes-requested as the sixth seeded flow on the pull_request_review.submitted trigger.

src → router.filter (review.state === "changes_requested")
    → agent.resolve_by_login (PR author)
    → forge.get_pull_request (using author token)
    → router.switch on `mergeable`
        • not_mergeable → render `rebase` + dispatch to author
        • default →
            agent.reviewer_for_pr (label-aware reviewer pick)
            forge.guard_author_dispatch (round-cap, #117)
                • proceed → fetch labels + render `address-review`
                            + dispatch to author
                • force_merge → forge.dispatch_force_merge to boss
                                with FORCE MERGE preamble + SSE

DSL extensions (3 new nodes)

  1. forge.guard_author_dispatch → wraps guardAuthorDispatch. Two ports: proceed (pass) / force_merge (carries {reviewer, rounds} on cap-trip).

  2. agent.reviewer_for_pr → wraps reviewerForPr from webhook-routing.ts. Picks reviewer agent type considering label match (M17-4 / #155) + author mapping. Without this the round-cap guard would count the wrong reviewer's submissions on dev-authored area:dashboard PRs.

  3. forge.dispatch_force_merge → wraps dispatchMerge(…, { force: true, reviewer, rounds }). Encapsulating the whole thing as one node preserves the force_merge_dispatched SSE broadcast (issue #141) and the ⚠️ FORCE MERGE preamble that instructs boss to skip the "latest review APPROVED" check. Splitting into render + dispatch primitives would require teaching agent.dispatch about SSE.

Also added gate input to forge.guard_author_dispatch and agent.reviewer_for_pr so the rebase branch (mergeable=false) drops the entire guard + dispatch chain via FILTER_DROP propagation.

Cutover

Same gate as pr-approved-merge (Phase 4B): node_flows.suppress_legacy: ["pull_request_review.submitted"] skips BOTH pull_request_approved AND pull_request_rejected legacy switch arms at webhook.ts:300-316. Only flip once BOTH pr-approved-merge AND this flow have soaked clean on GET /flows/divergence/summary. Documented in the loader header.

Test plan

  • 12 new e2e tests: non-changes-requested no-op, happy-path address-review, rebase precheck, round-cap force-merge, author unresolved drop
  • Full flows suite: 368 pass / 0 fail
  • Server typecheck clean
  • AGENT_NODE_COUNT bumped from 11 to 14
  • Soak in mode: "live" against real REQUEST_CHANGES review, confirm parity via GET /flows/divergence/summary
  • Soak round-cap path by intentionally hitting MAX_ROUNDS on a test PR
  • Flip suppress_legacy: ["pull_request_review.submitted"] after BOTH 4B + 4E soak

🤖 Generated with Claude Code

## Summary Ports `webhook-handlers.ts::handleChangesRequested` (lines 751-817) + `domain/workflow/review-loop.ts::guardAuthorDispatch` into a baked-in node-flow graph. Ships `pr-changes-requested` as the sixth seeded flow on the `pull_request_review.submitted` trigger. ``` src → router.filter (review.state === "changes_requested") → agent.resolve_by_login (PR author) → forge.get_pull_request (using author token) → router.switch on `mergeable` • not_mergeable → render `rebase` + dispatch to author • default → agent.reviewer_for_pr (label-aware reviewer pick) forge.guard_author_dispatch (round-cap, #117) • proceed → fetch labels + render `address-review` + dispatch to author • force_merge → forge.dispatch_force_merge to boss with FORCE MERGE preamble + SSE ``` ## DSL extensions (3 new nodes) 1. **`forge.guard_author_dispatch`** → wraps `guardAuthorDispatch`. Two ports: `proceed` (pass) / `force_merge` (carries `{reviewer, rounds}` on cap-trip). 2. **`agent.reviewer_for_pr`** → wraps `reviewerForPr` from `webhook-routing.ts`. Picks reviewer agent type considering label match (M17-4 / #155) + author mapping. Without this the round-cap guard would count the wrong reviewer's submissions on dev-authored `area:dashboard` PRs. 3. **`forge.dispatch_force_merge`** → wraps `dispatchMerge(…, { force: true, reviewer, rounds })`. Encapsulating the whole thing as one node preserves the `force_merge_dispatched` SSE broadcast (issue #141) and the `⚠️ FORCE MERGE` preamble that instructs boss to skip the "latest review APPROVED" check. Splitting into render + dispatch primitives would require teaching `agent.dispatch` about SSE. Also added `gate` input to `forge.guard_author_dispatch` and `agent.reviewer_for_pr` so the rebase branch (mergeable=false) drops the entire guard + dispatch chain via FILTER_DROP propagation. ## Cutover Same gate as `pr-approved-merge` (Phase 4B): `node_flows.suppress_legacy: ["pull_request_review.submitted"]` skips BOTH `pull_request_approved` AND `pull_request_rejected` legacy switch arms at `webhook.ts:300-316`. **Only flip once BOTH `pr-approved-merge` AND this flow have soaked clean** on `GET /flows/divergence/summary`. Documented in the loader header. ## Test plan - [x] 12 new e2e tests: non-changes-requested no-op, happy-path address-review, rebase precheck, round-cap force-merge, author unresolved drop - [x] Full flows suite: 368 pass / 0 fail - [x] Server typecheck clean - [x] AGENT_NODE_COUNT bumped from 11 to 14 - [ ] Soak in `mode: "live"` against real REQUEST_CHANGES review, confirm parity via `GET /flows/divergence/summary` - [ ] Soak round-cap path by intentionally hitting `MAX_ROUNDS` on a test PR - [ ] Flip `suppress_legacy: ["pull_request_review.submitted"]` after BOTH 4B + 4E soak 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(flows): NF-6 Phase 4E — pr-changes-requested baked-in flow
Some checks failed
qa / qa (pull_request) Has been cancelled
qa / dockerfile (pull_request) Has been cancelled
f60bd31878
Ports `webhook-handlers.ts::handleChangesRequested` (lines 751-817) +
`domain/workflow/review-loop.ts::guardAuthorDispatch` into a baked-in
node-flow graph. Ships `pr-changes-requested` as the sixth seeded flow
on the `pull_request_review.submitted` trigger:

  src → router.filter (review.state === "changes_requested")
      → agent.resolve_by_login (PR author)
      → forge.get_pull_request (using author token)
      → router.switch on `mergeable`
          • not_mergeable → render `rebase` + dispatch to author
          • default →
              agent.reviewer_for_pr (label-aware reviewer pick)
              forge.guard_author_dispatch (round-cap, #117)
                  • proceed → fetch labels + render `address-review`
                              + dispatch to author
                  • force_merge → forge.dispatch_force_merge to boss
                                  with FORCE MERGE preamble + SSE

Three new injection-shaped nodes:

1. `forge.guard_author_dispatch` → wraps `guardAuthorDispatch` from
   `domain/workflow/review-loop.ts`. Two ports: `proceed` (fires on
   pass) / `force_merge` (carries `{ reviewer, rounds }` on cap-trip).

2. `agent.reviewer_for_pr` → wraps `reviewerForPr` from
   `webhook-routing.ts`. Picks reviewer agent type for the PR
   considering label match (M17-4 / #155) + author mapping. Without
   this the round-cap guard would count the wrong reviewer's
   submissions on dev-authored `area:dashboard` PRs.

3. `forge.dispatch_force_merge` → wraps `dispatchMerge(…, { force: true,
   reviewer, rounds })` from `webhook-ci.ts`. Encapsulating the whole
   thing as one node preserves the `force_merge_dispatched` SSE
   broadcast (issue #141) and the `⚠️ FORCE MERGE` preamble that
   instructs the boss skill to skip the "latest review APPROVED"
   precondition. Splitting into render + dispatch primitives would
   require teaching `agent.dispatch` about SSE.

Also added `gate` input to `forge.guard_author_dispatch` and
`agent.reviewer_for_pr` so the rebase branch (mergeable=false) drops
the entire guard + dispatch chain via FILTER_DROP propagation.

Cutover: `node_flows.suppress_legacy:
["pull_request_review.submitted"]` skips BOTH the `pull_request_approved`
AND `pull_request_rejected` legacy switch arms at `webhook.ts:300-316`.
Only flip once BOTH `pr-approved-merge` (Phase 4B) AND this flow have
soaked clean on `GET /flows/divergence/summary`. Documented prominently
in the loader header.

Tests: 12 new e2e tests covering non-changes-requested no-op, happy
path, rebase precheck, round-cap force-merge, author unresolved drop.
AGENT_NODE_COUNT bumped from 11 to 14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(flows): pr-changes-requested — round-1 review parity for caveman appendix
All checks were successful
qa / qa (pull_request) Successful in 7m15s
qa / dockerfile (pull_request) Successful in 13s
53a9c47387
One IMPORTANT divergence + 2 test gaps caught in subagent review of #384:

1. **Caveman appendix divergence on address-review.** Legacy
   `handleChangesRequested` (`webhook-handlers.ts:797-806`)
   deliberately omits `maybeApplyCavemanAppendix` from the address-
   review prompt chain — only `applyAppendix` +
   `applyArtifactStyleAppendix`. The flow's `render_address` node
   used the default `appendix_mode: "full"` which fires caveman, so a
   chore PR receiving REQUEST_CHANGES would silently get
   caveman-compressed address-review prompts under flow mode where
   legacy did not.

   Fix: added `appendix_mode: "no-caveman"` to `agent.render_skill`
   (skips caveman, keeps `applyAppendix` + artifact-style). Wired the
   flag on the `render_address` node. Bumped graph version to v2 so
   the seeding migration rewrites the row on restart.

   `handleReviewRequested` (Phase 4C) DOES apply caveman intentionally
   (`webhook-handlers.ts:693`), so the flow's full-mode default is
   correct there — only the address-review path needs the opt-out.

2. **Test gap: `mergeable === null`.** Added a parity test pinning
   the fall-through arm (matches legacy `mergeable !== false`).

3. **Test gap: address-review caveman opt-out.** Added an assertion
   that the rendered prompt carries the per-instance prompt_appendix
   (legacy applies it) but NOT the caveman appendix marker (legacy
   doesn't), even when `token_economy.caveman = true` is forced ON.

Tests: 12 → 14, all pass. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charles force-pushed feat/flows-pr-changes-requested from 53a9c47387
All checks were successful
qa / qa (pull_request) Successful in 7m15s
qa / dockerfile (pull_request) Successful in 13s
to 32c392eb6e
All checks were successful
qa / qa (pull_request) Successful in 6m52s
qa / dockerfile (pull_request) Successful in 13s
2026-04-26 14:48:22 +00:00
Compare
charles deleted branch feat/flows-pr-changes-requested 2026-04-26 14:59:46 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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!384
No description provided.