# Agent Setup

How to configure a coding agent (Claude Code or equivalent) to be productive in this repo, and the style rules it must follow.

Two layers matter, and only one of them arrives with `git clone`:

| Layer                | Lives in                                       | How it arrives                    |
| -------------------- | ---------------------------------------------- | --------------------------------- |
| **Repo conventions** | [AGENTS.md](../AGENTS.md), [rules/](../rules/) | `git pull` — free                 |
| **Machine tools**    | [Brewfile](../Brewfile)                        | `bun run setup`, once per machine |

`bun run setup` installs the tools and wires the repo layer up. `bun run setup:check` is the doctor. Run it when an agent starts behaving as though the conventions do not exist. When the doctor reports stale per-package `node_modules`, `bun run setup:prune-stale` removes them without the brew steps.

---

## 1. What you get for free

`AGENTS.md` is always loaded. `rules/*.md` are **path-scoped**: each declares a `paths:` glob in frontmatter and loads only when a matching file comes into context. Opening a service file pulls in the service rules; opening a migration pulls in the database rules. You do not need to memorise them or name a specialist agent to get them.

After cloning, run once:

```sh
bun run setup   # brew bundle, bun install, then doctor
```

Setup installs the [Brewfile](../Brewfile), which provides `rg`, `fd`, `sd`, `ast-grep`, `bat`, `jq`, bun, `pulumi`, `aws`, `gh`, `dbmate`, `psql`, `temporal`, WARP, `cloudflared`, and Podman with the `docker` CLI (setup creates the `podman machine` VM). `libpq` is keg-only, so the doctor prints the `PATH` line to add to your shell profile rather than editing it for you.

Node is not in the Brewfile. A versioned `brew node@<major>` is keg-only, so installing it changes nothing on `PATH` and fights whichever version manager already owns node. The doctor reads `.nvmrc` and reports a mismatch instead.

`bun install` triggers the `postinstall` that runs [scripts/link-agents.sh](../scripts/link-agents.sh), creating `.claude/rules/<file>.md` → `rules/<file>.md`. Without those symlinks the path-scoped rules never load, and the failure is silent. `bun run agents:reset` rebuilds them after you add a rule file. The same `postinstall` then runs [scripts/link-cfx.sh](../scripts/link-cfx.sh), which links `~/.local/bin/cfx` to the main checkout's operator CLI, so `cfx` is on `PATH` once `~/.local/bin` is. See [packages/cfx-cli/README.md](../packages/cfx-cli/README.md) § Install.

The `postinstall` also creates `node_modules/.cache/nx/cloud`, which Nx prefers over the per-user `~/.nx/<id>/cache/cloud` for its Nx Cloud client. Every checkout of the repo shares that per-user folder, but each keeps its own record and download lock, so one worktree's download deletes the client another is loading and its run fails with a missing module. With the folder in place, each checkout keeps its own client; the task cache stays shared.

**Verify it worked.** Open any file under `packages/*-services/src/` and ask the agent to quote a rule from `rules/service-pattern.md` without reading the file. If it can't, the symlink is missing or your client needs project setting sources enabled.

### The rule index

| Rule                                                | Applies to                                 |
| --------------------------------------------------- | ------------------------------------------ |
| [typescript-style.md](../rules/typescript-style.md) | every `.ts` / `.tsx` file                  |
| [service-pattern.md](../rules/service-pattern.md)   | `packages/*-services/`, `packages/result/` |
| [api-pattern.md](../rules/api-pattern.md)           | `packages/*-interface/`, `packages/*-api/` |
| [database.md](../rules/database.md)                 | `packages/db/`, schema files, services     |
| [dashboard-auth.md](../rules/dashboard-auth.md)     | customer / bank dashboards                 |
| [admin-selectors.md](../rules/admin-selectors.md)   | admin entity selector specs + forms        |
| [package-barrels.md](../rules/package-barrels.md)   | `packages/*/src/**/index.ts`               |
| [testing.md](../rules/testing.md)                   | `**/test/`, `**/*.test.ts`                 |
| [pulumi.md](../rules/pulumi.md)                     | `packages/*/pulumi/`, `**/Pulumi*.yaml`    |
| [ci.md](../rules/ci.md)                             | `.github/workflows/`, `.github/actions/`   |
| [formatting.md](../rules/formatting.md)             | `**/*.sql`, `**/*.md`                      |
| [shadcn.md](../rules/shadcn.md)                     | `packages/ui/`                             |

---

## 2. Nothing to paste

Every rule an agent needs is in the repo. `AGENTS.md` (symlinked to `CLAUDE.md` by [scripts/link-agents.sh](../scripts/link-agents.sh)) always loads; `rules/*.md` load when a matching file comes into context. There is no separate block to copy into `~/.claude/CLAUDE.md`, and nothing to keep in sync by hand.

Anything you keep in your own `~/.claude/CLAUDE.md` layers on top of that and applies across every repo you work in, which is the right home for personal preference and the wrong home for this repo's conventions.

---

## 3. Style rules at a glance

The authority is [rules/typescript-style.md](../rules/typescript-style.md) and [AGENTS.md](../AGENTS.md). This is the orientation pass.

### Hard compile errors — these are not preferences

`tsconfig.base.json` sets `erasableSyntaxOnly: true`, banning every construct that emits runtime JavaScript. The source tree contains **zero** of each across 1,788 files:

- **No `enum`.** Use a `const` object with `as const`, or a string-literal union.
- **No constructor parameter properties.** `constructor(private readonly db: Db)` does not compile. The idiom is a declared field plus one destructured options object:

  ```ts
  private readonly db: B2bDatabase;

  constructor({ db }: { db: B2bDatabase }) {
    this.db = db;
  }
  ```

- **No runtime `namespace`, no `import =` / `export =`.**
- **No `@ts-ignore`** (`@ts-expect-error` with a reason, if you must).
- **Relative imports carry the `.ts` / `.tsx` extension.** ESM only, no `require`.
- **Index-signature reads use brackets** — `row["id"]`, never `row.id`.

### Conventions the linter will not catch

These are the ones to actually internalise, because CI stays green either way:

- **Default to `const`.** `let` is for a genuine loop accumulator, not for a value assigned once per branch — that is a ternary or an extracted function.
- **Never ECMAScript `#private`** — always TypeScript `private`. `#` is runtime-private, so tests cannot reach `service["cache"]` and `spyOn` cannot patch it.
- **`BigNumber` for all monetary arithmetic** — never raw JS `+ - * /` on amounts, including in test assertions.
- **Type fields by purpose**: `customerId: Customer["id"]`, not `customerId: string`.
- **No backwards-compat shims** during a rename — rename across all consumers.
- **Never log-and-continue** a step something downstream depends on. Throw, retryably.

### Errors and results

- Service methods return `Result<T, ServiceError<"CODE">>`. Functional errors (bad input, business rules) return `err({ code, message })`; non-functional errors (DB down, network) throw.
- `get` returns `T | null` — "not found" is never an error.
- Error codes are specific: `PRODUCT_NOT_FOUND`, never `NOT_FOUND`.
- Controllers must not use catch-all error handling — a service's error-code change should cause a type error.

### Before adding N of something

Read [docs/ENTROPY.md](ENTROPY.md). It is short, and it is the doctrine that decides design review here.

> Does handling case N+1 mean **touching one site, with the compiler naming what's missing** — or **find-and-match across N sites**?

The three shapes that reliably get rejected: per-entity wrapper components, helper factories that manufacture boilerplate, and two components that differ by chrome rather than behaviour. Find the axis of variation and parameterise it. A comment reading "kept in sync with X" marks drift being held back by hand.

---

## 4. Traps that have cost real hours

| Symptom                                                    | Cause                                                                                                                      |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Search output looks redacted or substituted                | You typed `rg -r`. It is `--replace`, not recursive.                                                                       |
| `typecheck` passes on a package that is actually broken    | An Nx cache hit. `typecheck` is `bun check`, which reads no `tsbuildinfo` or built `.d.ts`; re-run with `--skip-nx-cache`. |
| A CI task line reads `Remote Cache Hit`                    | It never compiled. Treat a green cached task as unverified, not as evidence.                                               |
| A package you never touched fails to typecheck             | Probably another agent's in-flight edits in the shared worktree. Check `but status` before calling it a latent bug.        |
| Post-edit IDE diagnostics report phantom errors            | They often reflect a pre-edit snapshot. `nx run <pkg>:typecheck` is ground truth — verify before acting on them.           |
| `bun fmt --check` fails after a `jq`/script edit           | Those tools re-serialise in their own style. Run `bun fmt`.                                                                |
| SQL / Markdown / YAML formatting drifts                    | `bun fmt` formats all of them (`.sql` via `prettier-plugin-sql`). Run it at the repo root.                                 |
| A repo-wide `nx affected` gate goes red on unrelated files | Concurrent work in the same worktree. Scope verification to your own packages; never reset the worktree.                   |

### One formatter, one linter

- **`bun fmt`** formats every supported file type, `.sql` through `prettier-plugin-sql` → `bun fmt` (write), `bun fmt --check` (gate)
- **`bun lint`** lints JS/TS, configured in `bunfig.toml` `[lint]` → `nx affected -t lint -c fix`
- There is no ESLint. `bun fmt` needs the Bun in `.bun-version` (1.4.4). Stable 1.4.4 has no `bun lint`, so CI lints on a Bun canary and locally you need one (`bun upgrade --canary`).

---

## 5. Before you push

CI is the gate — there are no local pre-commit hooks.

| Check       | Command                                                           |
| ----------- | ----------------------------------------------------------------- |
| Lint (code) | `nx affected -t lint -c ci --base=main` (`-c fix` to autofix)     |
| Format      | `bun fmt --check` (or `bun fmt`)                                  |
| Typecheck   | `nx affected -t typecheck --base=main`                            |
| Ratchet     | `nx affected -t strict-ratchet --base=main`                       |
| Tests       | `nx affected -t test --base=main --exclude=tag:nextjs,tag:lambda` |
| OpenAPI     | `nx affected -t gen-specs --base=main`, then `-t validate-specs`  |

Use `bun nx …` if `nx` is not on your `PATH` — workspace binaries run through bun here, and `bun install` is the only install command.
