Real Anthropic Claude integration per explicit product decision: a ticket's AI session diagnoses the problem via a structured-output call, applies a DB-configurable confidence-band policy (FR-005), and on "proceed" reasons and acts through a small permission/risk-gated tool system (FR-011/FR-012), optionally walking a matching runbook step by step with the application — never the model — owning the step index (FR-015/FR-016). Resolution requires real tool evidence, never customer claims alone (FR-018) — verifyProductResolution is a documented fail-closed placeholder mirroring the existing malware-scanner precedent, since no real per-product operational signal exists yet. AISupportSession.status mirrors onto Ticket.status through 003-ticketing's existing AI_ANALYZING/AI_TROUBLESHOOTING/AI_VERIFYING/AI_RESOLVED/ HUMAN_ESCALATION state machine, discovered during planning to have been built anticipating this exact feature. Two circular module dependencies (escalation<->sessions, tools<->sessions) were designed around rather than found as bugs: escalation is a pure summary formatter with no state dependencies of its own, and tools stays a clean leaf module with zero dependency on ai-support/sessions. Ticket creation enqueues the first diagnosis turn via the existing queue infrastructure (off the hot path of the inbound SaaS integration endpoint); a human actor changing ticket status ends the AI session via the event-bus scaffold that existed in this codebase but had never been wired to anything. A real Prisma limitation was found and fixed before it reached tests: compound-unique upsert rejects null for a nullable key column, so AIConfidencePolicy uses find-then-update/create instead, same fix class 004 already used for the same underlying limitation. Adds 9 unit tests (confidence-band, tool-policy-gate, runbook-step- advance) and 6 integration test files, including the two constitution- required standing E2E scenarios. AI-independent tests were run against real Postgres/Redis/MinIO (88 passed, 0 failed across the full suite, including every pre-existing 002/003/004 test). The AI-dependent tests compile and skip cleanly via describe.skipIf but were not run against a live model — no ANTHROPIC_API_KEY was available in this session; a real key must be supplied before this feature can actually run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.9 KiB
Development
docker compose --env-file .env.development -f docker-compose.development.yml up -d --build
Test
docker compose --env-file .env.test -f docker-compose.test.yml up --build
Production
docker compose --env-file .env.prod -f docker-compose.prod.yml up --build -d
Stop
docker compose -f docker-compose.prod.yml down
Local environment setup
.env.development, .env.test, and .env.prod are gitignored (they hold real credentials) —
copy .env.example to the one you need and fill in real values before running any command above.
CI/CD
Every push/PR triggers the Jenkins pipeline defined in Jenkinsfile. Stage order:
checkout → install → environment validation → typecheck → lint → format check → unit test →
integration test → E2E test → build → Docker build → publish → deploy. Publish/deploy only run
on branches with a configured deploy target (main → prod, develop/test → test); other
branches validate and build only. Pipeline run status and per-stage logs are visible in the
Jenkins UI for the relevant job — see specs/001-ci-pipeline/quickstart.md for how to validate
the pipeline itself, and specs/001-ci-pipeline/contracts/pipeline-stage-contract.md for the
guarantees each stage makes.
Required Jenkins credentials (see the header comment in Jenkinsfile for exact IDs): per target
environment (test, prod) a Postgres password, Redis password, JWT secret, and AWS access
key/secret, plus one shared Docker registry username/password. None of these are ever read from
a file in this repository.
SaaS Integration
POST /v1/support/requests is the trust boundary a registered SaaS product calls through — every
request must carry a Authorization: Bearer <signed-token> header (HMAC-SHA256, signed with the
integration's own secret) and a body matching the inbound contract. See
specs/002-saas-integration/contracts/inbound-request-contract.md for the full validation order
and error codes, and specs/002-saas-integration/quickstart.md for runnable scenarios.
Admins manage integrations under /admin/products/:externalProductId/integration (register) and
/admin/integrations/:integrationId/{rotate,revoke,status,audit-trail}. These admin routes are
not yet actually access-controlled — fastify.authenticate is a stub pending the identity/auth
module; don't expose them outside a trusted network until that's implemented.
Rate limits (rateLimitPerMinute, rateLimitPerUserPerMinute) are set per integration at
registration time and enforced via a Redis-backed fixed-window counter, independent of the
global @fastify/rate-limit floor already applied to every route.
Ticketing
A validated inbound request (see "SaaS Integration" above) creates a Ticket and Problem
immediately — before any diagnosis. See specs/003-ticketing/contracts/ticket-lifecycle-contract.md
for the full lifecycle state machine, message-visibility rules, and attachment pipeline, and
specs/003-ticketing/quickstart.md for runnable scenarios.
- Status transitions:
PATCH /tickets/:ticketId/statusrequiresexpectedVersion(optimistic concurrency — a stale version is rejected with409, never silently overwritten) and only accepts transitions defined in the state machine (400 INVALID_TRANSITIONotherwise). - Messages:
POST/GET /tickets/:ticketId/messages(customer-scoped — internal note types are never returned) andGET /agent/tickets/:ticketId/messages(agent-scoped — everything). A message's customer-visibility is always derived from its type, never caller-supplied. - Attachments: presigned-PUT upload (
POST .../attachments/upload-url→POST .../attachments/confirm) against MinIO/S3 — file bytes never transit this API. Nothing is downloadable yet (GET .../attachments/:attachmentId/download-urlalways returns409): the malware scanner is a placeholder that fails closed until a real one (src/modules/ticketing/attachments/mapper/malware-scanner.ts) replaces it. - Local/test object storage is MinIO — see the
minioservice indocker-compose.development.yml/docker-compose.test.ymland theAWS_S3_ENDPOINTvalue in the corresponding.env.*file.
Product Knowledge
Admin CRUD for KnowledgeEntry/ErrorCode/KnownIssue/Runbook, plus GET /knowledge/retrieve
— a filtered (not semantic/vector) query the future AI-support feature will call. See
specs/004-product-knowledge/contracts/knowledge-contract.md for the full route list and
specs/004-product-knowledge/quickstart.md for runnable scenarios.
- Versioning: editing a published
KnowledgeEntryorRunbooknever overwrites it in place — it creates a new row (versionincremented,isCurrentVersion: true), and the prior version stays queryable (GET /admin/knowledge/:code/versions). RequiresexpectedVersion; a stale value is rejected with409, same concurrency pattern as ticket status updates. - Retrieval (
GET /knowledge/retrieve?productId=&feature=&category=) only ever returnspublished, currently-effective, current-version entries scoped to the given product —validatedentries are ranked ahead ofunvalidatedones. An unregisteredproductIdreturns an empty array, not an error. - Full semantic/embedding-based retrieval is intentionally not implemented here — see
specs/004-product-knowledge/spec.mdAssumptions.
AI Support
A ticket's AI session diagnoses the problem via a real Anthropic Claude call, applies a
configurable confidence-band policy, and — on "proceed" — reasons and acts through a small,
permission/risk-gated tool system, optionally walking a matching runbook step by step. See
specs/005-ai-support/contracts/ai-support-contract.md for the full route list and
specs/005-ai-support/quickstart.md for runnable scenarios.
- Requires a real
ANTHROPIC_API_KEY(per explicit product decision — not a mock or pluggable-interface phase). The app boots and every non-AI test still passes without one; an AI session simply fails closed (escalates) if a reasoning call is attempted with none configured.AI_SUPPORT_MODEL/AI_SUPPORT_EFFORTand the system-wide confidence-threshold/question-budget defaults are all environment-configurable, never hardcoded (doc 11 §B2). - Session lifecycle: a session starts automatically (queued, off the hot path of
POST /v1/support/requests) when a ticket is created, and its status mirrors ontoTicket.statusthrough 003-ticketing's existing state machine (AI_ANALYZING→AI_TROUBLESHOOTING→AI_VERIFYING→AI_RESOLVED, orHUMAN_ESCALATIONfrom any point) — seespecs/005-ai-support/research.md"AISupportSession.status drives Ticket.status". A customer reply isPOST /tickets/:ticketId/ai-session/messages; the current session (with its diagnosis and conversation) isGET /tickets/:ticketId/ai-session. - Confidence policy:
PUT/GET /admin/products/:externalProductId/ai-policysets per-product (optionally per-category)highThreshold/lowThreshold/maxClarifyingQuestions, falling back to env-configured system defaults when nothing is configured — applies to the very next diagnosis, no deploy required. - Tool system: every tool call the AI proposes is evaluated by a deterministic policy gate
(
src/modules/ai-support/tools/service/policy-gate.ts) before anything executes — the gate never reads the AI's own proposal/justification text, only the tool's declared risk level and product scope. Low-risk tools (getTicketSnapshot,searchProductKnowledge,verifyProductResolution,escalateToHuman) auto-execute;overrideTicketPriorityis high-risk and always stayspending_approval— there's no human-approval UI yet (Phase 10), so it never actually runs, by design, not by oversight. Every proposal, decision, and result is recorded and auditable viaGET /tickets/:ticketId/ai-session/actions. verifyProductResolutionis a documented, fail-closed placeholder (same pattern asticketing/attachments's malware scanner) — it always returnsconfirmed: false, since there's no real per-product operational signal to check yet (doc 11 §A2). A ticket is only ever markedAI_RESOLVEDon a passing result from this tool, never from what the customer says alone — so in practice, genuinely automatic AI resolution won't happen until a real verification signal replaces this placeholder.- Runbook engine: when a diagnosis's
problemTypematches a runbook'skeyfor the product (004-product-knowledge), the application — never the model — tracks which step is current (currentStepIndex) and advances it by exactly one at a time; exhausting every step without resolving escalates with the full attempted sequence attached. - Semantic/vector retrieval, product-signal webhook verification, model routing/fallback, cost
dashboards, and localization are intentionally out of scope here — see
specs/005-ai-support/spec.mdAssumptions.