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 <noreply@anthropic.com>
This commit is contained in:
saqib mir
2026-09-03 11:27:19 +05:30
co-authored by Claude Sonnet 5
parent a0c1a9aa33
commit e437711c2a
2 changed files with 327 additions and 0 deletions
@@ -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.
+279
View File
@@ -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.