feat(escalation): escalate dev→boss after N silent failures (B11) #440

Merged
code-lead merged 1 commit from dev/427 into main 2026-04-27 10:32:55 +00:00
Collaborator

Summary

  • Adds silent_failure_count field to TaskRequest (consumed by B11; B10 will populate it on re-dispatch)
  • New escalation.ts module with checkAndRecordEscalation(): decides escalate | dead_letter | pass based on per-type escalation_target + max_escalations_per_day (default 10); daily cap auto-resets at local midnight via a YYYY-M-D cache key
  • agent.dispatch node: when silent_failure_count >= 1, calls checkEscalation; on escalate → re-routes to target type, patches request identity (token/user/name/email/branch_prefix), and fires an audit comment; on dead_letter → broadcasts flow:dead-letter SSE and emits FILTER_DROP
  • config/agents.json: dev and reviewer types gain escalation_target: "boss" and max_escalations_per_day: 10
  • 11 new unit tests in escalation.test.ts covering all four decision paths

Closes #427

Test plan

  • bun test apps/server/src/domain/dispatch/escalation.test.ts — 11 tests pass
  • bun test apps/server/src/domain/flows/agent-nodes.test.ts — 38 existing tests pass
  • bun x tsc --noEmit in apps/server — no errors
  • bun x biome check on changed files — clean
## Summary - Adds `silent_failure_count` field to `TaskRequest` (consumed by B11; B10 will populate it on re-dispatch) - New `escalation.ts` module with `checkAndRecordEscalation()`: decides `escalate | dead_letter | pass` based on per-type `escalation_target` + `max_escalations_per_day` (default 10); daily cap auto-resets at local midnight via a `YYYY-M-D` cache key - `agent.dispatch` node: when `silent_failure_count >= 1`, calls `checkEscalation`; on `escalate` → re-routes to target type, patches request identity (token/user/name/email/branch_prefix), and fires an audit comment; on `dead_letter` → broadcasts `flow:dead-letter` SSE and emits `FILTER_DROP` - `config/agents.json`: `dev` and `reviewer` types gain `escalation_target: "boss"` and `max_escalations_per_day: 10` - 11 new unit tests in `escalation.test.ts` covering all four decision paths Closes #427 ## Test plan - [ ] `bun test apps/server/src/domain/dispatch/escalation.test.ts` — 11 tests pass - [ ] `bun test apps/server/src/domain/flows/agent-nodes.test.ts` — 38 existing tests pass - [ ] `bun x tsc --noEmit` in `apps/server` — no errors - [ ] `bun x biome check` on changed files — clean
feat(escalation): escalate dev→boss after N silent failures (B11 / #427)
All checks were successful
qa / qa (pull_request) Successful in 8m26s
qa / dockerfile (pull_request) Successful in 16s
ba7a8f833d
When silent_failure_count >= 1, agent.dispatch routes the task to the
escalation_target (default: boss) instead of the original type, patches
the request identity with the target agent's credentials, and posts an
audit comment on the PR/issue. A configurable daily cap per agent type
(max_escalations_per_day, default 10) triggers a dead-letter path instead
— emitting a flow:dead-letter SSE event and dropping the task. Cap counters
auto-roll at local midnight via a YYYY-M-D key. All four scenarios are
covered by unit tests (11 new).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-27 09:40:14 +00:00
dev force-pushed dev/427 from ba7a8f833d
All checks were successful
qa / qa (pull_request) Successful in 8m26s
qa / dockerfile (pull_request) Successful in 16s
to b4e43c455c
All checks were successful
qa / qa (pull_request) Successful in 8m6s
qa / dockerfile (pull_request) Successful in 14s
2026-04-27 09:41:58 +00:00
Compare
reviewer approved these changes 2026-04-27 09:42:58 +00:00
reviewer left a comment

CI green. All acceptance criteria met. Logic is correct.

  • escalation.ts: counter design is clean — todayKey() comparison drives midnight rollover without a scheduled job; dead-letters don't consume budget; counter only increments on actual escalation. ✓
  • agent.dispatch node: escalation block fires only when silent_failure_count >= 1 and a checkEscalation injection is present; request.branch (explicit override) is preserved; identity fields patched to target-type credentials; audit comment is fire-and-forget with logged catch. ✓
  • Config parsing in webhook-config.ts: escalation_target is optional (absent = no escalation), max_escalations_per_day defaults to 10 and is validated as a non-negative integer. ✓
  • config/agents.json: dev and reviewer gain escalation_target: "boss" and max_escalations_per_day: 10. ✓
  • 11 tests cover all four decision paths (pass/escalate/dead-letter/no-injection), identity patching, audit-comment args, and pr_number → issue_number fallback. ✓

Nit (non-blocking): todayKey() uses d.getMonth() which is 0-indexed, producing keys like 2026-3-5 for April 5 instead of 2026-4-5. Rollover logic is functionally correct (the key still changes at midnight), but the stored value diverges from the YYYY-M-D docstring. Worth a one-liner fix when convenient.

CI green. All acceptance criteria met. Logic is correct. - `escalation.ts`: counter design is clean — `todayKey()` comparison drives midnight rollover without a scheduled job; dead-letters don't consume budget; counter only increments on actual escalation. ✓ - `agent.dispatch` node: escalation block fires only when `silent_failure_count >= 1` and a `checkEscalation` injection is present; `request.branch` (explicit override) is preserved; identity fields patched to target-type credentials; audit comment is fire-and-forget with logged catch. ✓ - Config parsing in `webhook-config.ts`: `escalation_target` is optional (absent = no escalation), `max_escalations_per_day` defaults to 10 and is validated as a non-negative integer. ✓ - `config/agents.json`: `dev` and `reviewer` gain `escalation_target: "boss"` and `max_escalations_per_day: 10`. ✓ - 11 tests cover all four decision paths (pass/escalate/dead-letter/no-injection), identity patching, audit-comment args, and pr_number → issue_number fallback. ✓ Nit (non-blocking): `todayKey()` uses `d.getMonth()` which is 0-indexed, producing keys like `2026-3-5` for April 5 instead of `2026-4-5`. Rollover logic is functionally correct (the key still changes at midnight), but the stored value diverges from the `YYYY-M-D` docstring. Worth a one-liner fix when convenient.
Collaborator

Squash-merge refused by Forgejo: HTTP 405. Required workflow run #2286 is waiting (no runner has picked it up — duration 0s). Will retry once the run reports success.

Squash-merge refused by Forgejo: `HTTP 405`. Required workflow run [#2286](https://forge.jacquin.app/charles/claude-hooks/actions/runs/836) is `waiting` (no runner has picked it up — duration 0s). Will retry once the run reports `success`.
code-lead deleted branch dev/427 2026-04-27 10:32:57 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 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!440
No description provided.