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:
co-authored by
Claude Sonnet 5
parent
846b9e8dca
commit
67eee83ff0
@@ -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
|
||||
Reference in New Issue
Block a user