Most contributors work with AI assistants. This file (AGENTS.md) is the
canonical, tool-agnostic source of truth; each assistant's own instruction
file is a thin pointer back here so guidance never drifts. When adding durable
guidance, put it in AGENTS.md (or docs/ for deep topics and link it) and
keep those pointer files thin. Per-app specifics belong in that app's
AGENTS.md.
Deeper guidance lives in docs/ — scan the directory and read a doc's
intro to judge relevance. Two are not optional: read
docs/AI_GUARDRAILS.md and
docs/AI_DEVELOPMENT_GUIDE.md before
security-, auth-, or reliability-sensitive work. Read the relevant
specs/ file before feature work — specs are the source of truth for
what a feature does, who may do it, and how it's verified.
Reusable agent skills (procedural runbooks in the
Agent Skills SKILL.md format) live in
.agents/skills/.
- Deployable apps live in
apps/, shared code inpackages/, config intooling/, and Turbo generators inturbo/. packages/health/package.jsonuses source-first entrypoints for monorepo workspace consumers and apublishConfigoverride for publisheddistartifacts. Do not removepublishConfigunless the workspace build orchestration guaranteesdistartifacts exist before any consumer resolves the package.
- Cross-platform: All shell scripts use
#!/usr/bin/env bashand are tested on macOS and WSL 2 (Ubuntu). Windows developers must use WSL 2 — do not run scripts in native Windows shells (cmd, PowerShell). Never write macOS-only commands (brew,open,launchctl) or Windows-only paths without a Linux fallback. - Node and pnpm are managed via NVM. The pnpm binary lives at
~/.nvm/versions/node/$(node --version)/bin/pnpmunder the currently active Node version. Ifpnpmis not onPATH, prepend that directory toPATHor run. ~/.nvm/nvm.sh && nvm usebefore running pnpm commands. - The recommended local dev environment uses Docker. Run
pnpm local:setuponce after cloning; see docs/LOCAL_DEV_DOCKER.md for setup instructions covering both macOS and WSL 2.
- First-time setup:
pnpm local:setup— copies per-directory.envfiles, starts Docker services, runs migrations, and seeds the database. See docs/LOCAL_DEV_DOCKER.md for the full guide. - Docker services:
pnpm docker:upto start (Postgres, Adminer, GCS emulator, Mailpit),pnpm docker:downto stop. - Each app and
packages/envhas its own.envfile (copied from.env.examplebypnpm local:setup). Never commit.envfiles. - Code quality: always run
pnpm lint:fixandpnpm format:fix(for the whole repo — or filter to a certain app/package) to ensure your code passes all lint and formatting checks. Also runpnpm typecheckto validate types. pnpm lintdoes not cover dead-code detection: CI'slintjob runspnpm lintandpnpm lint:unused(knip) as two separate steps, so runpnpm lint:unusedas well before pushing.pnpm ci:localchains the whole CI sequence (format→lint→lint:unused→typecheck→build→test) and is the closest local predictor of the CI gate.- Database helpers:
pnpm db:pull,pnpm db:push, andpnpm reset-test-db. - Every other build/dev/test command is a standard Turborepo invocation — see the root
package.jsonscripts.
- Use Prettier (
@acme/prettier-config) and ESLint (@acme/eslint-configbase/next/react) as the source of truth. - Name React components in PascalCase, prefix hooks with
use, and use kebab-case for files/directories (e.g.,apps/map/src). - Co-locate feature-specific assets and tests near their sources (e.g.,
apps/map/src/app/(feature)/).
- Never
console.*. Use thelog*helpers (logTrace…logFatal) from@acme/logger, imported via the app'slib/logging. Reserve the rawloggerfor request-scoped children (logger.child({ requestId })). - Signature is
(event, ctx, err)—eventfirst, unlike pino's native methods.eventis a fixed dot-namespaced literal (<area>.<feature>.<outcome>,snake_casesegments); per-occurrence data goes inctx:logError("api.rpc.handler_error", { orgId }, err). - Never log secrets or PII.
- Details:
docs/LOGGING.md(primer,LOG_LEVEL, event naming),packages/logger/README.md(API reference),docs/AI_DEVELOPMENT_GUIDE.md(what counts as sensitive).
- Pin third-party actions to a full commit SHA with a version comment (e.g.
actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2). SHAs are immutable — a semver tag can be force-pushed, a SHA cannot. Renovate (pinDigests: true) keeps the SHAs up to date automatically. - Drive the Node version from
.nvmrcviaactions/setup-node(node-version-file: .nvmrc) —.nvmrcis the single source of truth. Never hardcodenode-version:in a workflow. - Set the Docker target platform at build time, not in the
Dockerfile. Cloud Run only runslinux/amd64. The appDockerfileFROMlines must not pin--platform(BuildKit'sFromPlatformFlagConstDisallowedlint, and it forces emulation on arm64 dev machines). Instead pass the platform at the build invocation:platforms: linux/amd64ondocker/build-push-action(CI) and--platform=linux/amd64ondocker build(deploy). Building a deployable image locally on Apple Silicon therefore requires an explicitdocker build --platform=linux/amd64 …. Do not switch to$BUILDPLATFORMcross-builds —sharp's native binaries are platform-specific and would break in the amd64 runtime. - Share toolchain setup through the composite action
.github/actions/setup(pnpm + Node + pnpm-store cache + frozen install) instead of repeating setup steps per job. - The five CI check names (
format-check,lint,typecheck,build,test-coverage) are referenced by themainbranch ruleset and bycheck-regexpin the deploy workflows — renaming a job requires updating both.
- Use Vitest for unit and integration tests; name test files
*.test.ts[x]and place under or near source code or in__tests__. - Reset databases before any suite that mutates data (
pnpm reset-test-dborpnpm -C packages/db reset-test-db). - Prefer fixtures in
apps/map/testsorpackages/*/__mocks__instead of live service calls. - Coverage thresholds ratchet upward.
vitest.config.tsbeing rewritten by a test run is expected — commit it. Never setthresholds.autoUpdatetofalseor delete the key, and never lower a threshold to make a suite pass; add tests instead. Enforced inpnpm lintandpre-commit. How coverage is measured and why:docs/testing.md.
Apps that require sign-in (e.g. apps/map, apps/me) authenticate via apps/auth, which uses email-based MFA. No real inbox is involved — outbound mail is caught by Mailpit (http://localhost:8025, started by pnpm docker:up), and agents drive the full sign-in flow headlessly by reading the 6-digit code from its REST API.
The full recipe lives in apps/auth/AGENTS.md. Assistants that scan nested AGENTS.md files pick it up on their own; Claude Code reaches it through the @AGENTS.md import in apps/auth/CLAUDE.md.
This repo enforces Conventional Commits via commitlint + Lefthook. Every commit message must follow:
<type>(<scope>): <subject>
Scope is required. The Lefthook commit-msg hook will reject commits that omit it or use an unrecognized scope.
Use standard Conventional Commit types: feat, fix, chore, docs, style, refactor, perf, test, build, ci, revert.
The allowed scopes are the scope-enum array in
commitlint.config.mjs — read them there rather than
from a copy that can drift. When adding a new workspace, add its scope to
that array.
Multiple scopes are allowed, separated by a comma (,), a slash (/), or a
backslash — e.g. feat(map,api): add beatdown filter. Every scope in the list
must be in the enum; one unrecognized entry rejects the whole message.
Choosing a scope:
- Use the app or package the change primarily affects (e.g.,
fix(db): correct migration) - Use several when a change genuinely spans workspaces:
refactor(ui,map): extract shared card - For dependency updates:
chore(deps): bump next to 15.1 - For CI/GitHub Actions:
ci(ci): add deploy workflow - For root config, monorepo tooling, or multi-package changes:
chore(repo): update turbo pipeline - The
mainscope is reserved for Release Please's own release commits — never hand-write it.
How a commit type drives version bumps and changelogs — including the
non-obvious rule that Release Please attributes a commit by which files it
changed, not by its scope — is documented in
docs/RELEASE_PROCESS.md.
- Every pull request should:
- Include a clear summary, any related issue(s), commands run, and impact to DB/env.
- Add screenshots or screen recordings for UI changes in
apps/map. - Highlight any new migrations or environment variables.
- Never include secrets.
- Before opening a pull request, ensure both
pnpm lintandpnpm formatpass with no errors or changes required.
- Store all secrets in per-directory
.envfiles (one per app andpackages/env). Always usewith-envhelpers to load environment variables and never commit.envfiles to the repo. - Scope Sentry/analytics keys per environment and rotate if leaked. Run production DB changes only through scripts in
packages/db.