Imported from DataIntegrationGroup/OcotilloUI (
AGENTS.md). Install upstream withnpx skills add DataIntegrationGroup/OcotilloUI. Copyright stays with the author.
AGENTS.md
Guidance for AI coding agents working in the OcotilloUI repository.
OcotilloUI is the admin dashboard for the New Mexico Bureau of Geology & Mineral Resources (NMBGMR) — React 18 + TypeScript on Refine.dev, built with Vite, deployed to Google App Engine.
Documentation rule: record the model
Every document generated by an AI agent in this repo must state which model produced it. This applies to ADRs, design docs, runbooks, migration notes, research summaries, and any other prose artifact an agent writes — not to source code, tests, or commit messages.
Put the attribution in a YAML frontmatter block at the top of the file:
---
generated-by: claude-opus-5
generated-on: 2026-08-06
prompted-by: jakeross
---
# ADR 0007: Replace Yup with Zod for form validation
Rules for the field:
- Use the exact model ID, not a friendly name —
claude-opus-5, not "Claude" or "Opus". - If a human substantially rewrote the document, keep the field and add
human-edited: true. - If several models contributed, list them:
generated-by: [claude-opus-5, claude-sonnet-5]. - Never remove the field when editing an existing generated doc. Update
generated-oninstead.
The point is traceability: when a doc turns out to be wrong or stale, the reader should know what produced it and be able to weigh it accordingly.
ADRs go in docs/adr/ as NNNN-kebab-title.md, numbered sequentially. Other docs go in docs/.
Commands
npm run dev # Vite dev server on :5173
npm run test # Vitest watch
npm run test:run # Vitest once — use this in CI-like checks
npm run test:coverage # Vitest with v8 coverage
npm run lint # Biome lint
npm run lint:fix # Biome lint with safe autofix
npm run format # Biome format, writes
npm run format:check # Biome format, check only
npm run check # Biome lint + format together
npm run check:fix # Biome lint + format, writes
npm run typecheck # tsc, no emit to dist
npm run build # Production build
npm run build:ci # typecheck + build, sourcemaps off, 4GB heap
Before declaring work done, run npm run lint, npm run typecheck, and npm run test:run. The Lint workflow runs lint and typecheck on every PR, and Vitest runs in its own workflow, so these catch what CI would.
The :unsafe variants (lint:fix:unsafe, check:fix:unsafe) apply Biome fixes that can change behavior. Do not reach for them to clear a warning — read the warning instead.
Cypress E2E needs a server and the mock API:
npm run mock:server:cypress # Prism mock of openapi-auth.json on :4010
npx cypress run # against :5173 locally, :4173 in CI
Do not hand-edit generated code
src/generated/ is emitted by @hey-api/openapi-ts from openapi-auth.json and is wiped clean on every regeneration (output.clean: true). Never edit types.gen.ts or zod.gen.ts — changes are silently lost on the next codegen run.
To change generated types, update openapi-auth.json (or pull a fresh spec from the API), then:
npm run openapi:generate
Commit the regenerated output alongside the spec change.
Layout
src/
pages/ Route-level components
components/ Shared presentational + container components
resources/ Refine resource definitions (ocotillo, geothermal, st2, …)
routes/ Route tree, wired to accessControl
config/ navigation, auth, storage keys, units, time, pdf
contexts/ React contexts
hooks/ Shared hooks
providers/ Refine data/auth/accessControl providers
generated/ AUTO-GENERATED — do not edit
test/ Vitest suites, mirroring the src tree
cypress/e2e/ End-to-end specs
docs/ Prose docs (see documentation rule above)
scripts/ scaffold_resource.py — generates a new Refine resource
Adding a resource touches src/resources/, src/routes/, and src/config/navigation.ts together. Navigation entries are access-control aware — a page invisible to a role is usually a navigation.ts or accessControl issue, not a routing bug.
Style
Biome does both linting and formatting, configured in biome.json. There is no ESLint and no Prettier here — do not run npx prettier or npx eslint. Without a config to find, they will happily reformat the entire file to a style the repo does not use, producing a diff that buries the real change.
The formatter settings are non-obvious in two places: no semicolons (semicolons: "asNeeded") and single quotes (quoteStyle: "single"), plus 2-space indent, 80-column width, and es5 trailing commas. Match the surrounding file; do not reformat unrelated lines.
Linter scope is narrower than formatter scope: the linter covers src/**/*.ts, src/**/*.tsx, and root *.ts only, while src/generated, cypress, dist, and coverage are excluded from both. Key rules — useHookAtTopLevel is an error; noExplicitAny, noImplicitAnyLet, noUnusedVariables, and useExhaustiveDependencies are warnings, deliberately, to allow gradual cleanup. See LINTING.md for the full ruleset and how to suppress a rule properly.
Note that production still carries the pre-migration eslint.config.ts and .prettierrc. Those files are historical; on staging and anything branched from it, Biome is the only formatter.
UI layering is documented in FRONTEND.md — read it before adding styles. Short version: MUI owns components and theming, Refine owns CRUD scaffolding and routing, Tailwind v4 is available but should not fight the MUI theme. Prefer theme tokens over hard-coded colors.
Branches and deploys
| Branch | Trigger | Target |
|---|---|---|
| feature branch → PR | pull_request |
Cloud Run preview deploy |
staging |
push | App Engine staging (CD_staging.yml) |
production |
push | App Engine production (CD_production.yml) |
Where unfinished work gets exercised
Preview deploys are the sandbox. staging is a pre-production release branch — what is on it is a candidate for production, not an experiment. Anything not ready to ship gets exercised on its own PR preview (see docs/preview-deployments.md), which can run against an ephemeral API nobody else shares.
In code, that means WIP surfaces gate on dev or preview, never on staging:
export const SHOW_WIP_FEATURES =
import.meta.env.DEV || import.meta.env.VITE_APP_ENV === 'preview'
recordsGridLogic.ts gates exactly this way. Do not add 'staging' to that check, and do not add a staging arm to a new one.
Where to branch from
Everything bases off staging and targets staging in its PR — feature, fix, chore, docs, and CI work alike. The single exception is a hotfix, which bases off production and targets production.
# feature, fix, chore, docs, ci — the normal case
git fetch origin && git checkout -b BDMS-1234-short-description origin/staging
git fetch origin && git checkout -b chore/tighten-preview-cleanup origin/staging
# hotfix only
git fetch origin && git checkout -b hotfix/v1.2.3 origin/production
Existing branch names use a mix of conventions — a BDMS-####-description ticket prefix, or a feat/, fix/, chore/, task/, hotfix/ type prefix. Either is fine; match whichever the work resembles. The base branch is what matters, not the name.
This matters more than it looks. production is the repository's default branch, so a branch cut without thinking starts there — and a PR from a production-based branch into staging drags every commit production has that staging does not into the diff, burying the actual change. If a PR shows files you never touched, this is why: rebase onto origin/staging rather than trying to resolve it in review.
Never push directly to staging or production; both are deploy triggers.
CD_staging.yml also accepts workflow_dispatch, so a staging deploy can be re-run manually without a dummy commit:
gh workflow run CD_staging.yml --repo DataIntegrationGroup/OcotilloUI --ref staging
CD_production.yml is push-only — a production redeploy currently requires a new commit on production.
PR checks: Lint, Vitest, Cypress, and PR Build Test all run on pull_request.
Secrets and environment
Env files follow .env.development.example, .env.devserver.example, and .env.production.example. Copy the example, never commit a filled-in .env.
Deploy-time secrets (GCP_SA_KEY, PostHog keys) come from GitHub Actions secrets and Google Secret Manager. Do not inline credentials, tokens, or service-account JSON into source, tests, fixtures, or docs — including as "example" values that look real.
Working agreements
- Match existing patterns in the file you are editing over introducing a new one.
- Tests live in
src/test/, mirroring the source path of what they cover. - Do not bump dependency versions as a side effect of unrelated work.
- Do not commit or push unless asked. When asked, branch off
origin/staging(see Where to branch from) rather than committing to a deploy branch. - If a task is blocked or partially done, say which parts were left out and why rather than reporting completion.