AGENTS.md

Shared project instructions

Tier 1designengineeringproductA person checks this

Everyone inventing their own conventions, and every assistant inventing a different set again.

Add to your project
Add "Shared project instructions" from ntent to this repo.

Fetch https://ntent.app/r/f/constitution as plain text. Write it verbatim to AGENTS.md. Then point CLAUDE.md at it with a one-line @AGENTS.md import. Then make exactly these edits and no others: It is a template. Replace every <angle bracket>, delete the sections marked for tiers above yours, and cut any row of the enforcement table whose command this repo does not have.

Then check it: Ask an assistant "what are the rules in this repo" and it answers from the file.

Reads https://ntent.app/r/f/constitution

Read the code
AGENTS.mdAGENTS.md · point CLAUDE.md at it with a one-line @AGENTS.md import
206 lines
# AGENTS.md / CLAUDE.md template

> Copy this to your repo root as `AGENTS.md`, then point CLAUDE.md at it with a
> one-line `@AGENTS.md` import. One file, read by every tool and skimmed once by
> every human.
>
> THIS IS A TEMPLATE, AND IT IS THE ONE FILE IN THE PLAYBOOK YOU ARE MEANT TO
> EDIT. Everything else installs verbatim. Here you replace every `<angle
> bracket>`, delete the sections marked for tiers above yours, and cut any row
> of the enforcement table whose command your `package.json` does not have. A
> constitution that still says `<Project name>` and cites four commands nobody
> can run is worse than none: the first person who runs one and gets "command
> not found" stops believing the rest of the file.
>
> Keep it under about 200 lines. Past that people stop reading it, and an agent
> spends context on it every single turn. A rule that is only true in one
> directory belongs in a path-scoped `.claude/rules/` file, not in here.

---

# <Project name>

<One paragraph: what this is, who uses it, what makes it different from the
obvious thing. If you cannot write this paragraph, that is the finding.>

**Stack.** <Next.js 16 App Router, React 19, TypeScript strict, Tailwind v4,
shadcn/ui on Radix, Postgres via Drizzle, deployed on Vercel.>

**Status.** <Pre-launch / live with N customers / maintenance.> This matters:
it tells a reader whether a breaking change is cheap or expensive.

## Who decides what

<Ownership by name and by directory. An agent that knows a decision is not its
to make will ask instead of guessing. So will a junior.>

- **<Name>** owns product decisions, copy, and the design system. Directories:
  `src/components/`, `src/app/globals.css`.
- **<Name>** owns data architecture, migrations, and anything touching money.
  Directories: `src/lib/db/`, `drizzle/`.
- **AI assistants** help everywhere inside the rules below.

**Never change without asking:** <auth and session handling, payment code,
migrations that touch existing rows, anything under `src/lib/security/`.>
Mark these in the file itself with `// @ai-policy: read-only` so the marker
travels with the code when it moves.

## Hard rules

<Numbered, short, each one enforceable, each one carrying the reason. Number
them because the lint message, the review comment and the PR template all cite
the number, and "hard rule 3" is a shorter conversation than a paragraph.>

1. **Every user-visible string goes through `t()`.** Add the key to every file
   in `messages/` in the same change. Accessibility copy counts: `aria-label`,
   `placeholder` and `alt` are read aloud by a screen reader and are the ones
   that get forgotten, because they never appear in a screenshot review.
   *Enforced by: `pnpm i18n:check`.*

2. **No hardcoded colour.** Colour is authored in `src/app/globals.css` and
   nowhere else. No `bg-[#hex]`, no hex in a style object. If the colour you
   need does not exist, add a token.
   *Enforced by: `pnpm lint`.*

3. **No arbitrary type sizes.** Use the scale. `text-[13px]` bypasses the
   accessibility text-size setting.
   *Enforced by: `pnpm lint`.*

4. **Logical spacing, never physical.** `ps-4` not `pl-4`, `me-2` not `mr-2`,
   `start-0` not `left-0`. This costs nothing today and is the whole difference
   between supporting a right-to-left language in a week and in a quarter.
   *Enforced by: `pnpm lint`.*
   <Tier 1 and 2 with no RTL plan: keep this rule anyway. It is free now and
   expensive to retrofit. Drop it only if you are certain, and record why.>

5. **No raw `fetch` in feature code.** Reads go through the query layer in
   `src/lib/api/`, writes through server actions. Everything crossing the
   network is parsed by a Zod schema at the boundary.
   *Enforced by: review, plus `pnpm api:check` for the route side.*

6. **Every API route validates its input, checks who is calling, and declares a
   rate limit.** An exemption is a comment in the file saying why.
   *Enforced by: `pnpm api:check`.*

7. **Every environment variable is in `src/env.ts` and `.env.example`.**
   *Enforced by: `pnpm env:check`.*

8. **No em dash in user-facing copy.** Use a period, comma, colon, or
   parentheses. Em dashes read as machine-written and do not translate cleanly.
   Code comments and docs are exempt.
   *Enforced by: `pnpm lint`.*

## What enforces what

<Fill this table in. A rule with an empty right-hand cell is either promoted to
a gate or moved into the judgement section below. There is no third category.
Writing the table is the forcing function.>

<Every row below is an example. Keep the ones whose command exists in this
repo's package.json, delete the rest, and add your own. The right-hand column is
a claim about what runs, and a claim nobody checks is how a rule quietly stops
being enforced.>

| Command | Enforces | Runs |
|---|---|---|
| `pnpm typecheck` | Types | pre-commit, CI |
| `pnpm lint` | Rules 2, 3, 4, 8 | pre-commit, CI |
| `pnpm i18n:check` | Rule 1 | pre-commit, CI |
| `pnpm env:check` | Rule 7 | pre-commit, CI |
| `pnpm api:check` | Rule 6 | pre-commit, CI |
| `pnpm ds:check` | No component stranded in one route, none defined twice | pre-commit |
| `pnpm verify:fixtures` | Every schema still parses its fixtures | pre-commit, CI |
| `pnpm probes` | Every lint rule above actually fires | CI, and locally when lint config changes |

**Deliberate non-gates.** These detect real drift but cannot block a commit,
because someone with a clean clone and no credentials cannot clear them. They
print at session start instead. A gate you cannot satisfy is one people learn to
skip, and that habit spreads to the gates that matter.

| `pnpm coverage` | How much of the contract is built | session-start note |
| `pnpm flags:check` | Feature flags past their removal date | CI, nightly |

## Judgement calls no check can make

<This section is the one that stops a team from being individually compliant and
collectively inconsistent. Every rule above is about what a VALUE may be, so
code can use only tokens, only `t()` keys, only logical spacing, and still be
assembled wrong. Every piece of a bad assembly is legal on its own.>

**Before writing a new page, open the nearest existing one that does the same
kind of thing and copy its structure.** A settings page copies a settings page.
A list page copies a list page. This one line prevents more inconsistency than
any rule above, and it costs nothing.

**Where a message goes** is decided by `src/lib/errorToSurface.ts`, not
per feature. Four surfaces, asked in order, stop at the first yes:
1. Nothing to show at all? Full-page empty state with one retry.
2. One specific value wrong? Inline, at that value.
3. Still true while they are looking at it? A persistent banner.
4. Over and done? A toast.

Tie-breakers: a condition outliving four seconds is never a toast; a toast
carries no recovery action except undo; if the page already has the control that
would retry, the banner states the reason and offers nothing; the same kind of
message takes the same surface everywhere in the product.

**<Add your own here as you find them.> Each one gets the incident that produced
it, in one sentence.** That sentence is what stops the next person deleting the
rule when it annoys them, and it tells them the rule's real boundary.

## When you change something

<Keyed by the kind of change, not by the file. This is the section agents follow
most reliably, because it turns "keep things consistent" into a checklist.>

- **New user-facing string** -> the key lands in every file in `messages/` in
  the same commit.
- **New component** -> if two routes will use it, it goes in `src/components/`
  from the start, not in one route's folder.
- **New API route** -> Zod input schema, auth check, rate limit, and a fixture
  for the response shape.
- **New env var** -> `src/env.ts`, `.env.example`, and the hosting platform, in
  that order.
- **New DB column** -> migration, schema, seed factory, and a header comment on
  the migration answering: is it reversible, does it lock, what is the backfill,
  how do we roll back.
- **New feature flag** -> the registry, with an owner and a removal date.
- **Changed the contract** -> run `pnpm coverage` and note what it moved.

## Decisions

Settled decisions live in `DECISIONS.md` at the repo root. Read it before
re-opening something. If you disagree with one, that is a new dated entry, not
an edit to the old one.

Open questions live in `OPEN-QUESTIONS.md`, beside it. A feature that touches one
surfaces it rather than guessing past it. **If you do not know something, say
so and stop. Do not fill the gap with a plausible default:** a convention that
enters the codebase as a guess is indistinguishable from a decision six weeks
later.

## Running it

```
pnpm install     also wires the git hooks
pnpm doctor      what your machine is missing, and the command that fixes it
pnpm dev         http://localhost:3000
pnpm check       every gate, the way CI runs them
```

<Tier 3 and 4 only:>
## Repo layout

```
src/app/          routes. A route's own components go in _components/
src/components/   anything two routes use. Presentation only, no data fetching
src/lib/          server/  never imported from a client component
                  api/     the only place fetch is called
                  errors/  the surface classifier
packages/         shared across apps. Must not import next or react
```

**Boundaries that are enforced, not conventions:** nothing outside
`src/experiments/` may import from inside it; nothing in `src/lib/server/` may
be imported by a client component; `packages/core` must not import `next`.

Success check: Ask an assistant "what are the rules in this repo" and it answers from the file.

Included in