docs: task breakdown for orchestration and assignment feature

34 tasks across setup, foundational schema work, and five user stories
(automatic resolution, pluggable strategies with a dedicated concurrency
test, durable history, manual assignment, and re-escalation
verification). Notes the T021/T017 sequencing exception where an
implementation dependency crosses story-priority order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
saqib mir
2026-09-03 11:31:30 +05:30
co-authored by Claude Sonnet 5
parent 846b9e8dca
commit 67eee83ff0
+317
View File
@@ -0,0 +1,317 @@
---
description: "Task list for 007-orchestration-assignment"
---
# Tasks: Orchestration and Assignment
**Input**: Design documents from `specs/007-orchestration-assignment/`
**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md),
[data-model.md](./data-model.md), [contracts/orchestration-contract.md](./contracts/orchestration-contract.md),
[quickstart.md](./quickstart.md)
**Tests**: Included as first-class tasks. This feature has real, extractable pure logic (each
strategy's selection function) plus — for the first time since 003-ticketing — a genuine
concurrency requirement that needs a dedicated concurrency test, per the constitution's own
Testing gate.
**Organization**: Tasks are grouped by user story (US1 = P1 resolution, US2 = P1 strategies,
US3 = P2 history, US4 = P2 manual assignment, US5 = P3 re-escalation).
## Format: `[ID] [P?] [Story] Description`
All file paths are relative to `supporthub-api/` (repo root).
---
## Phase 1: Setup
- [ ] T001 [P] Populate `src/modules/orchestration/routing/` with `service/`, `types/`,
`index.ts` (no controller/routes — internal only), replacing the existing
`RoutingService.routeTicket` stub
- [ ] T002 [P] Extend `src/modules/orchestration/assignments/` with the standard module shape
(`controller/`, `routes/`, `schema/`, `repository/`, `service/`, `types/`, `mapper/`,
`constants/`, `index.ts`) around its existing `engine/`/`strategies/`/`rules/`/
`calculators/` directories, replacing every stub file's content
- [ ] T003 [P] Populate `src/modules/orchestration/orchestration/` with `service/`, `types/`,
`index.ts`, replacing the existing `OrchestrationService.orchestrateWorkflow` stub
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Schema for both entities, shared by every user story.
**⚠️ CRITICAL**: No user-story stage work can begin until this phase is complete.
- [ ] T004 Add `Assignment` (with `isCurrent`, `unassignedAt`) and `AssignmentHistory` models to
`prisma/schema.prisma` per data-model.md, plus `Ticket.assignments`/
`Ticket.assignmentHistory`/`Agent.assignments` back-relations (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 - A human-escalated ticket is automatically routed to eligible agents (Priority: P1) 🎯 MVP
**Goal**: On `HUMAN_ESCALATION`, resolve the applicable hierarchy node(s) and compute the
capability-eligible agent set — reusing 006 directly, never a second matching algorithm.
**Independent Test**: Quickstart Scenario 1.
### Tests for User Story 1
- [ ] T006 [US1] Integration test covering Quickstart Scenario 1 (escalation triggers
resolution automatically; matching-node case scopes correctly; no-match case falls back to
skill-only) against a real Postgres in `tests/integration/orchestration-resolution.test.ts`
(depends on T005)
### Implementation for User Story 1
- [ ] T007 [US1] Add `RoutingService.resolveEligibleAgents(ticketId)`: reads the ticket's
product/category/priority context, calls 006's `capabilityLookupService.findEligibleAgents`
directly (research.md — through `orchestration/hierarchy`'s public `index.ts`) with the
ticket's own required-skill context, and also returns which hierarchy node(s), if any,
matched (needed later for the node's own `assignmentStrategy`, T013) — in
`src/modules/orchestration/routing/service/routing.service.ts` (depends on T005)
- [ ] T008 [US1] Add the domain-event subscriber skeleton: register a
`DomainEventName.TICKET_UPDATED` handler in `src/events/handlers/index.ts` (alongside
005's existing subscriber) that calls into `orchestration`'s engine (T017) when
`payload.newStatus === 'HUMAN_ESCALATION'` — wired now so T007's resolution is reachable
end-to-end even before US2's strategy execution exists (depends on T007)
- [ ] T009 [US1] Run Quickstart Scenario 1 locally and confirm all 4 steps pass
**Checkpoint**: Every ticket reaching human escalation has its hierarchy node and eligible-agent
set resolved automatically. Nothing is assigned yet — that's User Story 2.
---
## Phase 4: User Story 2 - A pluggable assignment strategy picks exactly one eligible agent (Priority: P1)
**Goal**: `ROUND_ROBIN` (concurrency-safe), `LEAST_LOADED`, `SKILL_BASED` actually select an
agent from the eligible set; an empty set or unimplemented strategy is a recorded non-outcome,
never a crash.
**Independent Test**: Quickstart Scenario 2. This story is also required to satisfy the
constitution's concurrency-testing gate for assignment.
### Tests for User Story 2
- [ ] T010 [P] [US2] Unit tests for each strategy's pure selection function — `ROUND_ROBIN`
given a fixed counter value picks the expected index; `LEAST_LOADED` picks the lowest-load
agent; `SKILL_BASED` picks the highest-matching-level agent; an empty eligible array
returns `null` for every strategy — in `tests/unit/orchestration/strategies.test.ts`
- [ ] T011 [US2] Concurrency test: fire many simultaneous `ROUND_ROBIN` selections against the
same eligible set and a real Redis instance, and confirm every selected index is exactly
what the sequence of atomic `INCR` results implies — no two concurrent calls ever compute
the same index from the same counter value, and the final counter value equals the number
of calls — in `tests/concurrency/round-robin.test.ts` (depends on T005)
- [ ] T012 [US2] Integration test covering Quickstart Scenario 2 steps 3-5 (`LEAST_LOADED`,
`SKILL_BASED`, empty-eligible-set outcome recorded) against a real Postgres in
`tests/integration/orchestration-strategies.test.ts` (depends on T005)
### Implementation for User Story 2
- [ ] T013 [US2] Add the round-robin selection function — `INCR` against
`ticketing:round_robin:<hierarchyNodeId ?? 'unscoped'>` via the existing shared Redis
client, then `(count - 1) % eligibleAgents.length` into the eligible array sorted by
`agent.id` (research.md) — in
`src/modules/orchestration/assignments/strategies/round-robin.strategy.ts` (replacing the
existing stub) (depends on T005)
- [ ] T014 [P] [US2] Add `LEAST_LOADED` (reads each candidate's `AgentAvailability.currentLoad`,
lowest wins, ties fall through to T013's cursor — research.md) in
`src/modules/orchestration/assignments/strategies/least-loaded.strategy.ts` +
`calculators/workload.calculator.ts` (replacing the existing stub)
- [ ] T015 [P] [US2] Add `SKILL_BASED` (reads each candidate's `AgentSkill.level` for the
required skills, highest total wins, ties fall through to T013's cursor) in
`src/modules/orchestration/assignments/strategies/skill-based.strategy.ts` +
`calculators/skill-match.calculator.ts`
- [ ] T016 [US2] Add a strategy registry (`STRATEGY_REGISTRY: Record<string, (eligible, context)
=> Promise<Agent | null>>`) covering `ROUND_ROBIN`/`LEAST_LOADED`/`SKILL_BASED`, with
`MANUAL`/`DIRECT` deliberately absent (those are never auto-selected — they only ever come
from an explicit caller-supplied `agentId`, T024) — an unregistered strategy name resolves
to `null` (no agent), matching FR-008 — in
`src/modules/orchestration/assignments/service/strategy-registry.ts` (depends on T013,
T014, T015)
- [ ] T017 [US2] Add `AssignmentEngine.evaluateAndAssign(ticketId)` (replacing the existing
stub): calls T007's resolution, then T016's registry using the resolved node's
`assignmentStrategy` (or a configurable system default when no node matched), persists the
result via T019/T020's repository (approved agent → new `Assignment` + `assigned`
`AssignmentHistory`; no agent → `AssignmentHistory` alone with `agentId: null`), and on
success transitions the ticket via `ticketsService.updateStatus(ticketId, 'IN_PROGRESS',
ticket.version, 'system')` (research.md) — in
`src/modules/orchestration/assignments/engine/assignment.engine.ts` (depends on T016)
- [ ] T018 [US2] Wire T008's event-subscriber skeleton to actually call T017's
`evaluateAndAssign` — completing the automatic escalation→assignment flow — in
`src/modules/orchestration/orchestration/service/orchestration.service.ts` (depends on
T017)
- [ ] T019 [US2] Run Quickstart Scenario 2 locally and confirm all 5 steps pass
**Checkpoint**: Escalated tickets are now actually assigned — the full automatic flow works end
to end, concurrency-safely.
---
## Phase 5: User Story 3 - Every assignment decision is durably recorded (Priority: P2)
**Goal**: `Assignment`/`AssignmentHistory` persistence is correct under reassignment — old rows
superseded, never lost.
**Independent Test**: Quickstart Scenario 3.
### Tests for User Story 3
- [ ] T020 [US3] Integration test covering Quickstart Scenario 3 (reassignment supersedes the
current `Assignment` row, both remain in `AssignmentHistory`, `GET .../assignment` reflects
only the latest) against a real Postgres in `tests/integration/orchestration-history.test.ts`
(depends on T005)
### Implementation for User Story 3
- [ ] T021 [US3] Add `AssignmentRepository` (`createAssignment` — in one transaction, sets any
existing current row's `isCurrent: false`/`unassignedAt: now()` then inserts the new
current row, matching research.md's version-row-per-period refinement; `findCurrent`;
`AssignmentHistoryRepository.record` — always additive, never updates) in
`src/modules/orchestration/assignments/repository/assignment.repository.ts` +
`repository/assignment-history.repository.ts` (depends on T005) — this is the repository
T017 (US2) already depends on; implemented here since US3's own story is what specifies its
correctness guarantees in detail
- [ ] T022 [US3] Add Zod schema + `GET /tickets/:ticketId/assignment`,
`GET /tickets/:ticketId/assignment-history` routes in `assignments/schema/` + `routes/` +
`controller/`, registered from `src/api/routes.ts` (depends on T021)
- [ ] T023 [US3] Run Quickstart Scenario 3 locally and confirm all 3 steps pass
**Checkpoint**: Assignment history is durable and correctly superseded, independently of whether
a reassignment came from automatic re-escalation or a manual override.
---
## Phase 6: User Story 4 - A human can manually assign or reassign a ticket (Priority: P2)
**Goal**: An explicit admin override, recorded through the same mechanism as an automatic
assignment.
**Independent Test**: Quickstart Scenario 4.
### Tests for User Story 4
- [ ] T024 [US4] Integration test covering Quickstart Scenario 4 (manual assignment overrides;
nonexistent agent rejected with `404`; a second manual assignment supersedes the first)
against a real Postgres in `tests/integration/orchestration-manual-assignment.test.ts`
(depends on T021)
### Implementation for User Story 4
- [ ] T025 [US4] Add `AssignmentsService.assignManually(ticketId, agentId, actor, reason?,
strategy = 'MANUAL')`: validates the agent exists (resolve-or-404, research.md), then
reuses T021's repository the same way T017's automatic path does — in
`src/modules/orchestration/assignments/service/assignments.service.ts` (depends on T021)
- [ ] T026 [US4] Add Zod schema + `POST /admin/tickets/:ticketId/assignment` route (gated by
`fastify.authenticate`) in `assignments/schema/` + `routes/` + `controller/` (depends on
T025)
- [ ] T027 [US4] Run Quickstart Scenario 4 locally and confirm all 3 steps pass
**Checkpoint**: Both automatic and manual assignment paths exist, fully interchangeable from
history's point of view.
---
## Phase 7: User Story 5 - A ticket that still can't be resolved is re-escalated and reassigned (Priority: P3)
**Goal**: Re-escalation re-runs resolution and strategy selection fresh, never reusing a stale
eligible set.
**Independent Test**: Quickstart Scenario 5.
### Tests for User Story 5
- [ ] T028 [US5] Integration test covering Quickstart Scenario 5 (an already-assigned ticket
re-escalated gets a freshly computed eligible set and a new assignment; the prior one
remains in history) against a real Postgres in
`tests/integration/orchestration-re-escalation.test.ts` (depends on T018, T021)
### Implementation for User Story 5
- [ ] T029 [US5] Confirm (no new production code expected — this is a verification task): T008's
event subscriber and T017's engine already re-run resolution from scratch on every
`HUMAN_ESCALATION` event with no caching of a prior eligible set — if T028's test reveals
any staleness, fix it in `orchestration/routing`'s service (depends on T028)
- [ ] T030 [US5] Run Quickstart Scenario 5 locally and confirm both steps pass
**Checkpoint**: All five user stories work independently and together — escalation, resolution,
strategy selection, persistence, manual override, and re-escalation form one coherent,
concurrency-safe flow.
---
## Phase 8: Polish & Cross-Cutting Concerns
- [ ] T031 [P] Add an "Orchestration and Assignment" section to `README.md` describing the
automatic escalation trigger, the five strategies (and which ones are auto-selectable vs.
manual-only), the concurrency-safety mechanism, and what's explicitly deferred (SLA/
escalation-policy execution, workload lifecycle mutation)
- [ ] T032 [P] Update `specs/007-orchestration-assignment/checklists/requirements.md` Notes with
any implementation-time findings
- [ ] T033 Run `npx tsx scripts/check-architecture.ts` and `npm run lint`/`npm run typecheck`
- [ ] T034 Full regression: `npm run test:unit` (scoped to `tests/unit`) to confirm nothing broke
elsewhere, then the full integration + concurrency suite against real Docker-provisioned
infra
---
## 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-US5
- **User Story 2 (Phase 4)**: Depends on US1 (the resolution it assigns against) — genuinely not
independent, same class of dependency 005's US2 had on US1
- **User Story 3 (Phase 5)**: Depends on US2 (assignments to have history for) — though its own
repository (T021) is what US2's engine (T017) actually calls, so in practice T021 is built
before T017 completes even though the *story* is sequenced after
- **User Story 4 (Phase 6)**: Depends on US3's repository (T021) — independent of US2's automatic
strategies themselves
- **User Story 5 (Phase 7)**: Depends on US1/US2 (the flow being re-run) and US3 (history proving
nothing was lost) — almost entirely a verification story, not new mechanics
- **Polish (Phase 8)**: Depends on all five user stories
### Parallel Opportunities
- T001/T002/T003 (independent scaffolding)
- T010 (unit tests) alongside T013-T015 (the implementations they test)
- T014/T015 (independent strategy implementations) in parallel
- T031/T032 in Polish
### Sequencing Note
T021 (US3's repository) is a true prerequisite of T017 (US2's engine) despite being listed under
the later-priority story — this mirrors 006's own T013/T014 note that implementation dependency
order and user-story priority order aren't always the same thing; build T021 before or alongside
T017, not strictly after Phase 4 completes.
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Setup + Foundational (T001-T005)
2. User Story 1 (T006-T009)
3. **STOP and VALIDATE**: Quickstart Scenario 1 passes — every escalated ticket gets its
hierarchy node and eligible-agent set resolved automatically. Not yet assigning anyone — that
value lands with User Story 2.
### Incremental Delivery
1. Setup + Foundational → schema migrated
2. Add User Story 1 → resolution runs automatically on escalation
3. Add User Story 2 (with US3's repository built alongside it) → tickets are actually assigned,
concurrency-safely (MVP-complete automatic routing)
4. Add User Story 3's read endpoints → history is queryable, not just stored
5. Add User Story 4 → manual override exists
6. Add User Story 5 → re-escalation verified correct
7. Polish → docs and full regression