2026-08-19 16:29:17 +05:30
|
|
|
# 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
|
|
|
|
|
|
2026-08-21 17:25:46 +05:30
|
|
|
# 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.
|
2026-09-02 14:42:20 +05:30
|
|
|
|
|
|
|
|
# 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.
|
2026-09-02 15:21:37 +05:30
|
|
|
|
|
|
|
|
# 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/status` requires `expectedVersion` (optimistic
|
|
|
|
|
concurrency — a stale version is rejected with `409`, never silently overwritten) and only
|
|
|
|
|
accepts transitions defined in the state machine (`400 INVALID_TRANSITION` otherwise).
|
|
|
|
|
- **Messages**: `POST/GET /tickets/:ticketId/messages` (customer-scoped — internal note types are
|
|
|
|
|
never returned) and `GET /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-url` always returns `409`):
|
|
|
|
|
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 `minio` service in `docker-compose.development.yml`
|
|
|
|
|
/ `docker-compose.test.yml` and the `AWS_S3_ENDPOINT` value in the corresponding `.env.*` file.
|
2026-09-02 15:55:18 +05:30
|
|
|
|
|
|
|
|
# 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 `KnowledgeEntry` or `Runbook` never overwrites it in
|
|
|
|
|
place — it creates a new row (`version` incremented, `isCurrentVersion: true`), and the prior
|
|
|
|
|
version stays queryable (`GET /admin/knowledge/:code/versions`). Requires `expectedVersion`;
|
|
|
|
|
a stale value is rejected with `409`, same concurrency pattern as ticket status updates.
|
|
|
|
|
- **Retrieval** (`GET /knowledge/retrieve?productId=&feature=&category=`) only ever returns
|
|
|
|
|
`published`, currently-effective, current-version entries scoped to the given product —
|
|
|
|
|
`validated` entries are ranked ahead of `unvalidated` ones. An unregistered `productId` returns
|
|
|
|
|
an empty array, not an error.
|
|
|
|
|
- Full semantic/embedding-based retrieval is intentionally not implemented here — see
|
|
|
|
|
`specs/004-product-knowledge/spec.md` Assumptions.
|
2026-09-02 17:44:52 +05:30
|
|
|
|
|
|
|
|
# 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_EFFORT` and 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 onto
|
|
|
|
|
`Ticket.status` through 003-ticketing's *existing* state machine (`AI_ANALYZING` →
|
|
|
|
|
`AI_TROUBLESHOOTING` → `AI_VERIFYING` → `AI_RESOLVED`, or `HUMAN_ESCALATION` from any point) —
|
|
|
|
|
see `specs/005-ai-support/research.md` "AISupportSession.status drives Ticket.status". A
|
|
|
|
|
customer reply is `POST /tickets/:ticketId/ai-session/messages`; the current session (with its
|
|
|
|
|
diagnosis and conversation) is `GET /tickets/:ticketId/ai-session`.
|
|
|
|
|
- **Confidence policy**: `PUT/GET /admin/products/:externalProductId/ai-policy` sets
|
|
|
|
|
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; `overrideTicketPriority` is
|
|
|
|
|
high-risk and **always** stays `pending_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 via `GET /tickets/:ticketId/ai-session/actions`.
|
|
|
|
|
- **`verifyProductResolution` is a documented, fail-closed placeholder** (same pattern as
|
|
|
|
|
`ticketing/attachments`'s malware scanner) — it always returns `confirmed: false`, since there's
|
|
|
|
|
no real per-product operational signal to check yet (doc 11 §A2). A ticket is only ever marked
|
|
|
|
|
`AI_RESOLVED` on 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 `problemType` matches a runbook's `key` for 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.md` Assumptions.
|
2026-09-02 18:12:26 +05:30
|
|
|
|
|
|
|
|
# Support Organization
|
|
|
|
|
Admin CRUD for `Team`/`Agent`/`AgentSkill`/`AgentAvailability`, plus a fully dynamic
|
|
|
|
|
`HierarchyNode` tree — this system's explicit routing differentiator (doc 01: "changing one must
|
|
|
|
|
never require a code change"). See
|
|
|
|
|
`specs/006-support-organization/contracts/support-org-contract.md` for the full route list and
|
|
|
|
|
`specs/006-support-organization/quickstart.md` for runnable scenarios.
|
|
|
|
|
|
|
|
|
|
- **Teams and agents**: `POST/PATCH/GET /admin/teams`, `POST/PATCH/GET /admin/agents` —
|
|
|
|
|
active/inactive only, never hard deletion. Deactivating a team never cascades into deactivating
|
|
|
|
|
its agents.
|
|
|
|
|
- **Skills and availability**: `PUT /admin/agents/:agentId/skills/:skillTag` upserts a
|
|
|
|
|
proficiency level (never duplicates the same tag). `PUT /admin/agents/:agentId/availability`
|
|
|
|
|
upserts the agent's single current record — status/working hours/current load — using
|
|
|
|
|
**last-write-wins**, not this system's usual `expectedVersion` optimistic-concurrency pattern:
|
|
|
|
|
availability is frequently-changing operational telemetry, not a durable business record (see
|
|
|
|
|
`specs/006-support-organization/research.md`).
|
|
|
|
|
- **The dynamic hierarchy**: `POST/PUT /admin/hierarchy-nodes` build an arbitrary, nestable tree
|
|
|
|
|
scoped to product/category/priority, each node carrying an `assignmentStrategy` and
|
|
|
|
|
`slaPolicyId`/`escalationPolicyId` **reference** (no such policy tables exist yet — Phase 7/8)
|
|
|
|
|
and opaque `entryConditions`/`exitConditions`. A reparenting edit that would make a node its own
|
|
|
|
|
ancestor is rejected with `400 CYCLE_DETECTED`; every create/edit/activate/deactivate writes an
|
|
|
|
|
`AuditLog` row.
|
|
|
|
|
- **Capability-eligibility lookup** (`GET /support-org/capability-eligibility?skills=&productId=&
|
|
|
|
|
categoryId=&priorityId=`) is a **read-only** contract for the future orchestration/assignment
|
|
|
|
|
phase (Phase 7) to call — it answers "who is capability-eligible" (active agent, active team,
|
|
|
|
|
holds every required skill, composed with any matching hierarchy node's own skills), and
|
|
|
|
|
deliberately **never** filters by availability, working hours, or current load (doc 05 §3:
|
|
|
|
|
"capability is evaluated before availability"). It does not make an assignment decision.
|
|
|
|
|
- This feature supersedes the pre-existing `identity/agents` scaffold stub, which queried a
|
|
|
|
|
generic `User`/`UserRole` model unrelated to this system's real architecture (nothing else uses
|
|
|
|
|
it — `CustomerReference`, built in 002, is the real customer-identity mechanism).
|
|
|
|
|
`identity/customers` and `identity/auth` are untouched — out of scope here.
|
2026-09-03 11:58:17 +05:30
|
|
|
|
|
|
|
|
# Orchestration and Assignment
|
|
|
|
|
When a ticket reaches `HUMAN_ESCALATION`, this system automatically resolves 006's hierarchy/
|
|
|
|
|
capability-eligibility lookup for it and assigns exactly one eligible agent through a pluggable
|
|
|
|
|
strategy — no caller has to trigger this. See
|
|
|
|
|
`specs/007-orchestration-assignment/contracts/orchestration-contract.md` for the full route list
|
|
|
|
|
and `specs/007-orchestration-assignment/quickstart.md` for runnable scenarios.
|
|
|
|
|
|
|
|
|
|
- **Trigger**: entirely event-driven — `ticketsService.updateStatus` (003) publishes a domain
|
|
|
|
|
event on every status change, and this feature's subscriber (registered alongside 005's own
|
|
|
|
|
AI-session-ending one) reacts when `newStatus === 'HUMAN_ESCALATION'`. Neither `ticketing` nor
|
|
|
|
|
`ai-support` has any import of `orchestration` — the event bus is what keeps that direction
|
|
|
|
|
one-way. **The event bus itself was fixed while building this feature**: `EventBus.publish` now
|
|
|
|
|
actually awaits its subscribers (it previously fired them via `EventEmitter.emit`, which never
|
|
|
|
|
waits for an async listener) — without that fix, a status-update HTTP call could return before
|
|
|
|
|
orchestration (or 005's own hook) had actually finished. `registerDomainEventHandlers()` is
|
|
|
|
|
called from `buildApp()` itself now (idempotently — see its own code comment), not only from
|
|
|
|
|
`server.ts`'s production startup path, so this is true in tests too, not just production.
|
|
|
|
|
- **Strategies**: `ROUND_ROBIN` (concurrency-safe via an atomic Redis `INCR`, verified under real
|
|
|
|
|
concurrent load in `tests/concurrency/round-robin.test.ts`), `LEAST_LOADED`, `SKILL_BASED` are
|
|
|
|
|
auto-selected from the matched hierarchy node's own `assignmentStrategy` (or a configurable
|
|
|
|
|
system default, `ORCHESTRATION_DEFAULT_STRATEGY`, when no node matched). `MANUAL`/`DIRECT` are
|
|
|
|
|
never auto-selected — they only ever come from `POST /admin/tickets/:ticketId/assignment`'s
|
|
|
|
|
explicit `agentId`. `LEAST_LOADED`/`SKILL_BASED` ties fall through to the same concurrency-safe
|
|
|
|
|
cursor `ROUND_ROBIN` uses, never an arbitrary/unstable ordering.
|
|
|
|
|
- **A ticket's "required skill"** comes from its most recent AI diagnosis's `problemType` (005),
|
|
|
|
|
when one exists, unioned with any matching hierarchy node's own `skills` (006) — when neither
|
|
|
|
|
exists, there's no skill constraint at all (every active agent is eligible), never zero eligible
|
|
|
|
|
agents; see `specs/007-orchestration-assignment/research.md`.
|
|
|
|
|
- **History**: `Assignment` is a version-row-per-period model (reassigning supersedes the current
|
|
|
|
|
row, never overwrites it); `AssignmentHistory` is a separate, purely-additive event log —
|
|
|
|
|
`GET /tickets/:ticketId/assignment` (current) and `GET /tickets/:ticketId/assignment-history`
|
|
|
|
|
(everything, including "no eligible agent" outcomes with `agentId: null`).
|
|
|
|
|
- `currentLoad` (006) is read by `LEAST_LOADED`, never written by this feature — no
|
|
|
|
|
increment-on-assign/decrement-on-resolve lifecycle exists yet (that's a later phase's job once
|
|
|
|
|
ticket resolution itself is built).
|
|
|
|
|
- SLA policy execution and rule-driven escalation are intentionally out of scope here — doc 05
|
|
|
|
|
documents them alongside orchestration, but the roadmap places them in Phase 8. See
|
|
|
|
|
`specs/007-orchestration-assignment/spec.md` Assumptions.
|