AGENTS.md
Rama main · réplica de lectura del repositorio original.
# MailFathom Development Instructions
These instructions apply to the entire repository.
The product name is `MailFathom`. The service carries a solution of its own, `backend/MailFathom.slnx`, and `frontend/` stands beside it holding the client. **`frontend/` is a pnpm workspace of React and TypeScript**: the Uno Platform client that stood there was withdrawn — the platform did not work out for this project — and what replaced it is two packages under `frontend/src/`, `Client.Backend` and `Client.App`, with `frontend/README.md` as their page. `frontend/src-tauri/` beside them is the desktop head's Rust crate — the shell that wraps what those two build, and one of the two places a difference between the web head and the desktop head is allowed to exist, the other being a stated rule in the client's one stylesheet; `frontend/src/AGENTS.md` § *The two heads* holds that rule. The client's unit suite runs on Vitest, and a test in it sits beside the source it covers rather than in a tree of its own, so `frontend/tests/` holds the contract that governs all four suites beside the three that drive a built client and belong to neither package — `frontend/tests/AGENTS.md` says why, which of those three reaches a real deployment, and which of them drives the desktop head's own WebView. What this file holds is written for both stacks, and what governs the client alone is `frontend/AGENTS.md` beside `frontend/src/AGENTS.md`. Project directory and file names use short boundary names such as `Domain`, `Application`, and `Host`, while `backend/Directory.Build.props` applies the `MailFathom.*` prefix to assembly names and root namespaces.
## Project status
MailFathom is released. Tags are pushed, the image and the Helm chart are published, and the newest GitHub release carries the schema artifact that brings a database to it beside the `mfctl` binaries, so every file here describes something a reader can install rather than something being prepared for them. The major version is still `0`, and `<VersionPrefix>` in `Version.props` names the release being worked toward rather than the last one shipped. What follows is how a change against that is judged.
- A breaking change to the MCP tool contract, the configuration schema, the database schema, or the deployment contract is permitted, because the major version is `0`. ADR 0004 narrows SemVer's `0.y.z` clause to exactly that: a minor may break any of the four public surfaces, a patch may break none of them, and no deprecation window exists below `1.0.0`. So what a change decides is which shape of a contract is right, never whether the break may be taken — a wrong key name, a wrong tool argument, or a wrong column is not kept because somebody would have to act on an upgrade.
- MailFathom is under active development, and until `1.0.0` a release owes no compatibility with any earlier one — the root `README.md` tells every reader so before they install it. That reaches as far as complete incompatibility: a configuration that has to be rewritten rather than migrated, a tool contract replaced rather than extended, a deployment that has to be reinstalled. So a change is designed for the contract it should have, and never shaped, narrowed, or postponed to spare an existing installation an upgrade step.
- What a break does cost is the record of it. Name it in the issue and in the pull request, against the surface it breaks and with the operator's action rather than only the fact, because the release pull request composes `CHANGELOG.md` from that reading and nothing else writes the file. A break nobody wrote down is the defect here; the break itself is not.
- The database schema is append-only between releases, which is the one place the permission above stops being a licence: a change adds a migration and never regenerates, renames, or deletes one, and a release is deployable over the previous release's data unless its changelog entry says otherwise. The one rewrite is the release's own: its pull request folds the migrations the cycle added into one migration for that release, by concatenation and under the identifier of the last one it replaces, and proves the fold cannot be told apart from what it replaced. ADR 0050 records the lifecycle and `$prepare-release` runs it; no other change folds anything.
- Documentation, deployment assets, and the third-party register describe a product somebody is running, so an inaccuracy in one is a defect against a user rather than a note to fix before release.
- Still refused, and unchanged by any of the above: compatibility shims, deprecation machinery, versioning scaffolding, and migration paths for versions that never existed. Owning the current contract is not the same as inventing history behind it.
None of this relaxes the rest of these instructions.
## Where the rest of the contract lives
This file is loaded into every agent session in either stack, so it holds what has to be true before a file is read and what a session in one stack still owes the other: the non-negotiables, the two roles, the privacy and governance posture, the licensing obligations, the workflow and verification entry points, and the principles that outlive whichever language a stack is written in. A rule a session in the other stack could not act on is not one of them, so it lives in the stack that acts on it. Every rule lives in exactly one place, and no rule is stated twice. A row below is a pointer to the whole rule, never a summary of one.
| Where | Read when | What it holds |
|---|---|---|
| `backend/AGENTS.md` | A change anywhere under `backend/` | The service stack's whole contract: the .NET and C# conventions and the asynchronous return types, comment and XML-documentation discipline, the clean-architecture boundaries, the cross-boundary email invariants, the three custody phases every behaviour touching an account's mail answers for, package pinning and lock files, and the three verification rules that are this solution's alone |
| `backend/src/AGENTS.md` | A change under `backend/src/` | What holds for the service's production code alone: that several independent replicas run this code and what each design decision owes that — which state is one replica's, what a guarantee may rest on, how singleton work is excluded and a contested row claimed, and what a rolling upgrade running two builds obliges — then API and failure design, dependency injection and configuration, and the lifetime and bounds every outbound HTTP client is registered with |
| `backend/tests/AGENTS.md` | A change under `backend/tests/`, and any change to an agent or its prompt under `backend/src/AI/` | The service suite's own policy, coverage, what belongs in the integration suite instead, and what belongs in the evaluation suite that measures what an agent answers — including what a change to an agent owes it and how a case is written |
| `backend/src/Infrastructure/AGENTS.md` | A change under `backend/src/Infrastructure/` | Persistence and EF Core rules, and email-protocol safety |
| `backend/src/Mcp/AGENTS.md` | A change under `backend/src/Mcp/` | Where a result type and the types nested inside one are placed |
| `frontend/AGENTS.md` | A change anywhere under `frontend/` | The client stack's whole contract: what the toolchain already enforces and what a person does when it blocks them, the suppression policy, package pinning and the lock file, and driving the running client in a real browser |
| `frontend/src/AGENTS.md` | A change under `frontend/src/` | What holds for the client's application source alone: what the application is, the `Client.Backend` and `Client.App` boundary, the state model, module and component design, and the UX, UI, accessibility, and performance obligations every screen meets |
| `frontend/tests/AGENTS.md` | A change to a client test — a unit test, which lives beside the source it covers rather than under that directory, or a browser spec, which lives there | All four client suites' policy: where each test file sits and why, how it is named and what it may assert, how the two packages are covered differently, what is faked at the network boundary, how a component reading time or randomness is made deterministic, what the browser suite covers, where it runs, and what it keeps on failure, what the end-to-end suite reaches that none of the others can, and what the desktop suite proves in the shell's own WebView and how it drives one |
| `design/README.md` | A change to a client screen, or to the design itself | What `design/` holds, how an artboard page is put together, how a change to the design is made and what it owes the state inventory and the parity pairing, and how a disagreement between the design and the client is resolved |
| `design/AGENTS.md` | A change under `design/` | How the design is authored: the skills a screen is drawn with, that a design change is made fast, and what an artboard's own code does not owe and what it owes regardless |
| `docs/AGENTS.md` | A change under `docs/` | Documentation rules and the `describes:` marker every page carries |
| `docs/operations/issue-tracking.md` | `$start-task` opens, places, or claims an issue, `$finish-change` links one | Which work needs an issue, what its body contains, the one issue type, the `backend` or `frontend` label naming the stack it lands in and the `design` label naming work that changes `design/`, the runtime-specific `agent:claude` or `agent:codex` marker a session applies when it takes the issue, the milestone, the board's `Area`, `Queue`, and `Size` fields and its views, and how an arrival from outside the project is triaged |
| `docs/operations/agent-workflow.md` | Any of the workflow skills runs | The verification scripts at length, what each skill does, and how `Fathom review` behaves on a pull request |
| `docs/operations/local-development.md` | The SDK, the database, packages, or the deployment assets are involved | The environment, the EF Core command split, the pull-request checks, package sources, lock files, and how every pin in the repository is read against its upstream |
| `$check-docs-licenses` | Every change, as the mandatory completion gate | MailFathom's own AGPL-3.0-only record across its five files, and the third-party licensing rules and register |
| `$add-migration` | A model change needs a migration | The additive migration workflow and the SQL review that is part of it |
| `$update-dependencies` | The task is to move dependency pins | The order a bump runs in, what decides whether a pin moves at all, and the two pin families that are surveyed and never rewritten |
## Critical repository rules
- **Everything written here and around it is in English**: identifiers, comments, documentation, commit messages, branch names, pull requests, issues, and every comment on one. The repository is public and its conversation is part of it, so the language a task is *discussed* in — with the owner, in a chat session, in any tongue — never reaches an artifact. This is the one place the rule is stated, and it holds in every directory.
- **Two kinds of string never reach a tracked file.** One is the word `ponytail`, the marker a coding-assistant plugin puts in front of a comment about a deliberate shortcut: the comment states the ceiling and the upgrade path in plain words, with no tool's name in front of them. The other is an address on Claude's own web surfaces, `claude.ai` and `claudeusercontent.com`, which names a session, an artifact, or a design project nobody reading the repository can open; the public documentation on `claude.com` is not one of them. `scripts/test-agent-workflow.sh` fails on either, naming the file and the line, and this file is the one place exempt from that sweep because it states the rule.
- Never commit directly on `main` or `master`. Branch before committing; in the owner's checkout that branch is named `agent/<short-description>`.
- Preserve unrelated user changes. Stage only files that belong to the current task.
- Make architectural decisions before implementation. Keep changes small, reviewable, and aligned with the boundaries below and the decision records that refine them.
- Treat ADRs under `docs/decisions/` as required architectural context for AI agents. Before changing architecture, boundaries, configuration, persistence, provider integration, governance, security-sensitive behavior, or cross-cutting infrastructure, read the relevant ADRs and keep the change consistent with their current status and rationale.
- Treat MailFathom as an enterprise-grade system even during early scaffolding: preserve seams for governance, auditability, privacy controls, operational hardening, compliance evidence, and future Agent Governance Toolkit (AGT) adoption without prematurely adding runtime dependencies.
## The two roles this contract is written for
The repository is public and its roadmap board is not, so these rules run in two places and a few of them belong to only one. `$start-task` resolves which applies, from the workspace rather than from a question, and states it in the brief. That is one step into a session and a session need not start there, so a contributor states the fork role once in a local instruction file their harness loads first — `CLAUDE.local.md` beside `CLAUDE.md`, which `.gitignore` covers as `*.local.md`. `$get-started-contributors` writes it while setting the clone up — a skill a person invokes by hand, never an agent — and `docs/operations/agent-workflow.md` § *Stating the role before a skill resolves it* carries what a wrong guess costs.
**The owner's checkout** is where `origin` is `fathomise/MailFathom` and the credential can push to it; a clone of it without push access is neither role until it is repointed at a fork. Everything here applies, including the rules that exist nowhere else: the `agent/<short-description>` branch name, the linked worktree, the issue type, the runtime-specific `agent:claude` or `agent:codex` marker and the milestone on an issue, and pushing to this repository. The board's `Area`, `Queue`, and `Size` fields are always written here and sometimes elsewhere, for the reason the next paragraph gives.
**The fork role** is an ordinary clone of a fork, on a branch the contributor named, with `fathomise/MailFathom` configured as a second remote so every gate resolves the base it will actually merge into. Each rule above is absent there rather than relaxed: an issue from outside the project carries no issue type or label by design and triage supplies them, and attempting a write this role does not hold returns a permission error rather than a partial result. The board fields are the one part that follows access rather than role, because the owner grants read or write on project `1` whenever they decide to and a grant on a board is never authority over this repository; `$start-task` probes for it in either role instead of inferring it from the remote. The protected paths — `.github/`, `.config/`, `.agents/`, `.claude/`, `docs/decisions/`, the named root files, and any `AGENTS.md`, `CLAUDE.md`, `.editorconfig`, `.gitattributes`, `.worktreeinclude`, or `Directory.Build.props` at any depth — are open to every author in either role, agents included. What they add is a wait rather than a refusal: the `Protected paths` check holds a pull request that changes one from an author who is not a maintainer until a maintainer approves its current head, and passes a maintainer's own on authorship, so a task that reaches one makes the change in the same pull request instead of stopping, splitting it out, or turning it into an issue. A rule a path carries on its own grounds still holds — the release pull request's sole claim on `CHANGELOG.md` among them — and `$start-task` names the protected paths a task reaches so the brief says whether the pull request will wait on that approval.
Everything else is one contract. The architecture boundaries, the conventions, the privacy and licensing obligations, the tests, the documentation, and both verification scripts hold identically, and so do `$review-change`, `$check-docs-licenses`, `$closed-enumeration`, and `$add-migration`, none of which needs authority over this repository at all. `docs/operations/agent-workflow.md` records the split; `CONTRIBUTING.md` states the contributor's half of it.
## Documentation and test obligations
- Before using or changing a library, framework, protocol, CLI, or external API, consult its latest official documentation. Prefer Microsoft Learn, official project documentation, specifications, and upstream repositories.
- Pin every dependency version centrally in the stack that owns it, and never float one. Which file that is, and what else moves with a pin, is the stack's own; `backend/AGENTS.md` states it for the service.
- Unit tests are part of every behavior change, feature, and bug fix. Test observable behavior and domain invariants rather than private implementation details — one test describes one behavior even where several assertions prove it.
- Tests are fast, isolated, repeatable, order-independent, and safe to run in parallel. Do not use real clocks, nondeterministic random values, shared mutable fixtures, sleeps, network calls, databases, containers, or the filesystem. A suite earns an exception to that only where its own instructions state one.
- Develop and verify tests and production code before documenting the implemented behavior. Update affected durable documentation in the same reviewable change set; stale guidance is a defect.
- Read `docs/AGENTS.md` before changing documentation. It holds the rule that every ADR stays `proposed` until `1.0.0` and is created and edited in the change that needs it without asking first, and that `docs/decisions/` is still a protected path.
- Every link in the repository-root `README.md`, in `deploy/docker/README.md`, and in `deploy/helm/mailfathom/README.md` is absolute, including the ones that point back into this repository and the ones that point at a heading of the file itself. Those three files are the documentation rendered outside the repository — a chart listing, a package page, the Docker Hub repository overview, anything that copies the Markdown — and a relative path resolves against the wrong root everywhere except GitHub's own blob view, so it arrives as a broken link rather than as a link to somewhere else. A page the documentation site publishes links to the site, at the version-agnostic address `https://fathomise.github.io/MailFathom/docs/<path under docs/>.html`, because a reader arriving from a registry description wants the readable form rather than a Markdown file in a tree; the heading anchor carries over unchanged, since docfx and GitHub derive the same slug from a heading. Everything else links into the repository: a file as `https://github.com/fathomise/MailFathom/blob/main/<path>`, a directory as `.../tree/main/<path>`, an image at its `raw.githubusercontent.com` URL, and a heading on the README itself as `https://github.com/fathomise/MailFathom#<anchor>`. `docs/README.md` and the architectural decision records are in that second group deliberately — the site publishes neither, and `docs/operations/documentation-site.md` says why. `scripts/test-agent-workflow.sh` fails a site link naming a page that does not exist and a repository link to a page the site does publish, in any of the three, so the two groups cannot quietly swap. Each of the three is written for a reader the other two do not have, which is what decides what belongs on it: `deploy/docker/README.md` is the Docker Hub repository overview and nothing else, for somebody who already has an image reference and wants a running container — what the image needs, how it runs, how to verify it — and the same script fails it above the 25000 characters Docker Hub accepts, which is the check that exists because the root README outgrew that limit while serving as the overview and failed a release after the image was already published; `deploy/helm/mailfathom/README.md` is packaged into the chart and is what Artifact Hub and every other chart listing renders, for somebody who has already chosen Kubernetes and Helm — what the chart renders and refuses to render, what an operator supplies, the schema step, and the Pod Security posture — and it carries no length assertion because no chart listing imposes a limit; the root `README.md` is for somebody deciding whether to adopt the project at all, and is no longer copied to either registry. This applies to those three files alone: pages under `docs/` are read inside the repository and keep relative links, which is what lets a directory be moved without rewriting every reference into it.
- Never edit `CHANGELOG.md` during ordinary work. It states what a release shipped, so the release pull request writes it and nothing else does; `$check-docs-licenses` holds the reasoning and reports `n/a` for the file on every ordinary change.
- Write the licensing header by hand in every file that is not C#. IDE0073 reads C# only, so `dotnet format` and `scripts/verify-fast.sh` apply `.editorconfig`'s `file_header_template` to `.cs` files and to nothing else — a new workflow, script, Helm template, Quadlet unit source, skill, or documentation-site asset gets no header from either, and neither says so. A new file carries the same three lines in the form its own readers parse: `# ` comment lines opening a `.yml` or `.yaml` file, a `.toml` manifest, or a systemd unit source under `deploy/quadlet/`, the same comment lines under the shebang of a `.sh` file, a `{{- /* ... */ -}}` comment in a Helm template so the header stays out of the rendered manifest, `license` plus a `metadata` block naming the author and the repository in a `SKILL.md` frontmatter, `// ` lines opening a `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.rs`, or `.kt` module — the last of those being the Kotlin this repository writes under the Android head's generated project, which is source it owns however generated its roof is — one `/* ... */` block opening a `.css` file, which has no line comment to use instead, and one `<!-- ... -->` comment opening an `.html` document. A `.json` file carries none, and neither does a generated lock file, nor the Kotlin the Android head's project generator writes for itself: `MainActivity.kt` and everything under its `buildSrc/`, which a regeneration rewrites and would take a hand-typed header with it. `Cargo.lock` is written by resolving a manifest rather than by hand, exactly as `pnpm-lock.yaml` is, which is why the `.yaml` rule above excludes it. `design/runtime/support.js` carries none either: it is the runtime a design tool generated for the design's artboards to boot on, and it is replaced whole rather than edited, so a typed header would go with the next replacement — everything else under `design/` is written here and carries one. A skill vendored from another repository carries none for the same reason, being somebody else's text refreshed whole: its `SKILL.md` frontmatter keeps the upstream's `license` and names the upstream author and repository in `metadata`, the upstream licence text sits beside it as `LICENSE.txt`, and `THIRD_PARTY_LICENSES.md` carries a row naming that repository. A `package.json` is strict JSON, which has no comment syntax to put one in; a `tsconfig.json` does take one, because `tsc` parses it as JSONC, and the four this repository writes use that to carry their reasoning — but a comment one tool's parser grants is not a form the header is written in, and `scripts/test-agent-workflow.sh` asserts it on none of them. `scripts/test-agent-workflow.sh` fails when one is missing and reads the expected text out of `.editorconfig`, so the header is one decision recorded in one place and applied in seven forms.
- `$check-docs-licenses` is a mandatory completion gate, including when its verdict is `n/a`.
## Architecture
- The repository carries two stacks beside each other, each in its own `src/` and `tests/`. `backend/` holds the service solution and `frontend/` holds the client as a pnpm workspace of two packages, `Client.Backend` and `Client.App`, whose own build contract is `frontend/package.json` and `frontend/pnpm-workspace.yaml`, plus the desktop shell under `frontend/src-tauri/`, whose build contract is its own `Cargo.toml` and the `Cargo.lock` committed beside it. The service owns its build contract — `MailFathom.slnx`, `Directory.Build.props`, and `Directory.Packages.props` under `backend/` beside `backend/tools/` — because MSBuild walks up from a project directory and a copy of any of them at the root would govern every project in the repository rather than one stack's. What stays at the root is what genuinely governs both: `global.json` and `NuGet.config`, which are SDK and supply-chain decisions no stack may take twice, and `Version.props`, which `backend/Directory.Build.props` imports so a client and a server that ship together report one version. The two stacks share no other build file and no configuration file, and meet only over the endpoints `backend/src/Host/` exposes. The client reaches the service over the HTTP API served beneath `/api/client`, which is a transport surface of its own with its own listener and its own credentials, off unless a deployment enabled it; `docs/operations/client-endpoint.md` is the page. Serving a client *page* is a second setting again, and the bundle it serves travels inside every published image.
- Each stack states its own internal boundaries where it is acted on. `backend/AGENTS.md` § *Architecture* holds the service's, and the bullets below are what holds whichever stack a directory belongs to.
- Prefer direct, explicit dependencies and small interfaces at architectural boundaries. Do not introduce abstractions without a current testing, protocol, or replacement need.
- Keep a directory small enough to be one thing. Around fifteen files is where a listing stops reading as a group and starts reading as a list, so treat that as the point to ask whether the directory still holds one thing rather than several — and when it holds several, give each a subdirectory that names it. The count is a proxy for cohesion and nothing more: no script, no verification gate, and no review check enforces it, and a directory whose files genuinely are one concept stays flat past the number rather than being split into four two-file directories to satisfy it. `backend/src/Domain/Emails` is the worked example of ignoring it correctly, and `backend/src/Infrastructure/Persistence/{Entities,Connections,Sessions,Emails,Synchronization}` of applying it. Reasons to look: an agent reads a listing before it reads a file, so a forty-name listing costs the same context on every task that touches the project, and it is the point at which a new type gets dropped next to the nearest-sounding name instead of into the group it belongs to.
- The rule above is about a directory that holds a structure. `docs/operations/`, `scripts/`, and `.github/workflows/` are flat by design and stay flat at any size, because each of their entries is an independent document or tool that nothing else in the directory composes with. A test directory instead follows the structure of the code it covers, so `backend/tests/Host.UnitTests/Configuration/Endpoints/` mirrors `backend/src/Host/Configuration/Endpoints/`; the doubles a project's tests share are not covering anything and belong in its own `TestDoubles/`.
## The design leads the client, never the other way round
`design/` is the source of truth for what the client looks like and does, so it may run ahead of the client and the client never runs ahead of it. A pull request may change `design/` alone — a screen drawn before anybody builds it is the ordinary order — or `design/` and the client together, or the client alone where `design/` already shows what the change produces. What no pull request may do is leave the client showing something `design/` does not: a client change that adds or reshapes a screen, a state, a composition, an interaction, or a piece of copy the design does not draw, or that departs from the design for one of the three reasons `design/README.md` § *When the client and the design disagree* names, updates `design/` in the same pull request — the artboard and whatever of its data, logic, and styles the change reaches, `design/state-inventory.md` for every state it adds, removes, or gates differently, and `design/parity.json` where a paired screen moves or is reached by different presses. The correction is never deferred to a follow-up issue, because a client merged ahead of its design leaves the source of truth wrong about what ships, and every later `$read-design` reading and parity comparison starts from that wrong design.
A task that reaches both `design/` and the client is done design first. Change `design/` and check it the way `design/README.md` § *Changing the design* asks — opened at every composition it reaches, in both themes — and only then carry the change into the client, building it from the design just checked and holding it against that design with the parity loop. The client built first and the design drawn to match it afterwards is the inversion this section exists to prevent, however both halves end up in one pull request: the design becomes a tracing of the code, and the parity comparison holds the client against a picture of itself. When building the client proves the design wrong, the correction goes back into `design/` first, is checked there, and the client follows it.
Bringing the design along is not bending it to excuse a defect. A difference the parity loop reports on a screen the change did not set out to alter is still the client's to fix, and the design half of a client pull request draws exactly what the client half ships — drawn under `design/AGENTS.md` like any other design change and named in the pull request body as a design change, never folded into the client's diff unremarked.
## Enterprise governance, privacy, and GDPR readiness
- Design every feature with GDPR-aligned privacy by design and by default: data minimization, purpose limitation, storage limitation, confidentiality, integrity, availability, and accountable processing must be visible in the architecture, tests, and documentation.
- Email content, metadata, embeddings, retrieval snippets, audit events, and model/tool traces can contain personal data. Classify them as sensitive by default and avoid broad access, unnecessary copying, long retention, or unredacted logging.
- Keep explicit seams for future data-subject workflows such as access, export, rectification support, erasure, restriction of processing, retention holds, and audit evidence, even when those workflows are not implemented in the first release.
- Do not treat embeddings or derived indexes as anonymous. They inherit the classification, retention, access-control, deletion, and export constraints of the source mail content unless a reviewed privacy design proves otherwise.
- Future AGT adoption must remain an adapter-level governance concern. AGT policy decisions, audit records, and tool-call controls must not leak provider-specific or governance-framework types into `Domain` or `Application`.
- Before adding AGT or any governance/compliance package, verify the current official documentation, .NET 10 compatibility, license, service terms, telemetry behavior, and data-processing implications; update `THIRD_PARTY_LICENSES.md` for any dependency or externally sourced component.
## Reliability, security, and performance
- Apply timeouts to external I/O and distinguish caller cancellation, service shutdown, timeout, authentication failure, and transient transport failure.
- Retry only operations known to be transient and safe to repeat. Use bounded attempts with jittered backoff; never create nested retry storms.
- Use cryptographically secure random generation for tokens and security-sensitive identifiers. Compare secrets using appropriate constant-time APIs where relevant.
- Encode and validate data for its destination context. Treat email HTML, headers, filenames, URLs, tool arguments, and model output as untrusted input.
- Use least privilege for PostgreSQL, OAuth scopes, filesystem access, certificates, and operating-system service accounts.
- Optimize only after measurement. Prefer appropriate algorithms, bounded allocations, and streaming before low-level micro-optimizations.
- Set explicit size and count limits at every public or remote boundary.
## Dependency and implementation discipline
- Keep third-party types inside their owning adapter wherever practical.
- Prefer platform capabilities before adding packages. Every new package must have a clear owner and purpose.
- Adding a package source is a licensing and supply-chain decision in whichever stack takes it, so review it as one and record it in `THIRD_PARTY_LICENSES.md`. Which sources a stack restores from, and what declares them, is the stack's own.
- Treat email content, OAuth tokens, credentials, certificate material, and embeddings as sensitive data.
## Agent workflow and verification
`docs/operations/agent-workflow.md` describes every script and skill named here at length, including what each one asserts and why. These are the entry points and the rules that hold before one is reached.
- For file-changing tasks, start with `$start-task`. Before final verification, use `$review-change`. To finish, use `$finish-change`; it requires `$check-docs-licenses`, the fast loop, focused staging, and a pull request.
- Use `scripts/inspect-workspace.sh` for a read-only workspace preflight and `scripts/verify-fast.sh` both during implementation and as the gate a change is held to before it is committed. `scripts/verify-full.sh` is the pipeline's, and running it locally is a choice rather than a step: `CI` asserts every one of its verdicts on the pull request, from the same coverage target, the same contract suite, and the same client flow, and it does so on a runner per job rather than on the machine the work is being written on. Run it by hand where the answer is wanted before a push — a coverage figure, the whole solution's formatting, the client bundle — and otherwise let the pipeline give it.
- What the local gate withholds is worth knowing rather than discovering. Three of its verdicts are the full gate's rather than the fast loop's — the 85% coverage target, the workflow contract suite, and the client's bundle build — so they arrive either from a full gate somebody chose to run or from the pull request. Two arrive from the pull request and nowhere else: the browser suite and the desktop suite, neither of which either gate runs and which `CI`'s `Frontend` job is the only thing to assert — one job per suite in it. The client's lint, suite, and formatting are narrowed: the fast loop runs ESLint and Prettier over the client files the change touched and Vitest over the test files importing them, and `scripts/resolve-changed-client-files.sh` refuses that narrowing where the change reaches a shared client input or removes a client file. The type check still runs whole, and what the narrowing gives up — a type-aware lint rule whose verdict on an unchanged file moved with a changed file's types — is the pipeline's whole-workspace lint to report. The service build is narrowed in what it asks rather than where: the fast loop builds without the commit stamp and Source Link, so a commit recompiles only what it reached, and without `CA2000` and `IDE0055`; its one formatting pass repairs whitespace and the final newline ahead of the build without loading the workspace, so the build judges an already formatted tree. A disposal finding, the order of using directives, and an `IDE0055` finding are therefore the full gate's and the pipeline's to report. Because the two gates build with different properties, switching between them in one worktree rebuilds the solution once.
- Which stack a gate runs is never yours to decide. Both read the changed paths and run the service flow, the client flow, both, or neither, from the same two lists `ci.yml` gives its `detect-changes` job; `scripts/resolve-changed-stacks.sh` holds them, and `scripts/test-agent-workflow.sh` fails when either disagrees with the workflow. The one departure is a path under `.github/workflows/` or `.github/actions/`, which both gates pass over: in the pipeline a changed workflow runs itself, while no gate here runs one, so a stack's flow could not verify the edit and the contract suite is what reads it. The client flow restores the pnpm workspace under `frontend/` in locked mode and runs its lint, its type check, its suite, its formatting, and — in the full gate — its build, so it needs Node and pnpm on the path exactly as the service flow needs the SDK; a change no build reads runs no solution at all rather than a whole Release build of one it could not have broken.
- Neither gate proves the same tree twice. A run that passes records a digest of what it verified under `artifacts/verify/`, and a run handed a digest it already recorded reports the earlier run and stops in under a second — so run the gate rather than reasoning about whether the last one still holds, and never present a skipped run as a run that did not happen. A failing run records nothing, a fast loop whose formatting pass only repaired files records the formatted tree — commit the rewrite and push without running it again — and `VERIFY_FORCE=1` runs everything regardless. `docs/operations/agent-workflow.md` § *A gate does not prove the same tree twice* holds what a record claims and what retires one.
- Both verification scripts refuse to run on `main` or `master`. Check out the branch that carries the change instead of working around the refusal.
- Both gates fetch the base branch and reject a branch that does not contain it, listing the commits that arrived. Read them before answering the refusal: rebase when one of them conflicts with the change or relates to it — touches a file, a name, a contract, or a rule the change relies on — and otherwise run the gate again with `VERIFY_IGNORE_BASE_DRIFT=1`, which reports the same drift and carries on, because a rebase nothing asked for rewrites every SHA a review reply quotes and proves nothing the merge queue does not. Treat an unreachable remote as a blocked gate either way: the fetch is never skipped, since a drift nobody fetched is a drift nobody read. The pipeline asks something else. A pull request behind `main` is not a failed pull request and merges without a rebase: every merge goes through the merge queue, which builds and tests the tree the merge actually produces — `main`, the entries ahead, and the pull request — and reuses the pull request's own result when that tree is the head it already tested, while `Coherence` stops a pull request only when a change related to it landed underneath. So the pipeline never asks whether a branch is current, and a workflow that fails a pull request merely for being behind `main` turns every merge into a red check on every branch open beside it. ADR 0038 records the model, and `docs/operations/agent-workflow.md` § *The merge queue* what to do when the queue removes a pull request. The one checkout that carries on regardless is one whose remotes name no MailFathom at all: there is no base to be behind, the fast loop says so and continues, and the full gate still refuses it.
- Never run a stack's own formatter by hand. Both gates already run what belongs to the stacks a change reaches, and `backend/AGENTS.md` § *Verification* states what that is for the service and what it does and does not report.
- Use `scripts/review-obligations.sh` to see what a change obliges elsewhere: the tests covering each changed source file, named for the service and sitting beside it for the client, the pages whose `describes:` marker covers each changed path, and the registers whose trigger moved. It is the same index `Fathom review` runs on a pull request, reached through an adapter rather than a second implementation, so a rule cannot hold in review and lapse in the pipeline. It reports and never gates, and nothing it prints is a finding until it is confirmed in the file it points at.
- Use `scripts/lint-shell-and-workflows.sh` before pushing a change to a shell script, a workflow, or a composite action. Neither gate reads a `run:` block as shell, and this is what the `Shell and workflow lint` job of `Repository contracts` runs: ShellCheck, actionlint, and zizmor at pinned versions, with what each is told to accept recorded in `.github/actionlint.yaml` and `.github/zizmor.yml`. `docs/operations/agent-workflow.md` § *Entry points* says why it is a job beside the contract suite rather than a step of either gate.
- Use `scripts/update-dependencies.sh` to read every pin in the repository against its own upstream and against the terms `THIRD_PARTY_LICENSES.md` recorded for it — packages in both stacks, the desktop shell's crates, the tool manifest, the SDK pins, the workflow action references, the container images, and the linters `scripts/lint-shell-and-workflows.sh` downloads. It reports and never gates, like `scripts/review-obligations.sh`; `--apply` rewrites the six pin families that can be written mechanically and regenerates whichever lock files those pins belong to, and it never edits the register, which is prose recording a completed review — including the census it records for each of the client's three closures, which a moved client pin obliges and nothing recomputes. `docs/operations/local-development.md` § *Reading every pin against its upstream* holds what it covers and what it refuses to write.
- Move a pin only through `$update-dependencies`, and only when the task in front of you is a dependency update. A pin being behind is never a defect to fix inside an unrelated change: what a dependency is worth updating to is a judgement, taken with an issue and a pull request of its own, and noticing one mid-task is worth a sentence in the report rather than a line in the diff.
- Read the version with `scripts/read-declared-version.sh` rather than retyping it. `<VersionPrefix>` in `Version.props` is the only application version number in the repository, and everything that has to put that number somewhere gets it there at build or package time. ADR 0004 and `docs/operations/release-procedure.md` record the whole scheme.
- Never cut a release as part of a task. `$prepare-release` is manual-invocation only — its frontmatter sets `disable-model-invocation`, so an agent cannot reach it — because when a version becomes real is the owner's decision rather than something to infer from work that looks release-shaped. It pushes no tag either: tagging stays the owner's act as well.
- Never start `Fathom review`. It reviews a published pull request on its own and spends Claude subscription usage doing it; `$review-change` is the review an agent performs, and asking for another is the owner's act through a comment. A comment line beginning with `fathom-review` or `@fathom-review` triggers a run only when the phrase leads the line, which is what makes writing it mid-sentence in prose about the workflow safe.
- Review a change under `deploy/` by reading it. No script here deploys anything. The chart is the one part with a gate: `scripts/render-helm-manifests.sh` lints and renders it against every `deploy/helm/mailfathom/ci/*-values.yaml`, holds the rendering against the manifests committed under `ci/golden/`, and requires every values document under `ci/refusals/` to be refused with the wording that document records, `scripts/render-helm-manifests.sh --update` takes an intended change into them, and `CI` runs the same script on every pull request that touches `deploy/helm/`. Neither verification script runs it, because Helm is not a prerequisite of building this repository. The rest of `deploy/` gates on nothing: `QUADLET_UNIT_DIRS=deploy/quadlet /usr/lib/systemd/system-generators/podman-system-generator --user --dryrun` prints the units Quadlet would generate from `deploy/quadlet/` and reports a key it does not recognize, and is the useful local reading there.
- Inspect the final diff for accidental secrets, unrelated edits, generated files, and dependency-boundary violations.