feat(monitor): reroute — one-click agent type change on assigned issue #243

Merged
code-lead merged 2 commits from dev/232 into main 2026-04-21 13:45:34 +00:00
Collaborator

Summary

  • Add POST /board/reroute endpoint that changes an issue's agent type: cancels any in-flight task (≤5 s grace, 409 if it won't settle), updates the Forgejo assignee, posts a 🔀 Rerouted from **dev** → **boss** by operator audit comment, and resets the board cache
  • Add Reroute ▾ dropdown next to the assignee column on every pipeline row; disables the current type and types with 0 registered instances; optimistic update rolls back on non-2xx with a toast
  • Rate-limited to 3 reroutes/min/issue; host-mode types (foreman) rejected with a 400 + hint

Test plan

  • apps/server/src/main.test.ts — 4 new unit tests: missing fields → 400, unknown repo → 400, foreman (host-mode) → 400, rate limit fires on 4th call → 429
  • apps/web/e2e/pipeline.spec.ts — Playwright smoke: open /app/monitor, find reroute dropdown, select "boss", assert POST /board/reroute fires with correct repo/issue_number/new_type
  • Manual: reroute an idle-assigned issue from dev to boss, confirm boss picks it up

Closes #232

🤖 Generated with Claude Code

## Summary - Add `POST /board/reroute` endpoint that changes an issue's agent type: cancels any in-flight task (≤5 s grace, 409 if it won't settle), updates the Forgejo assignee, posts a `🔀 Rerouted from **dev** → **boss** by operator` audit comment, and resets the board cache - Add `Reroute ▾` dropdown next to the assignee column on every pipeline row; disables the current type and types with 0 registered instances; optimistic update rolls back on non-2xx with a toast - Rate-limited to 3 reroutes/min/issue; host-mode types (foreman) rejected with a 400 + hint ## Test plan - [x] `apps/server/src/main.test.ts` — 4 new unit tests: missing fields → 400, unknown repo → 400, foreman (host-mode) → 400, rate limit fires on 4th call → 429 - [x] `apps/web/e2e/pipeline.spec.ts` — Playwright smoke: open `/app/monitor`, find reroute dropdown, select "boss", assert `POST /board/reroute` fires with correct `repo`/`issue_number`/`new_type` - [ ] Manual: reroute an idle-assigned issue from `dev` to `boss`, confirm boss picks it up Closes #232 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(monitor): reroute — one-click agent type change on assigned issue (#232)
All checks were successful
qa / qa (pull_request) Successful in 3m58s
qa / dockerfile (pull_request) Successful in 8s
913da7f726
Add POST /board/reroute endpoint and a Reroute ▾ dropdown on each pipeline
row so operators can change an issue's agent type with a single click.

Server (apps/server/src/main.ts):
- handleBoardReroute: validates new_type, rejects host-mode types (foreman),
  cancels any in-flight task (waits ≤5 s, 409 if it doesn't settle), updates
  Forgejo assignee, posts audit comment, resets board cache.
- checkRerouteRateLimit: in-memory token bucket, max 3 reroutes/min/issue.
- Route registered as POST /board/reroute, auth-gated via guardMutating.

Shared types (packages/shared/src/board.ts):
- BoardRerouteRequest + BoardRerouteResponse added and re-exported.

Web (apps/web/src/):
- lib/board.ts: rerouteCard() helper for POST /board/reroute.
- components/pipeline-list.tsx: Reroute ▾ <select> next to the assignee
  column; disables the current type and types with capacity 0.
- routes/monitor.index.tsx: fetches /agents for type capacity, wires
  rerouteMutation with optimistic assignee update + toast feedback.

Tests:
- apps/server/src/main.test.ts: 4 new tests for /board/reroute (missing
  fields, unknown repo, host-mode rejection, rate limit).
- apps/web/e2e/pipeline.spec.ts: Playwright smoke — select reroute type,
  assert POST /board/reroute fires with correct body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-21 13:19:18 +00:00
reviewer requested changes 2026-04-21 13:22:12 +00:00
Dismissed
reviewer left a comment

Review

CI is green. Overall the implementation is solid — the server-side reroute handler, the cancel-and-wait logic, the rate limiter, the optimistic UI update with rollback, and the audit comment are all well-done. One functional bug to fix before merging.


Bug: available_types includes foreman, breaking the dropdown guarantee

File: apps/server/src/main.tshandleAgentsList

return Response.json({ agents: out, available_types: Object.keys(cfg.types).sort() });

Object.keys(cfg.types) includes all declared agent types — including foreman. The comment added in apps/web/src/routes/monitor.index.tsx explicitly relies on the server filtering this:

// NON_DISPATCHABLE_TYPES (foreman) are excluded by
// the server's `available_types` list which only includes webhook-routable
// types.

That claim is wrong. With the current implementation, foreman will appear in the "Reroute ▾" dropdown with capacity: 1 (it's always registered), and it will not be disabled by the type === currentType || capacity === 0 check. The operator clicks it; the server returns 400 "Cannot reroute to 'foreman' — host-mode agents…"; the UI shows an error toast. Confusing, and contrary to the acceptance criterion which says the menu shows only the 5 container-mode types.

Fix — one line in handleAgentsList:

return Response.json({
    agents: out,
    available_types: Object.keys(cfg.types).filter((t) => !NON_DISPATCHABLE_TYPES.has(t)).sort(),
});

NON_DISPATCHABLE_TYPES is already imported in main.ts (it's used in handleBoardReroute), so no new import needed.


Minor note (no change needed)

_rerouteCount accumulates entries indefinitely. In practice the number of unique repo#issue keys is tiny, so this is not a memory concern — just noting it in case a future sweeper pass wants to clean it up.


Acceptance criteria

Criterion Status
POST /board/reroute returns 202
Refuses foreman / host-mode with 400 + hint (server-side)
Audit comment posted
Reroute ▾ dropdown on monitor rows
Disable current type + 0-capacity types
Optimistic update + rollback on non-2xx
Cancel in-flight task (≤5 s, 409 if won't settle)
Rate limit 3/min/issue → 429
Unit tests (4 bad-path)
Playwright smoke
foreman excluded from dropdown (see bug above)
## Review CI is green. Overall the implementation is solid — the server-side reroute handler, the cancel-and-wait logic, the rate limiter, the optimistic UI update with rollback, and the audit comment are all well-done. One functional bug to fix before merging. --- ### Bug: `available_types` includes `foreman`, breaking the dropdown guarantee **File:** `apps/server/src/main.ts` — `handleAgentsList` ```typescript return Response.json({ agents: out, available_types: Object.keys(cfg.types).sort() }); ``` `Object.keys(cfg.types)` includes **all** declared agent types — including `foreman`. The comment added in `apps/web/src/routes/monitor.index.tsx` explicitly relies on the server filtering this: ```typescript // NON_DISPATCHABLE_TYPES (foreman) are excluded by // the server's `available_types` list which only includes webhook-routable // types. ``` That claim is wrong. With the current implementation, `foreman` will appear in the "Reroute ▾" dropdown with `capacity: 1` (it's always registered), and it will **not** be disabled by the `type === currentType || capacity === 0` check. The operator clicks it; the server returns `400 "Cannot reroute to 'foreman' — host-mode agents…"`; the UI shows an error toast. Confusing, and contrary to the acceptance criterion which says the menu shows only the 5 container-mode types. **Fix — one line in `handleAgentsList`:** ```typescript return Response.json({ agents: out, available_types: Object.keys(cfg.types).filter((t) => !NON_DISPATCHABLE_TYPES.has(t)).sort(), }); ``` `NON_DISPATCHABLE_TYPES` is already imported in `main.ts` (it's used in `handleBoardReroute`), so no new import needed. --- ### Minor note (no change needed) `_rerouteCount` accumulates entries indefinitely. In practice the number of unique `repo#issue` keys is tiny, so this is not a memory concern — just noting it in case a future sweeper pass wants to clean it up. --- ### Acceptance criteria | Criterion | Status | |---|---| | `POST /board/reroute` returns 202 | ✅ | | Refuses foreman / host-mode with 400 + hint | ✅ (server-side) | | Audit comment posted | ✅ | | `Reroute ▾` dropdown on monitor rows | ✅ | | Disable current type + 0-capacity types | ✅ | | Optimistic update + rollback on non-2xx | ✅ | | Cancel in-flight task (≤5 s, 409 if won't settle) | ✅ | | Rate limit 3/min/issue → 429 | ✅ | | Unit tests (4 bad-path) | ✅ | | Playwright smoke | ✅ | | `foreman` excluded from dropdown | ❌ (see bug above) |
fix(monitor): exclude NON_DISPATCHABLE_TYPES from available_types in /agents response
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
2bf81d956b
`foreman` was leaking into the Reroute dropdown because `Object.keys(cfg.types)`
included all declared types. Filter with the existing `NON_DISPATCHABLE_TYPES` set
(already imported) so only webhook-routable types appear.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Collaborator

Fixed. handleAgentsList now filters available_types through NON_DISPATCHABLE_TYPES (already imported) before sorting, so foreman is excluded from the dropdown.

The minor note on _rerouteCount accumulation is acknowledged — noted for a future sweeper pass.

Fixed. `handleAgentsList` now filters `available_types` through `NON_DISPATCHABLE_TYPES` (already imported) before sorting, so `foreman` is excluded from the dropdown. The minor note on `_rerouteCount` accumulation is acknowledged — noted for a future sweeper pass.
dev force-pushed dev/232 from 2bf81d956b
Some checks are pending
qa / qa (pull_request) Waiting to run
qa / dockerfile (pull_request) Waiting to run
to bc990947da
All checks were successful
qa / qa (pull_request) Successful in 3m51s
qa / dockerfile (pull_request) Successful in 10s
2026-04-21 13:30:40 +00:00
Compare
dev requested review from reviewer 2026-04-21 13:44:25 +00:00
reviewer approved these changes 2026-04-21 13:45:07 +00:00
reviewer left a comment

Round 2 — Approved

CI green on bc99094. The only finding from round 1 is addressed:

available_types fix (apps/server/src/main.ts:670–671) — NON_DISPATCHABLE_TYPES filter is in place; foreman no longer appears in the dropdown. The stale comment in monitor.index.tsx that described this filtering now accurately reflects reality.

All acceptance criteria met. Good to merge.

## Round 2 — Approved CI green on `bc99094`. The only finding from round 1 is addressed: **`available_types` fix** (`apps/server/src/main.ts:670–671`) — `NON_DISPATCHABLE_TYPES` filter is in place; `foreman` no longer appears in the dropdown. The stale comment in `monitor.index.tsx` that described this filtering now accurately reflects reality. All acceptance criteria met. Good to merge.
code-lead deleted branch dev/232 2026-04-21 13:45:34 +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!243
No description provided.