53 tasks across setup, foundational schema/env/LLM-client work, and five user stories (diagnosis+confidence policy, clarification loop, gated tool system, runbook engine, evidence-based verification), plus the two constitution-required standing E2E scenarios in Polish. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
25 KiB
description
| description |
|---|
| Task list for 005-ai-support |
Tasks: AI Support Agent
Input: Design documents from specs/005-ai-support/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/ai-support-contract.md, quickstart.md
Tests: Included as first-class tasks. Unlike 004 (thin Prisma queries with no pure-logic
surface), this feature has several genuinely extractable pure functions (confidence-band
decision, tool policy gate, runbook step-advancement) that get real unit tests, plus integration
tests that — for the first time in this codebase — depend on a real external network call (a real
ANTHROPIC_API_KEY) and incur real per-run API cost, not just Docker-local infra. The
constitution's Testing gate also requires this feature to add: AI tool-permission tests, and the
two standing E2E scenarios (AI resolves directly / AI escalates to human) that were impossible
before this feature existed.
Organization: Tasks are grouped by user story (US1 = P1 diagnosis/confidence-policy, US2 = P2 clarification loop, US3 = P2 tool system, US4 = P3 runbook engine, US5 = P3 verification/ resolution).
Format: [ID] [P?] [Story] Description
All file paths are relative to supporthub-api/ (repo root).
Phase 1: Setup
- T001 [P] Scaffold
src/modules/ai-support/sessions/andsrc/modules/ai-support/tools/with the standard module shape (controller/,routes/,schema/,repository/,service/,types/,mapper/,constants/,index.ts) - T002 [P] Scaffold
src/modules/ai-support/troubleshooting/andsrc/modules/ai-support/escalation/with the reduced shape plan.md specifies for internal-only submodules (service/,types/,index.ts— noroutes/controller/schema, since neither has its own HTTP surface) - T003 [P] Scaffold
src/infrastructure/ai/(Anthropic client singleton) andsrc/jobs/ai-session/(empty worker module, populated in Phase 3) - T004 Add
@anthropic-ai/sdkas a runtime dependency (npm install @anthropic-ai/sdk)
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Schema, env config, and the LLM client every user story depends on.
⚠️ CRITICAL: No user-story stage work can begin until this phase is complete.
- T005 Add
AISupportSession,AIDiagnosis,AIInteraction,AIAction,AIActionResult,AIKnowledgeReference,AIConfidencePolicymodels toprisma/schema.prismaper data-model.md, plus theTicket.aiSessionsback-relation (depends on T001-T003) - T006 Run
npm run prisma:generateand create the migration (npm run prisma:migrate) for T005 (depends on T005) - T007 Add env vars to
src/config/env.ts:ANTHROPIC_API_KEY(z.string().optional()— the app must still boot and every non-AI test must still pass without it; the Anthropic client wrapper (T008) is what throws a clear, explicit error if a reasoning call is attempted with it unset — spec.md Assumptions' "no offline fallback path" is enforced at the point of use, not by making every test fixture supply a fake credential),AI_SUPPORT_MODEL(defaultclaude-opus-5),AI_SUPPORT_EFFORT(defaultmedium),AI_SUPPORT_DEFAULT_HIGH_CONFIDENCE(default0.75),AI_SUPPORT_DEFAULT_LOW_CONFIDENCE(default0.4),AI_SUPPORT_DEFAULT_MAX_CLARIFYING_QUESTIONS(default2),AI_SUPPORT_MAX_REASONING_ITERATIONS_PER_TURN(default4— doc 11 §B2's runaway-loop cap on tool-call iterations within one reasoning turn). Mirror the new vars into.env.example(withANTHROPIC_API_KEY=CHANGE_MEand a comment that a real key is required for this feature to function) —.env.development/.env.testare gitignored, so note in the task (not the repo) that the user must add a real key there themselves (depends on T004) - T008 Add the Anthropic client singleton in
src/infrastructure/ai/anthropic.client.ts— constructsnew Anthropic()(credential resolved fromANTHROPIC_API_KEYper the SDK's own env resolution), exports the configuredmodel/effortfrom env, and a guard that throws a clearAppErrorif a reasoning call is attempted with no key configured (depends on T007)
Checkpoint: Schema migrated, env validated, LLM client ready. User stories can now be built.
Phase 3: User Story 1 - The AI diagnoses a new ticket and confidence decides what happens next (Priority: P1) 🎯 MVP
Goal: A ticket's first AI turn — diagnose (real, structured, knowledge-grounded LLM call),
apply the confidence-band policy, and reach one of proceed/ask/escalate, with Ticket.status
correctly reflecting the outcome through the existing state machine.
Independent Test: Quickstart Scenario 1.
Tests for User Story 1
- T009 [P] [US1] Unit tests for the confidence-band decision function — proceed/ask/escalate
boundary values, most-specific-
(productId, categoryId)-match-with-fallback resolution — intests/unit/ai-support/confidence-band.test.ts - T010 [US1] Integration test covering Quickstart Scenario 1 (diagnosis recorded with
confidence; low
highThresholdstill proceeds; highlowThresholdescalates; no-knowledge product escalates) against a real Postgres and a real Anthropic API call intests/integration/ai-diagnosis.test.ts(depends on T006, T008; requires a realANTHROPIC_API_KEYin the test environment to run)
Implementation for User Story 1
- T011 [US1] Add
AIConfidencePolicyRepository/AIConfidencePolicyService(most-specific-match lookup:(productId, categoryId)→(productId, null)→ env defaults; upsert) insrc/modules/ai-support/sessions/repository/confidence-policy.repository.ts+service/confidence-policy.service.ts(depends on T006) - T012 [P] [US1] Add the pure confidence-band decision function
decideConfidenceBand(confidence, policy) => 'proceed' | 'ask' | 'escalate'insrc/modules/ai-support/sessions/service/confidence-band.ts(no dependencies — pure function, can be written and unit-tested in parallel with T011) - T013 [US1] Add
AISupportSessionRepository(create; findActiveByTicketId — enforces FR-001's one-active-session rule; update status/runbook fields/counters) insrc/modules/ai-support/sessions/repository/session.repository.ts(depends on T006) - T014 [US1] Add
AIDiagnosisRepository(create; findLatestBySession) insrc/modules/ai-support/sessions/repository/diagnosis.repository.ts(depends on T006) - T015 [US1] Add the diagnosis LLM call —
zodOutputFormatschema matching data-model.md'sAIDiagnosisshape,client.messages.parse()against the ticket's problem statement + conversation-so-far, no tools — insrc/modules/ai-support/sessions/service/diagnose.ts(depends on T008) - T016 [US1] Add
EscalationService.escalate(sessionId, reason)— builds the structured summary (problem, diagnosis, steps attempted so far, confidence — FR-021), callsticketsService.updateStatus(ticketId, 'HUMAN_ESCALATION', expectedVersion, 'ai')(003, reused per research.md), ends the session (status: escalated) — insrc/modules/ai-support/escalation/service/escalation.service.ts(depends on T013; this is needed by US1 itself, since "escalate" is one of US1's three outcomes — not deferred to a later story) - T017 [US1] Add
SessionsService.runFirstTurn(ticketId): create the session (status: analyzing, mirrored ontoTicket.status: AI_ANALYZINGviaticketsService.updateStatus(..., 'ai')), run T015's diagnosis call, callknowledgeService.retrieve(...)(004, throughai-support/knowledge'sindex.ts— research.md) and recordAIKnowledgeReferencerows for what was actually retrieved, escalate via T016 if no knowledge exists (FR-006) or apply T011/T012's confidence-band decision otherwise:escalate→ T016;ask→ produce a clarifying question (plainclient.messages.create()call framing the diagnosis + retrieved knowledge, no tools yet) and post it viamessagesService.post(ticketId, 'ai', 'AI_MESSAGE', ...)(003, reused);proceed→ transition tostatus: troubleshooting(Ticket.status: AI_TROUBLESHOOTING) — full tool/runbook wiring for the proceed branch lands in US3/US4, so for this storyproceedonly needs to reach the correct status, not yet call any tool — insrc/modules/ai-support/sessions/service/session.service.ts(depends on T011, T012, T013, T014, T015, T016) - T018 [US1] Add Zod schema +
PUT/GET /admin/products/:externalProductId/ai-policyroutes (gated byfastify.authenticate, per contracts/ai-support-contract.md) insrc/modules/ai-support/sessions/schema/+routes/, registered fromsrc/api/routes.ts(depends on T011) - T019 [US1] Add the
AI_SESSIONworker —registerAiSessionWorker()insrc/jobs/ai-session/index.ts, callingsessionsService.runFirstTurn(ticketId)— and register it insrc/bootstrap/queue.bootstrap.tsalongside the existing attachment worker (depends on T017) - T020 [US1] Modify
TicketsService.createFromInboundRequest(src/modules/ticketing/tickets/service/tickets.service.ts) to enqueue aQueueName.AI_SESSIONjob ({ ticketId: ticket.id }) whenwasExistingisfalse(research.md "Session triggering" — never on an idempotent replay) (depends on T019) - T021 [US1] Modify
TicketsService.updateStatusto end any activeAISupportSessionfor the ticket (status: ended_by_agent) when called with an actor other than'ai'— FR-023's concrete mechanism (research.md) — before the status update itself commits (depends on T013) - T022 [US1] Run Quickstart Scenario 1 locally (with a real
ANTHROPIC_API_KEY) and confirm all 5 steps pass
Checkpoint: Every new ticket gets a real, knowledge-grounded diagnosis, and confidence
correctly decides proceed/ask/escalate, with Ticket.status reflecting it. This alone is a
usable automatic-triage surface even before clarification, tools, runbooks, or verification exist.
Phase 4: User Story 2 - The AI asks a clarifying question and re-diagnoses from the answer (Priority: P2)
Goal: The "ask" branch becomes a real back-and-forth instead of a dead end — a customer reply triggers a new diagnosis, with the confidence-band policy re-applied and the question-count cap enforced.
Independent Test: Quickstart Scenario 2.
Tests for User Story 2
- T023 [US2] Integration test covering Quickstart Scenario 2 (question posted as
customer-visible
AI_MESSAGE; reply triggers a secondAIDiagnosis; policy re-applied; question cap reached → escalate) against a real Postgres and a real Anthropic API call intests/integration/ai-clarification.test.ts(depends on T022)
Implementation for User Story 2
- T024 [US2] Add Zod schema +
POST /tickets/:ticketId/ai-session/messagesroute (404if no active session — contracts/ai-support-contract.md guarantee 1) insrc/modules/ai-support/sessions/schema/+routes/+controller/(depends on T017) - T025 [US2] Add
SessionsService.handleCustomerReply(ticketId, message): record the reply as anAIInteraction(role: customer) and aTicketMessage(type: CUSTOMER_MESSAGE, via the existingmessagesService), re-run T015's diagnosis call with the full conversation, incrementclarifyingQuestionsAskedwhen continuing from anaskoutcome, escalate via T016 onceclarifyingQuestionsAsked >= policy.maxClarifyingQuestions(FR-009) regardless of the new diagnosis's own confidence, otherwise re-apply the confidence-band decision as in T017 — insrc/modules/ai-support/sessions/service/session.service.ts(depends on T024) - T026 [US2] Add Zod schema +
GET /tickets/:ticketId/ai-sessionread route (status, latest diagnosis, interaction history) insessions/schema/+routes/+controller/(depends on T013, T014) - T027 [US2] Run Quickstart Scenario 2 locally and confirm it passes
Checkpoint: US1 and US2 together deliver a full diagnose → clarify → re-diagnose loop with a correctly enforced question budget.
Phase 5: User Story 3 - The AI proposes tool calls; the application decides whether to run them (Priority: P2)
Goal: The proceed branch actually does something — the AI can request real, permission/
risk-gated tool calls, evaluated by one shared deterministic gate the AI's own text can never
influence.
Independent Test: Quickstart Scenario 3. This story also satisfies the constitution's required "AI tool-permission tests" category (Testing, Observability & CI/CD Gates).
Tests for User Story 3
- T028 [P] [US3] Unit tests for
evaluateToolProposal— unknown tool refused, product not insupportedProductsrefused, low-risk auto-approved, medium/high-riskpending_approval, and an explicit case asserting the AI's own proposal/justification text is never read by the gate (prompt-injection resistance, FR-024) — intests/unit/ai-support/tool-policy-gate.test.ts - T029 [US3] Integration test covering Quickstart Scenario 3 (low-risk tool executes and is
recorded;
overrideTicketPriorityproposal ispending_approvalwith no result; an out-of-scope tool proposal isrefused) against a real Postgres and a real Anthropic API call intests/integration/ai-tool-actions.test.ts(depends on T022)
Implementation for User Story 3
- T030 [P] [US3] Add the tool registry —
getTicketSnapshot(low),searchProductKnowledge(low),verifyProductResolution(low, fail-closed placeholder — research.md, mirrorsUnimplementedPlaceholderScanner),escalateToHuman(low, always policy-approved),overrideTicketPriority(high) — with Zod input schemas,permission,riskLevel,supportedProducts,auditRequiredper tool — insrc/modules/ai-support/tools/constants/tool-registry.ts(depends on T003) - T031 [US3] Add
evaluateToolProposal(toolName, sessionContext)— the shared deterministic gate (research.md): unknown-tool / out-of-scope →refused;low→approved;medium/high→pending_approval. Reads only the tool name and session's product/ permission context, never the AI's proposal text — insrc/modules/ai-support/tools/service/policy-gate.ts(depends on T030) - T032 [US3] Add
AIActionRepository/AIActionResultRepository(create action + evaluation outcome; create result when executed; list by session, newest first) insrc/modules/ai-support/tools/repository/(depends on T006) - T033 [US3] Add real tool execution handlers —
getTicketSnapshot(readsTicket+Problem+recentTicketMessages via existing repositories),searchProductKnowledge(callsknowledgeService.retrieve(...)),verifyProductResolution(always returns{ confirmed: false, status: 'unknown' }— documented placeholder),escalateToHuman(calls T016'sEscalationService.escalate) — insrc/modules/ai-support/tools/service/tool-executor.ts(depends on T031;overrideTicketPriorityhas no execution handler yet — it can never reachapproved, so it's never called) - T034 [US3] Add
ToolsService.proposeAndEvaluate(sessionId, toolUseBlocks): for each proposedtool_useblock, run T031's gate, persist theAIAction(T032), execute + persist anAIActionResult(T032) only whenapproved, and increment the failure count toward escalation triggers on a failed result (FR-014) — insrc/modules/ai-support/tools/service/tools.service.ts(depends on T032, T033) - T035 [US3] Wire the reasoning/response call into
SessionsService'sproceedbranch (research.md "two calls per reasoning turn," step 3): aclient.messages.create()call with the tool registry'sAnthropic.Tool[]definitions, the diagnosis + retrieved knowledge + conversation as context; route anytool_useblocks through T034, feedtool_resultblocks back for a follow-up call, capped atAI_SUPPORT_MAX_REASONING_ITERATIONS_PER_TURNiterations (doc 11 §B2) — insrc/modules/ai-support/sessions/service/session.service.ts(depends on T034) - T036 [US3] Add Zod schema +
GET /tickets/:ticketId/ai-session/actionsroute insessions/schema/+routes/+controller/(ortools/— whichever module owns the route registers it; the data comes from T032's repository either way) (depends on T032) - T037 [US3] Run Quickstart Scenario 3 locally and confirm it passes
Checkpoint: US1-US3 together deliver diagnose → clarify → act-through-gated-tools, with every proposal, decision, and result durably recorded and auditable.
Phase 6: User Story 4 - A matching runbook drives guided troubleshooting, not the AI's own improvisation (Priority: P3)
Goal: When a diagnosis matches a runbook, the application — not the model — controls which step is next.
Independent Test: Quickstart Scenario 4.
Tests for User Story 4
- T038 [P] [US4] Unit tests for the pure runbook step-advancement function — advances by
exactly one on a "try next step" outcome, reports exhaustion after the last step, never
skips or resets — in
tests/unit/ai-support/runbook-step-advance.test.ts - T039 [US4] Integration test covering Quickstart Scenario 4 (matching runbook sets
activeRunbookKey/currentStepIndex: 0; advances exactly one step per turn; exhaustion escalates with every attempted step listed) against a real Postgres and a real Anthropic API call intests/integration/ai-runbook-troubleshooting.test.ts(depends on T022)
Implementation for User Story 4
- T040 [P] [US4] Add the pure step-advancement function
advanceRunbookStep(steps, currentStepIndex, outcome) => { nextIndex } | { exhausted: true }insrc/modules/ai-support/troubleshooting/service/step-advance.ts(no dependencies) - T041 [US4] Add
RunbookEngineService.matchRunbook(problemType, productId)— callsrunbooksService.findCurrentByKey(...)(004, throughai-support/knowledge'sindex.ts) — insrc/modules/ai-support/troubleshooting/service/runbook-engine.service.ts(depends on T003) - T042 [US4] Wire T041/T040 into
SessionsService: after aproceeddiagnosis, attempt T041's match; if found, setactiveRunbookKey/currentStepIndex: 0on the session; the reasoning call (T035) receives onlysteps[currentStepIndex]in its prompt context, never the full list; a customer's step-outcome reply advances via T040, and exhaustion ({ exhausted: true }) escalates via T016 with every attempted step in the summary (FR-016) — insrc/modules/ai-support/sessions/service/session.service.ts(depends on T041) - T043 [US4] Run Quickstart Scenario 4 locally and confirm it passes
Checkpoint: When a runbook matches, troubleshooting follows its authored order exactly — the model presents and interprets, the application sequences.
Phase 7: User Story 5 - A ticket is only marked AI-resolved when there's real evidence, not a customer's claim alone (Priority: P3)
Goal: The resolved/AI_RESOLVED transition is guarded on real tool evidence, never on
message content.
Independent Test: Quickstart Scenario 5. This story is also required to complete the constitution's standing "(A) AI resolves directly" E2E scenario (Phase 8 wires the test itself, since it depends on every prior story existing).
Tests for User Story 5
- T044 [US5] Integration test covering Quickstart Scenario 5 (customer claims fixed with no
tool evidence → not resolved;
verifyProductResolution's placeholder never confirms → session doesn't auto-resolve) against a real Postgres and a real Anthropic API call intests/integration/ai-verification.test.ts(depends on T022)
Implementation for User Story 5
- T045 [US5] Add the resolution guard in
SessionsService: a session only transitionstroubleshooting/verifying → resolved(mirrored toTicket.status: AI_VERIFYING → AI_RESOLVED) when anAIActionResultfromverifyProductResolutionwithconfirmed: trueexists for the session (T032's repository) — a customer's "it's fixed" reply is recorded as anAIInteractiononly and never inspected by this guard (FR-018/ FR-019) — insrc/modules/ai-support/sessions/service/session.service.ts(depends on T033) - T046 [US5] Run Quickstart Scenario 5 locally and confirm it passes
Checkpoint: All five user stories work independently and together — the full diagnose → clarify → act → troubleshoot → verify flow, with policy (never the model) deciding every MUST-level outcome.
Phase 8: Polish & Cross-Cutting Concerns
- T047 [P] Integration test for the prompt-injection edge case (quickstart.md — a customer
reply containing an instruction-like string never changes
evaluationOutcomefor a subsequent high-risk proposal) intests/integration/ai-tool-actions.test.ts(extends T029's file) or a newtests/integration/ai-prompt-injection.test.ts(depends on T037) - T048 The constitution's standing E2E scenario (A) — "AI resolves directly: problem →
knowledge → guided troubleshooting → verification → AI-resolved" — as a single, real,
end-to-end integration test spanning US1/US3/US4/US5 in one session in
tests/integration/e2e-ai-resolves.test.ts(depends on T022, T037, T043, T046) - T049 The constitution's standing E2E scenario (B) — "AI escalates to human: problem →
failed AI troubleshooting → escalation → [ticket reaches HUMAN_ESCALATION, ready for
orchestration/assignment when that phase exists]" — in
tests/integration/e2e-ai-escalates.test.ts(depends on T022, T037, T043) - T050 [P] Add an "AI Support" section to
README.mddescribing the session lifecycle, the confidence-policy config surface, the tool registry (including the two documented known limitations:verifyProductResolution's fail-closed placeholder andoverrideTicketPriority's permanently-pending_approvalstate pending a future approval UI), and the runbook engine - T051 [P] Update
specs/005-ai-support/checklists/requirements.mdNotes with any implementation-time findings - T052 Run
npx tsx scripts/check-architecture.tsandnpm run lint/npm run typecheck - T053 Full regression:
npm run test:unit(scoped totests/unit) to confirm nothing broke elsewhere
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 (extends
SessionsService, reuses T015-T017) — genuinely not independent of US1's diagnosis/session machinery, unlike 004's stories - User Story 3 (Phase 5): Depends on US1 (the
proceedbranch it fills in) — independent of US2's clarification loop - User Story 4 (Phase 6): Depends on US1 (the
proceedbranch) and benefits from, but doesn't strictly require, US3 (a runbook step could in principle need a tool call — not required by any FR here, so US4 doesn't block on US3 completing) - User Story 5 (Phase 7): Depends on US3 (needs
verifyProductResolution's execution handler, T033) and US1's session status machinery - Polish (Phase 8): Depends on all five user stories
Parallel Opportunities
- T001/T002/T003 (independent scaffolding)
- T009 alongside T011-T016 once T006 exists (unit test doesn't need the real implementation)
- T012 (pure function) can be written independently of T011
- T028 alongside T030-T034
- T030 (registry) alongside T031's early drafting, though T031 needs T030's exports to compile
- T038 (pure function) independent of T041
- T047/T050/T051 in Polish
Implementation Strategy
MVP First (User Story 1 Only)
- Setup + Foundational (T001-T008)
- User Story 1 (T009-T022)
- STOP and VALIDATE: Quickstart Scenario 1 passes — every new ticket gets a real,
knowledge-grounded diagnosis, and confidence correctly decides proceed/ask/escalate, with
Ticket.statusreflecting it end to end. Usable automatic-triage value even before clarification, tools, runbooks, or verification exist.
Incremental Delivery
- Setup + Foundational → schema migrated, LLM client ready
- Add User Story 1 → every ticket gets a real AI diagnosis (MVP)
- Add User Story 2 → the "ask" branch becomes a real conversation
- Add User Story 3 → the "proceed" branch can act, through a gate the AI can't talk its way past
- Add User Story 4 → matched problems get consistent, product-approved troubleshooting sequences
- Add User Story 5 → resolution requires real evidence, closing the loop safely
- Polish → the two constitution-required standing E2E scenarios, docs, full regression