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:
co-authored by
Claude Sonnet 5
parent
a0c1a9aa33
commit
e437711c2a
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user