feat(flows): NF-6 Phase 4A — issue.labeled label-routing flow #380

Merged
charles merged 2 commits from feat/flows-issue-labeled-route into main 2026-04-26 12:52:56 +00:00
Collaborator

First Phase 4 cutover candidate. Mirrors legacy webhook-handlers.ts::handleIssueLabeled end-to-end so a node_flows.suppress_legacy: ["issue.labeled"] cutover keeps every guard rail.

What

Pipeline: src.labelsAfterforge.route_labelagent.resolve_by_typeagent.render_skillagent.dispatch.

Two new nodes:

  • agent.resolve_by_type — parallel to agent.resolve_by_login but goes through resolveAgentByType (label routes return type names, not Forgejo logins). Same ResolvedAgentEnvelope output, same FILTER_DROP-on-miss semantics. Refactored the 12-field FILTER_DROP envelope into a shared emitResolvedEnvelope helper.

  • forge.route_label — table-driven first-match. Walks inputs.labels: string[] against args.routes: Record<label, {agent, skill}> and emits (type, skill, matched_label) for the first matching label (FILTER_DROP otherwise). Mirrors legacy webhook-routing.ts::routeForLabels.

New baked-in flow at apps/server/src/domain/flows/issue-labeled-graph.{json,ts}. Same load-time validation + seed-time version-bump migration as the default flow. Seeded alongside the default flow at boot via seedDefaultFlowAtBoot.

flow-dispatch.ts gains a parallel resolveAgentByType injection sharing a new envelopeFromAgent projection helper.

Tests

  • 6 new e2e on the flow: area:design → designer/implement, area:design-review → design-reviewer/review, non-routing labels silent skip, first-match-wins (legacy parity — no double-dispatch when both routing labels present), unconfigured agent no-op.
  • 1 new schema sanity test.
  • 1861 server tests pass (was 1855 pre-Phase-4A); 5 pre-existing failures unchanged.
  • Typecheck + biome clean.

Cutover

After a soak window of zero divergence on GET /flows/divergence/summary, set:

"node_flows": { "mode": "live", "suppress_legacy": ["issue.assigned", "issue.labeled"] }

and the legacy handleIssueLabeled arm is dead.

Phase 4 ship order (from research map)

# Flow Effort Status
1 issue-labeled-route S this PR
2 pr-approved-merge S next
3 review-requested S
4 slash-breakdown S
5 pr-changes-requested M depends on PR #379 nodes
6 issue-closed-deps M depends on issue.assigned cutover
7 pr-fixci-probe L depends on CI state machine confidence

🤖 Generated with Claude Code

First Phase 4 cutover candidate. Mirrors legacy `webhook-handlers.ts::handleIssueLabeled` end-to-end so a `node_flows.suppress_legacy: ["issue.labeled"]` cutover keeps every guard rail. ## What Pipeline: `src.labelsAfter` → `forge.route_label` → `agent.resolve_by_type` → `agent.render_skill` → `agent.dispatch`. Two new nodes: - **`agent.resolve_by_type`** — parallel to `agent.resolve_by_login` but goes through `resolveAgentByType` (label routes return type names, not Forgejo logins). Same `ResolvedAgentEnvelope` output, same FILTER_DROP-on-miss semantics. Refactored the 12-field FILTER_DROP envelope into a shared `emitResolvedEnvelope` helper. - **`forge.route_label`** — table-driven first-match. Walks `inputs.labels: string[]` against `args.routes: Record<label, {agent, skill}>` and emits `(type, skill, matched_label)` for the first matching label (FILTER_DROP otherwise). Mirrors legacy `webhook-routing.ts::routeForLabels`. New baked-in flow at `apps/server/src/domain/flows/issue-labeled-graph.{json,ts}`. Same load-time validation + seed-time version-bump migration as the default flow. Seeded alongside the default flow at boot via `seedDefaultFlowAtBoot`. `flow-dispatch.ts` gains a parallel `resolveAgentByType` injection sharing a new `envelopeFromAgent` projection helper. ## Tests - 6 new e2e on the flow: `area:design` → designer/implement, `area:design-review` → design-reviewer/review, non-routing labels silent skip, first-match-wins (legacy parity — no double-dispatch when both routing labels present), unconfigured agent no-op. - 1 new schema sanity test. - 1861 server tests pass (was 1855 pre-Phase-4A); 5 pre-existing failures unchanged. - Typecheck + biome clean. ## Cutover After a soak window of zero divergence on `GET /flows/divergence/summary`, set: ```json "node_flows": { "mode": "live", "suppress_legacy": ["issue.assigned", "issue.labeled"] } ``` and the legacy `handleIssueLabeled` arm is dead. ## Phase 4 ship order (from research map) | # | Flow | Effort | Status | |---|---|---|---| | 1 | `issue-labeled-route` | S | **this PR** | | 2 | `pr-approved-merge` | S | next | | 3 | `review-requested` | S | | | 4 | `slash-breakdown` | S | | | 5 | `pr-changes-requested` | M | depends on PR #379 nodes | | 6 | `issue-closed-deps` | M | depends on `issue.assigned` cutover | | 7 | `pr-fixci-probe` | L | depends on CI state machine confidence | 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(flows): NF-6 Phase 4A — issue.labeled label-routing flow
All checks were successful
qa / qa (pull_request) Successful in 6m11s
qa / dockerfile (pull_request) Successful in 12s
b17836dad6
First Phase 4 cutover candidate. Mirrors legacy
`webhook-handlers.ts::handleIssueLabeled` end-to-end so a
`node_flows.suppress_legacy: ["issue.labeled"]` cutover keeps every
guard rail.

Pipeline: src.labelsAfter → forge.route_label (table-driven first-match
against `area:design` / `area:design-review`) → agent.resolve_by_type
(envelope from `resolveAgentByType`) → agent.render_skill (template +
appendices) → agent.dispatch (enable_dedup + cancel_stale).

New nodes:
- `agent.resolve_by_type` — parallel to `agent.resolve_by_login` but
  via `resolveAgentByType` (label routes return type names, not
  Forgejo logins). Same `ResolvedAgentEnvelope` output, same
  FILTER_DROP-on-miss semantics.
- `forge.route_label` — table-driven first-match. Walks
  `inputs.labels: string[]` against `args.routes: Record<labelName,
  {agent, skill}>` and emits `(type, skill, matched_label)` of the
  first match (FILTER_DROP otherwise). Matches legacy `routeForLabels`.

Refactor: shared `emitResolvedEnvelope` helper between the two
resolve nodes (was duplicated 12-field FILTER_DROP-on-miss block).
`flow-dispatch.ts` gains a parallel `resolveAgentByType` injection
sharing the same `envelopeFromAgent` projection helper.

New baked-in flow: `apps/server/src/domain/flows/issue-labeled-graph.{json,ts}`.
Same load-time validation + seed-time version-bump migration as the
default flow. Seeded alongside the default flow at boot.

Tests: 6 new e2e on the flow (area:design / area:design-review / non-
routing labels silent skip / first-match-wins / unconfigured agent
no-op) + 1 schema sanity. All green; 1861 server tests pass total
(was 1855 pre-Phase-4A); 5 pre-existing failures unchanged.

Cutover: after a soak window of zero divergence on `GET
/flows/divergence/summary`, set `node_flows.suppress_legacy:
["issue.assigned", "issue.labeled"]` and the legacy
`handleIssueLabeled` arm is dead.

Risk-ordered next per Phase 4 design map: `pr-approved-merge` (S),
then `review-requested` (S), then `slash-breakdown` (S), then
`pr-changes-requested` (M), `issue-closed-deps` (M),
`pr-fixci-probe` (L).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(flows): NF-6 Phase 4A — addedLabel routing parity + author-error guards
All checks were successful
qa / qa (pull_request) Successful in 7m16s
qa / dockerfile (pull_request) Successful in 16s
d62c717d06
Round 1 review feedback on PR #380. Two blockers + four majors.

Blockers:
- Flow wired `forge.route_label.labels` from `src.out.labelsAfter`
  (full set), but legacy `handleIssueLabeled` routes on the SINGLE
  just-added label. An `area:design` issue that gets `priority:high`
  added later → legacy: no dispatch; flow: silent re-dispatch of
  designer on every subsequent label change. Cutover would silently
  regress this for every routing-labeled issue.
- `addedLabel` was extracted at the wire layer (`webhook-normalize.ts:
  202, 435, 688`) but dropped during `toTriggerEvent`. Flow had no path
  to it. Fix: surface `addedLabel: string | null` on
  `TriggerEventIssueLabeled`, populate in the converter.

Now `forge.route_label` accepts `added_label` AND `labels` inputs.
Single-label path wins when `added_label` is non-null (matches
`action === "labeled"`); falls back to walking `labels` when
`added_label` is null (matches Forgejo v15 `label_updated`).

Majors:
- `forge.route_label` now throws on author errors instead of silent
  FILTER_DROP: missing `args.routes`, empty routes table, or partial
  route value (`{ agent }` missing skill, etc.) all fail loudly with
  `status: "error"` so a misconfigured flow doesn't silently no-op.
- `seedDefaultFlowAtBoot` now returns `Record<flowId, SeedStatus>`
  instead of just the default-flow status; `main.ts` boot banner logs
  each flow's outcome separately. Surfaces operator-edited or schema-
  drift skips that the previous shape silently swallowed.
- `agent.resolve_by_type` gets a 5-test unit suite (envelope hit,
  null-on-miss, not-wired throw, empty-input rejection, input
  precedence) — was only tested transitively through the flow.
- New `issue-labeled-graph.test.ts` divergence test pins the
  legacy parity intent: issue already carries `area:design`, just-
  added label is non-routing → NO dispatch. Without `added_label`
  wiring this would have re-dispatched silently. Plus a v15
  `label_updated` fallback test (addedLabel=null walks labelsAfter).

Bumped `ISSUE_LABELED_GRAPH_VERSION` 1 → 2 so the seed migration
rewrites the row on next service restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charles deleted branch feat/flows-issue-labeled-route 2026-04-26 12:52:56 +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!380
No description provided.