# ticket — agents.md

Project constitution. Inherits from `/srv/CLAUDE.md` (server scope) and
`/srv/apps/CLAUDE.md` (tenant scope). Only rules meeting the admission rubric
(failure-backed, tool-enforceable, decision-encoding, or triggerable) belong
here — no generic advice a model already applies. Budget: ~200 lines.

Model-independence: this file is the source of truth. `CLAUDE.md` is a symlink
to it so Claude Code loads the same content.

## Read first (in order)
1. **`brief.md`** — what this project is, and the stack decision.
2. **`.logs/planning/01-plan.md`** — the build plan: domain model, email
   pipeline, time model, decisions (proposals until rian confirms),
   milestones, gates.
3. **`tools/caddie-watch.sh`** — run it. If it reports changes, read them and
   update `.logs/planning/02-caddie-feedback.md` §1 before building.
4. **`/srv/projects/standards/README.md`** — server-wide coding standards
   (dispatches by stack: `coding.md` → `react.md` → `frontend.md`), then
   `/srv/apps/caddie/.logs/planning/05-building-a-caddie-app.md`.
5. **`.logs/handoff.md`** — newest-first; where the last session left off.

## Project-specific rules

- **This is a Pattern B app — never gate the host.** `ticket.bowden.works`
  owns its own `/login` + `/auth/*`; a domain gate would 404 sign-in. To hide
  the app before launch: `srv-gw id-site-set --host ticket.bowden.works
  --lan-only true`, never a password gate.
- **The vendored kit files are standard-owned, not app code.**
  `main/app/{bw_auth,bw_accounts,bw_view_as,bw_admin_api,bw_store_sqlalchemy}.py`,
  the conformance tests, and `main/frontend/src/bw-admin/` are refreshed
  byte-for-byte on every scaffolder run — never hand-edit; changes go in
  `bw-app.toml` or your own routers/components. Same for the `caddie-ui`
  pack and the interaction embryo (`services/interaction.py`,
  `attachments.py`): copied byte-identical from their canonical homes, with
  the hash in `.logs/planning/caddie-snapshot.sha256`.
- **This is one of many caddie drop-ins, not one app.** Before adding a
  feature or making a significant change, check it against caddie: either it
  fits the standards as they stand (`/srv/apps/caddie/.logs/planning/05-…`,
  `02-…`, `04-…`, the `caddie-ui` README), or it is a deliberate, written
  case in `.logs/planning/02-caddie-feedback.md`. Never a silent third thing.
  Caddie-general learnings go to that file's §3 (for the runbook); ticket's
  own bugs go to `handoff.md`.
- **Email is hostile input at every layer.** Never let email text reach a
  shell, a tool, an HTML renderer unescaped, or an instruction position in a
  prompt. The webhook is HMAC-verified (timestamp + nonce); the sender must
  pass the forwarder allow-list AND SPF/DKIM; a message failing any check is
  **quarantined** and never parsed into a ticket or shown to the model. Raw
  `.eml` is kept under `data/mail/`, attachments under `data/uploads/` with
  the Interaction Standard addendum's rules (allow-listed sniffed types,
  server names, size caps, `nosniff`, visibility re-checked via the ticket).
- **The AI is schema-bound and side-effect-free.** Triage uses structured
  outputs (`messages.parse`), **no tools**, ids validated against the
  candidate lists before anything is written, every decision logged in
  `triage` with the model and token counts, every decision correctable in the
  triage inbox. The AI never writes to a client. Model: `claude-opus-5` with
  adaptive thinking unless rian says otherwise — never downgrade silently.
- **External participants are namespaced actors.** A comment's
  `author_username` is a BW username OR `email:<addr>` / `ai:triage`. Never
  attribute an email to the forwarder, and never mint a kit account for an
  email address.
- **Time credits are estimates and say so.** `kind ∈ {email_read,
  email_write}` entries carry their inputs + constants version in `estimate`,
  are labelled "est." in UI and exports, keep `edited_from_minutes` when
  changed, and are created **once per message** (dedupe by fingerprint).
  Constants live in config, never inline. Ticket stores no rates — `with`
  owns money.
- **No ticket FK ever points at a kit table** (caddie D5). The kit instance
  id is a plain string on `project`; actor columns are username strings with
  display-name snapshots; state changes land in append-only tables
  (`ticket_event`, `inbound`, `triage`).
- **Postgres is Alembic-only.** `create_all` runs only on the SQLite dialect
  (tests). A dialect-specific expression is exercised against the deployed
  Postgres once before it is trusted (json equality, `IS DISTINCT FROM`).
- **Secrets never appear in chat, compose, or logs.** `ANTHROPIC_API_KEY`,
  `INBOUND_WEBHOOK_SECRET`, `INBOUND_ALIAS`, `POSTGRES_PASSWORD` live in
  `.app.env` (660, `ticket-dev`), staged as `REPLACE_WITH_*` placeholders for
  rian to fill; verify with `grep -c REPLACE_WITH_ .app.env` → 0, never `cat`.
- **Verify the account system with `--check`, not by eye.**
  `/srv/system/id-auth/app-auth/new-bw-app.sh --dir /srv/apps/ticket --check`
  (venv at `.bw-venv/`). Re-run form that works: `new-bw-app.sh --name ticket
  --host ticket.bowden.works --dir /srv/apps/ticket`.
- **Any change to the inbound mail path (DNS, MX, Worker, webhook) is a
  network change**: security review first, rian's explicit approval, then
  `srv-gw security-audit` after.

## Operational
- **Manage:** `srv-gw {deploy,restart,logs,status} --project ticket`.
- **Deploy confirmation:** the running version is visible (`/api/meta`, UI
  footer, or equivalent). Every change bumps it and adds a newest-first
  CHANGELOG line — the visible version is how you confirm a deploy landed.
- **Write access:** `ticket-dev` Unix group (setgid + default ACL). New files need
  `chmod 664` / dirs `775`, or `srv-gw fix-permissions --project ticket`,
  so the service user can read them.
- **Create new subdirectories under `sg ticket-dev`.** If the session's groups
  don't include `ticket-dev`, the kernel silently strips setgid on `chmod 2775`
  and files underneath get the wrong group.

## Layout
```
/srv/apps/ticket/          # workspace — docs and runtime state
├── brief.md  agents.md  CLAUDE.md -> agents.md  CHANGELOG.md
├── .logs/{planning/, handoff.md, diary.md, v8-feedback.md}
├── .memory/  .archive/  notes/  tools/ (caddie-watch.sh)
├── data/{mail/, uploads/}  # runtime state — raw .eml and attachments
└── main/                  # the instance — code only
```
Runtime state (uploads, dumps) belongs at the workspace root, not in
`main/`. The compose file and `.env` sit at the workspace root because
`srv-gw deploy` requires them there — a documented deviation until T3.1.
