From e437711c2abd33a2c2e403b3294691d6b8741220 Mon Sep 17 00:00:00 2001 From: saqib mir Date: Thu, 3 Sep 2026 11:27:19 +0530 Subject: [PATCH] docs: spec for orchestration and assignment feature (007) Phase 7 of the roadmap: the orchestration engine (resolving 006's hierarchy nodes and capability-eligibility lookup against an escalated ticket), pluggable assignment strategies (ROUND_ROBIN/LEAST_LOADED/ SKILL_BASED/MANUAL/DIRECT) with concurrency-safe round robin, and durable assignment history. Explicitly excludes SLA/escalation-policy execution (Phase 8). Triggered via 005's existing domain-event publishing rather than a new notification path. Co-Authored-By: Claude Sonnet 5 --- .../checklists/requirements.md | 48 +++ specs/007-orchestration-assignment/spec.md | 279 ++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 specs/007-orchestration-assignment/checklists/requirements.md create mode 100644 specs/007-orchestration-assignment/spec.md diff --git a/specs/007-orchestration-assignment/checklists/requirements.md b/specs/007-orchestration-assignment/checklists/requirements.md new file mode 100644 index 0000000..fa7de6d --- /dev/null +++ b/specs/007-orchestration-assignment/checklists/requirements.md @@ -0,0 +1,48 @@ +# Specification Quality Checklist: Orchestration and Assignment + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-09-02 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Scope is Phase 7 per `docs/10-implementation-roadmap.md`: the orchestration engine, pluggable + assignment strategies (including concurrency-safe round robin), and assignment history — + explicitly not SLA policy execution or rule-driven escalation, which doc 05 documents alongside + orchestration but the roadmap places in Phase 8. +- This is the first feature to *consume* 006-support-organization's capability-eligibility + lookup and hierarchy tree as a real caller, rather than that lookup existing only for a + documented future consumer — the dependency the two features' specs both anticipated. +- The trigger mechanism (human escalation → orchestration) reuses 005-ai-support's domain-event + publishing (`DomainEventName.TICKET_UPDATED`) rather than adding a second event/notification + path — the same event bus 005 first put to real use. +- Constitution Principle VII's concurrency-safety requirement is directly testable here (unlike + 004/005/006, which had no genuinely concurrent-writer scenario) — `ROUND_ROBIN` is this + codebase's first real concurrency-under-load test target since 003-ticketing's optimistic + ticket-status concurrency. +- All items pass; no revision iterations were needed. diff --git a/specs/007-orchestration-assignment/spec.md b/specs/007-orchestration-assignment/spec.md new file mode 100644 index 0000000..45acabe --- /dev/null +++ b/specs/007-orchestration-assignment/spec.md @@ -0,0 +1,279 @@ +# Feature Specification: Orchestration and Assignment + +**Feature Branch**: `007-orchestration-assignment` + +**Created**: 2026-09-02 + +**Status**: Draft + +**Input**: User description: "Phase 7 of docs/10-implementation-roadmap.md: Orchestration +engine, capability matching, pluggable assignment strategies, concurrency-safe round robin, +assignment history. Per docs/05-orchestration-sla-escalation.md §1, §3-4, §7 and +docs/06-database-schema.md 'Domain: Assignment'." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - A human-escalated ticket is automatically routed to the right eligible agents (Priority: P1) + +When a ticket reaches human escalation, the system resolves which part of the support +organization should handle it — the applicable hierarchy node for the ticket's product/category/ +priority context — and computes the set of agents who are actually eligible to take it, on +capability grounds. This happens automatically, without an admin having to trigger it by hand. + +**Why this priority**: Every later capability in this feature — picking one agent, recording the +decision — has nothing to act on until the right hierarchy node and eligible-agent set have been +resolved for the ticket in front of it. + +**Independent Test**: Escalate a ticket whose product/category matches a configured hierarchy +node with a required skill; confirm orchestration resolves that node and an eligible-agent set +containing only agents who hold the required skill. Escalate a ticket matching no configured +node; confirm orchestration still runs, falling back to a skill-only match across all active +agents rather than failing. + +**Acceptance Scenarios**: + +1. **Given** a ticket transitions to human escalation, **When** orchestration runs, **Then** it + resolves the ticket's product/category/priority context and finds the hierarchy node(s) whose + scope matches, exactly as the existing capability-eligibility lookup (006) already does for a + direct query. +2. **Given** a resolved hierarchy node with required skills, **When** the eligible-agent set is + computed, **Then** it contains only active agents, on active teams, holding every required + skill — capability before availability, per doc 05 §3. +3. **Given** no hierarchy node matches the ticket's context, **When** orchestration runs, + **Then** it still produces an eligible-agent set (skill-only, across all active agents) rather + than failing outright. +4. **Given** orchestration runs for a ticket, **When** it completes, **Then** it always happens + automatically on human escalation — no admin action is required to trigger it. + +--- + +### User Story 2 - A pluggable assignment strategy picks exactly one eligible agent (Priority: P1) + +Once the eligible-agent set exists, a configurable strategy — the one named on the resolved +hierarchy node — picks exactly one agent from it to assign the ticket to. Different hierarchy +nodes can use different strategies without any code change. `ROUND_ROBIN` cycles fairly through +the eligible set and is safe even when two tickets are being assigned at the exact same moment. + +**Why this priority**: This is the actual routing decision doc 05 exists to make — +capability-only resolution (User Story 1) has no value on its own until one specific agent is +actually chosen. + +**Independent Test**: Configure a node with `ROUND_ROBIN` over three eligible agents; escalate +three tickets in sequence and confirm each goes to a different agent, cycling back to the first +on a fourth. Fire two escalations at the same instant against the same eligible set; confirm +exactly one ticket goes to each of two different agents — never both to the same agent, never a +lost or corrupted cycle position. Configure `LEAST_LOADED` and confirm the agent with the lowest +current workload is chosen among otherwise-eligible agents. + +**Acceptance Scenarios**: + +1. **Given** a hierarchy node configured with `ROUND_ROBIN`, **When** consecutive tickets are + assigned against the same eligible set, **Then** each goes to the next agent in a fair, + repeating cycle. +2. **Given** two assignment attempts against the same eligible set happen concurrently, **When** + both resolve, **Then** the cycle position was never corrupted and no single agent received + both — round robin's cursor is concurrency-safe by construction, not by luck (doc 05 §4). +3. **Given** a hierarchy node configured with `LEAST_LOADED`, **When** an assignment runs, + **Then** the eligible agent with the lowest recorded current workload is chosen; only *now* — + after the eligible set already exists — does workload become part of the decision (doc 05 §3's + capability-before-availability ordering, honored end to end). +4. **Given** a hierarchy node configured with `SKILL_BASED`, **When** an assignment runs, + **Then** the eligible agent with the strongest match on the required skills' proficiency + levels is preferred over one with a merely-present but lower-level match. +5. **Given** the eligible-agent set is empty, **When** an assignment is attempted, **Then** no + agent is assigned, the ticket remains in human escalation, and this outcome is itself recorded + — never a silent no-op indistinguishable from success. + +--- + +### User Story 3 - Every assignment decision is durably recorded (Priority: P2) + +Every time a ticket is assigned or reassigned, a durable record captures which agent, which +strategy chose them, when, and why (including the "no eligible agent" outcome). This history is +never overwritten — reassigning a ticket adds to its history, it doesn't erase what came before. + +**Why this priority**: Depends on User Story 2 producing decisions to record. Without this, +"which agent has this ticket and why" is only ever knowable from the current state, and a dispute +or debugging question about a past decision has no answer. + +**Independent Test**: Assign a ticket, then reassign it (User Story 4). Query its assignment +history and confirm both the original assignment and the reassignment are present, in order, +each with its own actor, strategy, and reason — neither entry overwritten by the other. + +**Acceptance Scenarios**: + +1. **Given** a ticket is assigned, **When** the assignment completes, **Then** a history entry + records the agent, the strategy used, the actor (`system` for automatic orchestration), and a + timestamp. +2. **Given** an already-assigned ticket is reassigned, **When** it completes, **Then** a new + history entry is added — the prior entry remains queryable, never deleted or overwritten. +3. **Given** a ticket's current assignment, **When** it's read, **Then** it always reflects the + most recent assignment decision, while the full history remains separately available. + +--- + +### User Story 4 - A human can manually assign or reassign a ticket (Priority: P2) + +An admin (or, in this phase, whoever can reach the admin surface — see Assumptions) can +explicitly assign a ticket to a named agent, overriding whatever the automatic strategy would +have chosen — including moving a ticket that already has an assignee to someone else, with a +reason. + +**Why this priority**: Automatic routing (User Stories 1-2) won't always be right — an agent +might be unexpectedly unavailable, or a specific ticket might need a specific person regardless +of the configured strategy. Depends on User Story 3 existing so a manual override is recorded the +same way an automatic one is. + +**Independent Test**: Manually assign a ticket to a specific agent; confirm it's reflected as the +current assignment and recorded in history with `strategy: MANUAL` (or `DIRECT`) and the +supplying actor. Reassign the same ticket to a different agent; confirm the history shows both, +in order. + +**Acceptance Scenarios**: + +1. **Given** an unassigned or already-assigned ticket, **When** an admin explicitly assigns it to + a named, existing agent, **Then** that agent becomes the current assignee regardless of what + the automatic strategy would have picked. +2. **Given** a manual assignment targets an agent who doesn't exist, **When** it's attempted, + **Then** it's rejected — a manual assignment can never point at a nonexistent agent. +3. **Given** a manual assignment, **When** it's recorded, **Then** its history entry is + indistinguishable in structure from an automatic one — same fields, same guarantees (User + Story 3) — only the strategy/actor differ. + +--- + +### User Story 5 - A ticket that still can't be resolved is re-escalated and reassigned (Priority: P3) + +When an already-assigned ticket is escalated again (the current agent can't solve it), the same +orchestration flow runs again — a fresh eligible-agent set is computed and a new assignment is +made — rather than the ticket being stuck with an agent who has already said they can't help. + +**Why this priority**: Depends on Users Stories 1-3 already working for a first assignment; this +is the same flow applied a second time, not new mechanics — but it's a distinct, real scenario +(doc 05 §7's re-escalation flow) worth its own explicit guarantee. + +**Independent Test**: Assign a ticket, then trigger human escalation on it again; confirm a new +eligible-agent set is computed fresh (not reused from the first time) and a new assignment is +made, with both assignments present in history. + +**Acceptance Scenarios**: + +1. **Given** a ticket already has a current assignment, **When** it's escalated again, **Then** + orchestration re-runs User Story 1's resolution and User Story 2's strategy selection fresh — + never reusing a stale eligible-agent set from the first time. +2. **Given** re-escalation produces a new assignment, **When** it completes, **Then** the + previous assignment is superseded (no longer the current one) but remains in history. + +--- + +### Edge Cases + +- What happens if the hierarchy node resolved for a ticket has no `assignmentStrategy` an + implementation exists for (e.g., a typo, or a strategy reserved for a future phase)? Treated as + a resolution failure — no agent is assigned, the same "no eligible outcome" recording as an + empty eligible set (User Story 2 Scenario 4), never a crash and never a silent fallback to a + different strategy than the one configured. +- What happens if two hierarchy nodes both match a ticket's context equally well? Out of scope + for tie-breaking logic beyond what 006 already defined (its own `order`/matching behavior) — + this feature resolves however many nodes 006's matching returns and unions their criteria, the + same composition rule 006's capability-eligibility lookup already established. +- What happens if an agent who holds an active assignment is deactivated? Out of scope — this + feature does not react to agent deactivation by auto-reassigning; that's a future + operational-hardening concern, not part of the base orchestration flow. +- What happens when `LEAST_LOADED` or `SKILL_BASED` must break a tie between two equally-good + agents? Falls back to `ROUND_ROBIN`'s own concurrency-safe cycle as the tiebreaker, rather than + an arbitrary/unstable ordering (e.g., insertion order) that could vary between otherwise + identical runs. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The system MUST automatically run orchestration when a ticket transitions to human + escalation — no admin action required to trigger it. +- **FR-002**: Orchestration MUST resolve the hierarchy node(s) matching the ticket's product/ + category/priority context using the same scope-matching rule 006-support-organization's + capability-eligibility lookup already implements — never a second, divergent matching + algorithm. +- **FR-003**: Orchestration MUST compute an eligible-agent set (active agents, on active teams, + holding every required skill from the resolved context) before any assignment strategy runs — + capability is always evaluated before availability (doc 05 §3). +- **FR-004**: When no hierarchy node matches, orchestration MUST still compute a skill-only + eligible-agent set across all active agents rather than failing. +- **FR-005**: The system MUST support at least `ROUND_ROBIN`, `LEAST_LOADED`, `SKILL_BASED`, + `MANUAL`, and `DIRECT` as pluggable assignment strategies, selected by the resolved hierarchy + node's configured `assignmentStrategy` value. +- **FR-006**: `ROUND_ROBIN` MUST be concurrency-safe — two simultaneous assignment attempts + against the same eligible set MUST NEVER both select the same next agent, and MUST NEVER + corrupt or lose the cycle position for future assignments. +- **FR-007**: `LEAST_LOADED` MUST select the eligible agent with the lowest current recorded + workload; `SKILL_BASED` MUST prefer the eligible agent with the strongest proficiency-level + match on the required skills, not merely presence of the tag. +- **FR-008**: When the eligible-agent set is empty, or the resolved strategy has no + implementation, the system MUST NOT assign any agent, MUST leave the ticket in human + escalation, and MUST record that this outcome occurred. +- **FR-009**: Every assignment or reassignment, automatic or manual, MUST produce a durable + history entry recording the agent (if any), the strategy, the actor, a reason (if given), and a + timestamp — history entries are never overwritten or deleted. +- **FR-010**: A ticket's current assignment MUST always reflect its most recent assignment + decision, while every prior decision remains separately queryable as history. +- **FR-011**: The system MUST let an admin manually assign or reassign a ticket to a specific, + existing agent, overriding whatever the automatic strategy would have chosen; targeting a + nonexistent agent MUST be rejected. +- **FR-012**: A manual assignment MUST be recorded through the same history mechanism as an + automatic one (FR-009), distinguished only by its `strategy`/`actor` values. +- **FR-013**: Re-escalating an already-assigned ticket MUST re-run resolution (FR-002/FR-003) and + strategy selection fresh — never reusing a previously computed eligible-agent set — and MUST + produce a new current assignment while preserving the prior one in history. +- **FR-014**: Once an assignment is made (automatic or manual), the ticket MUST transition out of + human escalation into an in-progress, human-owned state, using the ticket lifecycle's existing + state machine (003-ticketing) — this feature does not define a new ticket status. + +### Key Entities + +- **Assignment**: The current, single active assignment record for a ticket — which agent, which + strategy chose them, when. Superseded (not deleted) by a later reassignment. +- **Assignment History**: The complete, append-only record of every assignment decision ever made + for a ticket, including outcomes where no agent was assigned. +- **Assignment Strategy**: A pluggable algorithm that selects one agent from an eligible-agent + set — this feature ships `ROUND_ROBIN`/`LEAST_LOADED`/`SKILL_BASED`/`MANUAL`/`DIRECT`, chosen by + the hierarchy node's own configuration, never hardcoded to a single strategy. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of tickets reaching human escalation have orchestration run against them + automatically, with no manual trigger. +- **SC-002**: Under concurrent assignment attempts against the same eligible set, 100% resolve to + distinct agents (up to the size of the eligible set) with zero double-assignments and zero + corrupted round-robin state, verified under real concurrent load, not just sequential calls. +- **SC-003**: 100% of assignment decisions — automatic or manual, successful or "no eligible + agent" — produce exactly one history entry. +- **SC-004**: A ticket's full assignment history is always retrievable and always shows every + prior assignment, even after multiple reassignments — zero data loss across any number of + reassignments. +- **SC-005**: A manual assignment targeting a nonexistent agent is rejected in 100% of attempts, + never silently creating a dangling assignment. + +## Assumptions + +- **This feature does not build SLA or escalation-policy logic** — doc 05 groups orchestration, + SLA, and escalation into one document, but the roadmap splits them: SLA/escalation-rule + execution is Phase 8. A hierarchy node's `slaPolicyId`/`escalationPolicyId` (006) remain + unvalidated references, exactly as 006 left them. +- **The trigger is the existing `Ticket.status` transition to `HUMAN_ESCALATION`**, reusing + 005-ai-support's domain-event mechanism (a ticket status change already publishes a + `DomainEventName.TICKET_UPDATED` event) rather than adding a second notification path. +- **Round-robin's concurrency safety uses an atomic Redis operation**, per doc 05 §4's explicit + "database-level lock/transaction or an atomic Redis operation, not an in-memory counter" — this + codebase already has Redis wired for exactly this class of atomic-counter need (rate limiting, + replay-guard). +- **"Admin" manual assignment (User Story 4) is gated by the same `fastify.authenticate` stub + every prior feature's admin surface uses** — this feature does not build a distinct + agent-facing identity/permission model; that remains deferred to `identity/auth`, same known + limitation as every feature since 002. +- **Reacting to agent deactivation, workload rebalancing, and SLA-driven re-escalation are out of + scope** — this feature provides the mechanism a later phase's SLA breach handler would call + (re-run orchestration on a ticket), not the trigger logic that decides *when* to call it beyond + the explicit human-escalation and manual-reassignment paths already in scope here.