tasks: task breakdown for SLA and escalation feature (008)

48 tasks across 9 phases (6 user stories + setup/foundational/polish),
sequenced US1 (policy definition) -> US2 (calendar-aware run creation) ->
US3 (durable pause/resume, P1-complete MVP) -> US4 (breach detection) ->
US5 (breach-triggered escalation) -> US6 (manual escalation), each
dependent on the last since every story builds on the previous one's
mechanism rather than being independently orderable.

Also folds in two design refinements found while cross-checking the
existing scaffold against research.md's plan: TICKET_ASSIGNED (defined in
domain-events.ts since 007, never published) becomes the real wiring point
for SLA-run creation, and src/jobs/sla//src/jobs/escalation/ turn out to
already exist as their own stub scaffolding, reused rather than
duplicated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
saqib mir
2026-09-03 12:27:40 +05:30
co-authored by Claude Sonnet 5
parent 199bd4eb4e
commit bb31e9d641
3 changed files with 451 additions and 3 deletions
+15 -3
View File
@@ -112,9 +112,17 @@ supporthub-api/
│ BusinessCalendar, Holiday, EscalationPolicy,
│ EscalationRule, EscalationEvent
├── src/
│ ├── infrastructure/
│ │ └── queue/ # MODIFIED — register the breach-detection
│ │ repeatable job alongside existing workers
│ ├── events/
│ │ └── handlers/index.ts # MODIFIED — first real publish of the
│ │ existing-but-unused TICKET_ASSIGNED event
│ │ (from 007's persistAndTransition), plus two
│ │ new TICKET_UPDATED subscribers (pause/
│ │ resume, completion) — research.md
│ ├── jobs/
│ │ └── sla/index.ts # REPLACED stub — schedules the repeatable
│ │ breach-detection job (research.md); jobs/
│ │ escalation/ stays untouched (reserved for a
│ │ future notification-dispatch step)
│ └── modules/
│ ├── platform/
│ │ └── business-calendars/ # REPLACED stub — full standard shape +
@@ -123,6 +131,10 @@ supporthub-api/
│ │ │ mapper/ constants/ index.ts
│ │ └── calculators/
│ └── orchestration/
│ ├── assignments/ # 007, MODIFIED — persistAndTransition
│ │ └── engine/assignment.engine.ts publishes TICKET_ASSIGNED; new
│ │ assignToSpecificNode() method for
│ │ escalation's scoped re-assignment
│ ├── sla/ # REPLACED stub — full standard shape, keeps
│ │ ├── controller/ routes/ schema/ its existing calculators/ dir (due-date
│ │ │ repository/ service/ types/ calculator replaced, not removed) and adds
+49
View File
@@ -166,6 +166,55 @@
- **Alternatives considered**: A minimal condition-matching evaluator (e.g. `{ minPriority }`) —
rejected as speculative; spec.md never asked for conditional rule filtering beyond trigger type.
## Decision: SLA-run lifecycle is wired entirely through the existing domain-event bus
- **Decision**: `DomainEventName.TICKET_ASSIGNED` — defined in `src/events/domain-events.ts`
since 007 but never actually published by any code — is published for the first time by
`AssignmentEngine.persistAndTransition` (007's single shared success path for automatic,
manual, and this feature's new scoped-escalation assignment) with `{ ticketId, agentId,
strategy, actor }`. A new subscriber in `src/events/handlers/index.ts` reacts by resolving the
applicable `SLAPolicy` and creating the `SLARun` — but only if `ticketId` doesn't already have
one (`SLARun.ticketId @unique` makes this a natural existence check), so a re-escalation's
second `TICKET_ASSIGNED` publish (spec.md Assumptions: 1:1 with the *first* assignment only)
is correctly a no-op. Two further `TICKET_UPDATED` subscribers (same file, same pattern as
005's and 007's own) watch for `newStatus === 'WAITING_FOR_CUSTOMER'` (pause) /
`previousStatus === 'WAITING_FOR_CUSTOMER'` (resume), and for `newStatus === 'RESOLVED'`
(complete, per 003's state machine — `RESOLVED` is the terminal status every path reaches
before `CLOSED`/`REOPENED`).
- **Rationale**: Same "a module never needs to import another module it affects" decoupling this
codebase has used consistently since 005 — `orchestration/assignments` doesn't need to know
`orchestration/sla` exists, and `ticketing/tickets` already doesn't know about any of its
status-change consumers. Publishing `TICKET_ASSIGNED` for real is the natural use of an event
this codebase already named and reserved for exactly this purpose.
- **Alternatives considered**: A direct call from `AssignmentEngine.persistAndTransition` into an
`orchestration/sla` service method — rejected; would create the exact cross-module coupling
007→008 the event bus exists to avoid, and would need every future consumer of "a ticket got
assigned" to be added as another direct call in 007's own code.
## Decision: The breach-detection job reuses `src/jobs/sla/`'s existing stub; escalation firing reuses `src/jobs/escalation/`'s
- **Decision**: `registerSlaWorker()` (`src/jobs/sla/index.ts`, currently just a log line) is
extended to, on startup, schedule one BullMQ repeatable job (`queueManager.getQueue(QueueName
.SLA).add('detect-breaches', {}, { repeat: { every: 60_000 } })`) whose processor calls a
single, directly-callable, side-effect-only method — `slaService.runBreachDetectionSweep()` —
containing 100% of the actual logic: the two polling queries from research.md's breach-
detection decision, marking runs breached/first-response-breached, and, for each new breach,
calling `escalationService.handleBreach(ticketId, triggerType)` directly (a plain in-process
call, not a second queued job) since escalation firing has no meaningful reason to be
async-relative-to-detection. `src/jobs/escalation/`'s existing `registerEscalationWorker()`
stub, and its `ESCALATION` queue, are left untouched — reserved, per their own existing
scaffold, for a possible future async notification-dispatch step (spec.md Assumptions: no
notification delivery is built by this feature).
- **Rationale**: `runBreachDetectionSweep()` being a plain importable async function (not
reachable only through a running BullMQ worker) is what makes it possible to write an
integration test for "one job tick" without a real running worker process or a real 60-second
wait — the exact "no worker process in this test, call the job's own logic inline" convention
already established by `tests/integration/ticket-attachments.test.ts` for the malware-scan job.
- **Alternatives considered**: Splitting detection and escalation firing into two separately
queued BullMQ jobs (using the `ESCALATION` queue for the firing step) — rejected as an
unnecessary indirection; nothing in spec.md requires escalation firing to be decoupled in time
from the breach that caused it, and a single sweep function is simpler to test and reason about.
## Decision: SLA/Escalation admin endpoints reuse the existing auth stub
- **Decision**: Every admin CRUD endpoint (policies, calendars, escalation rules) and the manual-
+387
View File
@@ -0,0 +1,387 @@
---
description: "Task list for 008-sla-escalation"
---
# Tasks: SLA and Escalation
**Input**: Design documents from `specs/008-sla-escalation/`
**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md),
[data-model.md](./data-model.md),
[contracts/sla-escalation-contract.md](./contracts/sla-escalation-contract.md),
[quickstart.md](./quickstart.md)
**Tests**: Included as first-class tasks. This feature has real, extractable pure logic (the
calendar-walk algorithm, most-specific policy match, breach/no-breach/paused-no-breach logic)
plus — for the first time since the constitution's Principle VII was written — a genuine
process-restart-survival requirement that needs a dedicated test rebuilding `buildApp()`
mid-test, not just a within-process concurrency test.
**Organization**: Tasks are grouped by user story (US1 = P1 policy definition, US2 = P1 run
creation with calendar-aware due dates, US3 = P1 durable pause/resume, US4 = P2 breach detection,
US5 = P2 breach-triggered escalation, US6 = P3 manual escalation).
## Format: `[ID] [P?] [Story] Description`
All file paths are relative to `supporthub-api/` (repo root).
---
## Phase 1: Setup
- [ ] T001 [P] Populate `src/modules/platform/business-calendars/` with the full standard shape
(`controller/`, `routes/`, `schema/`, `repository/`, `service/`, `types/`, `mapper/`,
`constants/`, `index.ts`) plus a `calculators/` directory, replacing the existing
`BusinessCalendarsService.isWorkingHour` stub's content
- [ ] T002 [P] Extend `src/modules/orchestration/sla/` to the full standard shape around its
existing `engine/`/`calculators/` directories, replacing every stub file's content
(`SlaEngine.evaluateSlaTargets`, `SlaDueDateCalculator.calculateDueTime`)
- [ ] T003 [P] Extend `src/modules/orchestration/escalation/` to the full standard shape around
its existing `engine/` directory, replacing the `EscalationEngine.triggerEscalation` stub's
content
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Schema for every entity, shared by every user story.
**⚠️ CRITICAL**: No user-story stage work can begin until this phase is complete.
- [ ] T004 Add `SLAPolicy`, `SLARun` (incl. the additive `firstResponseBreachedAt` refinement),
`BusinessCalendar`, `Holiday`, `EscalationPolicy`, `EscalationRule`, `EscalationEvent`
models to `prisma/schema.prisma` per data-model.md, plus `Ticket.slaRun`/
`Ticket.escalationEvents`, `Product.slaPolicies`/`Product.escalationPolicies`,
`Category.slaPolicies`, `HierarchyNode.escalationRules` back-relations, and an
`SLARun @@index([status, resolutionDueAt])` for the breach-detection sweep (depends on
T001-T003)
- [ ] T005 Run `npm run prisma:generate` and create the migration (`npm run prisma:migrate`) for
T004 (depends on T004)
**Checkpoint**: Schema migrated. User stories can now be built.
---
## Phase 3: User Story 1 - Admin defines SLA policies as configuration (Priority: P1) 🎯 MVP (part 1)
**Goal**: `SLAPolicy` CRUD and the most-specific-match resolution function exist and are
independently correct — not yet wired to ticket assignment.
**Independent Test**: Quickstart Scenario 1.
### Tests for User Story 1
- [ ] T006 [P] [US1] Unit tests for `findApplicablePolicy` (specificity-count match, wildcard
handling on each of the 4 scope dimensions independently, tie-break by latest `updatedAt`,
no-match returns `null`) in `tests/unit/orchestration/sla-policy-match.test.ts`
- [ ] T007 [US1] Integration test covering Quickstart Scenario 1 (a product-scoped policy is
preferred over a global one; deactivating it falls back to the global policy) against a
real Postgres in `tests/integration/sla-policy-resolution.test.ts` (depends on T005)
### Implementation for User Story 1
- [ ] T008 [US1] Add `SLAPolicyRepository` (CRUD, `findActiveCandidates(scope)`) and the Zod
create/update schema — with resolve-or-404 existence checks for `productId`/`categoryId`/
`businessCalendarId` when provided (research.md) — in `sla/repository/` + `sla/schema/`
(depends on T005)
- [ ] T009 [US1] Add `findApplicablePolicy(ticketContext)` (specificity-count + tie-break, per
data-model.md's Resolution section) in `sla/service/sla-policy-resolver.service.ts`
(depends on T008)
- [ ] T010 [US1] Add `POST/GET/GET:id/PATCH/DELETE /admin/sla-policies` routes (soft-delete via
`active: false`, gated by `fastify.authenticate`) in `sla/controller/` + `sla/routes/`,
registered from `src/api/routes.ts` (depends on T008)
- [ ] T011 [US1] Run Quickstart Scenario 1 locally and confirm all 4 steps pass
**Checkpoint**: SLA policies can be defined and correctly resolved. Nothing creates an `SLARun`
yet — that's User Story 2.
---
## Phase 4: User Story 2 - SLA run starts automatically with calendar-aware due dates (Priority: P1) 🎯 MVP (part 2)
**Goal**: `BusinessCalendar`/`Holiday` CRUD, the calendar-walk algorithm, and `SLARun` creation
wired into 007's assignment-success path via the first real publish of `TICKET_ASSIGNED`.
**Independent Test**: Quickstart Scenario 2.
### Tests for User Story 2
- [ ] T012 [P] [US2] Unit tests for `addBusinessMinutes` — weekend exclusion, holiday exclusion,
partial-day clipping on the start day, a day with no configured window contributing zero
time, and correctness across a DST transition in the calendar's own timezone — in
`tests/unit/platform/business-calendars/calendar-walk.test.ts`
- [ ] T013 [US2] Integration test covering Quickstart Scenario 2 (calendar-aware due date lands
the next working day past a weekend+holiday, never a naive addition; a ticket assigned with
no matching policy gets no `SLARun` and `GET .../sla-run` returns `404`) against a real
Postgres in `tests/integration/sla-run-creation.test.ts` (depends on T005, T009, and 007's
existing assignment flow)
### Implementation for User Story 2
- [ ] T014 [US2] Add `addBusinessMinutes(start, minutes, calendar, holidays)` using `luxon` in
`business-calendars/calculators/business-hours.calculator.ts`, replacing the
`isWorkingHour` stub's logic (research.md's day-by-day walk)
- [ ] T015 [US2] Add `BusinessCalendarRepository`/`HolidayRepository`, Zod schema (IANA timezone
validation, `HH:mm` + `start < end` validation per data-model.md), and
`POST/GET/GET:id/PATCH /admin/business-calendars` +
`POST /admin/business-calendars/:id/holidays` +
`DELETE /admin/business-calendars/:id/holidays/:holidayId` routes in
`business-calendars/repository/` + `schema/` + `controller/` + `routes/` (depends on T014)
- [ ] T016 [US2] Replace `SlaDueDateCalculator.calculateDueTime`'s naive addition with a call
into T014's `addBusinessMinutes` (via `business-calendars`'s public `index.ts` — FR-004) in
`sla/calculators/sla-due-date.calculator.ts` (depends on T014)
- [ ] T017 [US2] Add `AssignmentEngine.persistAndTransition` (007,
`src/modules/orchestration/assignments/engine/assignment.engine.ts`) publishing
`DomainEventName.TICKET_ASSIGNED` (`{ ticketId, agentId, strategy, actor }`) after its
existing persistence step — the event is already defined in `src/events/domain-events.ts`
but has never been published (research.md)
- [ ] T018 [US2] Add `SlaService.handleTicketAssigned(ticketId, agentId)`: no-ops if the ticket
already has an `SLARun` (`SLARun.ticketId @unique` — covers re-escalation's second publish,
spec.md Assumptions); otherwise resolves the applicable policy (T009), computes
`firstResponseDueAt`/`resolutionDueAt` via T016, and creates the `SLARun` — in
`sla/service/sla.service.ts` (depends on T009, T016)
- [ ] T019 [US2] Subscribe `DomainEventName.TICKET_ASSIGNED` to T018's handler in
`src/events/handlers/index.ts`, following the existing "module never imports the module it
affects" registration pattern (depends on T017, T018)
- [ ] T020 [US2] Add `GET /tickets/:ticketId/sla-run` route (`404` if none) in `sla/controller/` +
`sla/routes/` (depends on T018)
- [ ] T021 [US2] Run Quickstart Scenario 2 locally and confirm all 4 steps pass
**Checkpoint**: Every successfully-assigned ticket with a matching policy gets an `SLARun` with
correctly calendar-computed due dates. MVP-complete for read-only SLA visibility.
---
## Phase 5: User Story 3 - SLA pause/resume is durable across a process restart (Priority: P1)
**Goal**: `WAITING_FOR_CUSTOMER` transitions pause/resume the run by shifting its absolute due
dates — no in-memory state anywhere, verified across an actual rebuilt `buildApp()`.
**Independent Test**: Quickstart Scenario 3.
### Tests for User Story 3
- [ ] T022 [P] [US3] Unit tests for the pause/resume shift arithmetic (resume shifts both due
dates forward by exactly `now - pausedAt`; a second pause/resume cycle composes correctly)
in `tests/unit/orchestration/sla-pause-resume.test.ts`
- [ ] T023 [US3] Integration test covering Quickstart Scenario 3 — including rebuilding
`buildApp()` mid-test to simulate a real process restart while paused, then asserting the
resumed due date is exactly the original plus the paused wall-clock duration — against a
real Postgres in `tests/integration/sla-pause-resume.test.ts` (depends on T018)
### Implementation for User Story 3
- [ ] T024 [US3] Add `SlaService.pause(ticketId)` / `resume(ticketId)` (shift
`firstResponseDueAt`/`resolutionDueAt` forward by the paused duration on resume, per
research.md/data-model.md — no separate remaining-minutes field) in `sla/service/
sla.service.ts` (depends on T018)
- [ ] T025 [US3] Subscribe two `DomainEventName.TICKET_UPDATED` handlers in
`src/events/handlers/index.ts``newStatus === 'WAITING_FOR_CUSTOMER'` calls T024's
`pause`, `previousStatus === 'WAITING_FOR_CUSTOMER'` calls `resume` — alongside the existing
005/007 subscribers on the same event (depends on T024)
- [ ] T026 [US3] Subscribe a third `TICKET_UPDATED` handler — `newStatus === 'RESOLVED'` sets
`SLARun.completedAt` and `status: 'completed'` (data-model.md) — in the same file (depends
on T018)
- [ ] T027 [US3] Run Quickstart Scenario 3 locally and confirm all 4 steps pass, including the
restart-boundary step
**Checkpoint**: Every P1 user story is complete. SLA runs are created, calendar-computed, and
durably pause/resume-correct. This is the feature's MVP.
---
## Phase 6: User Story 4 - Breaches are detected even if no one is watching in real time (Priority: P2)
**Goal**: A repeatable BullMQ job durably detects both resolution and first-response breaches,
never missing one because the process wasn't running at the due instant, never flagging a
completed-in-time or paused run.
**Independent Test**: Quickstart Scenario 4.
### Tests for User Story 4
- [ ] T028 [P] [US4] Unit tests for the breach-detection predicate logic (a `running` run past
`resolutionDueAt` breaches; a `paused` run past `resolutionDueAt` does not; a `completed`
run does not; a `running` run past `firstResponseDueAt` with no prior `AGENT_MESSAGE`
breaches first-response exactly once, guarded by `firstResponseBreachedAt`) in
`tests/unit/orchestration/sla-breach-detection.test.ts`
- [ ] T029 [US4] Integration test covering Quickstart Scenario 4 (a short-`resolutionMinutes`
policy breaches within one sweep call; resolved-in-time and paused runs are never breached
even after their due instant passes) against a real Postgres in
`tests/integration/sla-breach-detection.test.ts` (depends on T018, T024)
### Implementation for User Story 4
- [ ] T030 [US4] Add `SlaService.runBreachDetectionSweep()` — queries every `running` `SLARun`
with `resolutionDueAt <= now()` (marks `breached`/`breachedAt`) and every `running` run with
`firstResponseDueAt <= now()` and `firstResponseBreachedAt: null` and no `AGENT_MESSAGE`
recorded for the ticket (marks `firstResponseBreachedAt`) — a single, directly-callable,
side-effect-only method (research.md — no worker process needed to invoke it in tests) in
`sla/service/sla.service.ts` (depends on T024, T026)
- [ ] T031 [US4] Replace `registerSlaWorker()`'s stub body in `src/jobs/sla/index.ts`: on
registration, schedule a BullMQ repeatable job on `QueueName.SLA` (`{ repeat: { every:
60_000 } }`) whose processor calls T030's `runBreachDetectionSweep` (depends on T030)
- [ ] T032 [US4] Run Quickstart Scenario 4 locally and confirm all 5 steps pass
**Checkpoint**: Breaches are durably detected. Nothing reacts to a breach yet beyond marking the
run — that's User Story 5.
---
## Phase 7: User Story 5 - A breach automatically triggers rule-driven escalation (Priority: P2)
**Goal**: `EscalationPolicy`/`EscalationRule` CRUD, breach-triggered `EscalationEvent` firing, and
a new scoped-assignment entry point on 007's `AssignmentEngine` that re-assigns to exactly the
rule's `targetNodeId`.
**Independent Test**: Quickstart Scenario 5.
### Tests for User Story 5
- [ ] T033 [P] [US5] Unit tests for escalation-policy resolution (product-specific preferred over
global, per research.md) and rule matching (every active rule whose `triggerType` matches
the firing breach type fires; an inactive or wrong-trigger-type rule doesn't) in
`tests/unit/orchestration/escalation-rule-match.test.ts`
- [ ] T034 [US5] Integration test covering Quickstart Scenario 5 (a breach with a matching rule
produces exactly one `EscalationEvent` and reassigns to an agent eligible under the rule's
specific `targetNodeId`, not the ticket's originally-resolved node; a breach with no
matching rule is still recorded breached with no `EscalationEvent`) against a real Postgres
in `tests/integration/sla-escalation-firing.test.ts` (depends on T030)
### Implementation for User Story 5
- [ ] T035 [US5] Add `EscalationPolicyRepository`/`EscalationRuleRepository` (CRUD,
`findActiveRules(policyId, triggerType)`), Zod schema (all 10 doc-05 `triggerType` values
accepted; `targetNodeId` resolve-or-404 at rule creation, FR-012) in
`escalation/repository/` + `escalation/schema/` (depends on T005)
- [ ] T036 [US5] Add `POST/GET /admin/escalation-policies`,
`POST/PATCH/DELETE /admin/escalation-policies/:id/rules[/:ruleId]` routes in
`escalation/controller/` + `escalation/routes/` (depends on T035)
- [ ] T037 [US5] Add `AssignmentEngine.assignToSpecificNode(ticketId, hierarchyNodeId, actor,
reason?, strategyOverride?)` (007, `assignments/engine/assignment.engine.ts`) — resolves
the eligible-agent set scoped to exactly the given node (reusing `RoutingService`'s
capability-lookup call, research.md) and persists through the existing
`persistAndTransition` (T017), so it also publishes `TICKET_ASSIGNED` for free (depends on
T017)
- [ ] T038 [US5] Add `EscalationService.handleBreach(ticketId, triggerType)`: resolves the
applicable `EscalationPolicy` (product-match-or-global, research.md), finds every active
matching `EscalationRule` (T035), and for each, creates an `EscalationEvent`
(`ruleId`, `fromNodeId` from the ticket's current assignment, `toNodeId: rule.targetNodeId`,
`triggeredBy: 'system'`) and calls T037's `assignToSpecificNode` — records nothing when no
rule matches (FR-015) — in `escalation/service/escalation.service.ts` (depends on T035,
T037)
- [ ] T039 [US5] Wire T030's `runBreachDetectionSweep` to call T038's `handleBreach` for each
newly-detected breach, passing the corresponding trigger type (`resolution_breach` /
`first_response_breach`) — in `sla/service/sla.service.ts` (depends on T030, T038)
- [ ] T040 [US5] Run Quickstart Scenario 5 locally and confirm all 3 steps pass
**Checkpoint**: Breaches automatically escalate through rule-driven, scoped re-assignment.
---
## Phase 8: User Story 6 - A human can manually escalate a ticket to a specific node (Priority: P3)
**Goal**: The same `EscalationEvent` + scoped-reassignment mechanism, triggered explicitly by a
caller instead of a breach.
**Independent Test**: Quickstart Scenario 6.
### Tests for User Story 6
- [ ] T041 [US6] Integration test covering Quickstart Scenario 6 (manual escalation creates an
`EscalationEvent` with `ruleId: null` and reassigns via the scoped path; a nonexistent
`targetNodeId` returns `404` with no event created; a manual escalation racing an automatic
breach escalation on the same ticket records both events without a corrupted final
assignment) against a real Postgres in `tests/integration/manual-escalation.test.ts`
(depends on T037, T038)
### Implementation for User Story 6
- [ ] T042 [US6] Add `EscalationService.escalateManually(ticketId, targetNodeId, actor, reason)`:
resolve-or-404 on `targetNodeId` (FR-017), creates an `EscalationEvent` (`ruleId: null`,
`triggeredBy: actor`) and calls T037's `assignToSpecificNode` — in `escalation/service/
escalation.service.ts` (depends on T037)
- [ ] T043 [US6] Add `POST /tickets/:ticketId/escalate` route (gated by `fastify.authenticate`)
in `escalation/controller/` + `escalation/routes/`, registered from `src/api/routes.ts`
(depends on T042)
- [ ] T044 [US6] Run Quickstart Scenario 6 locally and confirm all 4 steps pass
**Checkpoint**: All six user stories work independently and together — policy definition,
calendar-aware run creation, durable pause/resume, durable breach detection, and both automatic
and manual escalation form one coherent, restart-safe flow.
---
## Phase 9: Polish & Cross-Cutting Concerns
- [ ] T045 [P] Add an "SLA and Escalation" section to `README.md` describing the calendar-aware
due-date computation, the durable pause/resume mechanism, the breach-detection job interval,
which 2 of doc 05's 10 escalation trigger types actually fire, and what's explicitly
deferred (notification delivery, investigation/customer-response timers, reopen-cycle SLA
restart)
- [ ] T046 [P] Update `specs/008-sla-escalation/checklists/requirements.md` Notes with any
implementation-time findings
- [ ] T047 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
- [ ] T048 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing broke
elsewhere, then the full integration suite (including 007's own suite, since T017/T037
modify its `AssignmentEngine`) against real Docker-provisioned Postgres/Redis
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies
- **Foundational (Phase 2)**: Depends on Setup — BLOCKS all user stories
- **User Story 1 (Phase 3)**: Depends on Foundational — no dependency on US2-US6
- **User Story 2 (Phase 4)**: Depends on US1 (the policy it resolves against) — genuinely not
independent, same class of dependency 007's US2 had on US1
- **User Story 3 (Phase 5)**: Depends on US2 (the run it pauses/resumes)
- **User Story 4 (Phase 6)**: Depends on US3 (a run that can be paused must be excluded from
breach detection correctly, so the pause mechanism must exist first)
- **User Story 5 (Phase 7)**: Depends on US4 (the breach it reacts to) and on 007's
`AssignmentEngine` (T037's new method)
- **User Story 6 (Phase 8)**: Depends on US5 (T037/T038's scoped-reassignment mechanism, reused
directly rather than duplicated)
- **Polish (Phase 9)**: Depends on all six user stories
### Parallel Opportunities
- T001/T002/T003 (independent scaffolding)
- T006 (unit tests) alongside T008-T009 (the implementations they test)
- T012 (unit tests) alongside T014 (the implementation it tests)
- T022 alongside T024; T028 alongside T030; T033 alongside T035/T038
- T045/T046 in Polish
### Sequencing Note
T017 (publishing `TICKET_ASSIGNED` from 007's `AssignmentEngine`) and T037 (the new
`assignToSpecificNode` method on the same class) both modify a file 007 already owns and has its
own passing test suite for — run 007's full integration suite (part of T048) after each, not only
at the very end, to catch a regression close to its cause.
---
## Implementation Strategy
### MVP First (User Stories 1-3 Only)
1. Setup + Foundational (T001-T005)
2. User Story 1 (T006-T011) — policies exist and resolve correctly
3. User Story 2 (T012-T021) — runs are created with real calendar-aware due dates
4. User Story 3 (T022-T027) — pause/resume is durable, including across a restart
5. **STOP and VALIDATE**: Quickstart Scenarios 1-3 pass — every assigned ticket has a correctly
computed, durably pausable `SLARun`. Nothing reacts to a breach yet — that value lands with
User Story 4/5.
### Incremental Delivery
1. Setup + Foundational → schema migrated
2. Add User Story 1 → SLA policies are configurable and resolve correctly
3. Add User Story 2 → runs are created automatically with calendar-aware due dates
4. Add User Story 3 → pause/resume is durable (P1-complete, MVP)
5. Add User Story 4 → breaches are durably detected
6. Add User Story 5 → breaches automatically escalate and reassign
7. Add User Story 6 → manual escalation exists, reusing the same mechanism
8. Polish → docs and full regression