Repository Operations
Commands, Git workflow, verification tiers and gates, Convex environments
Agent rule sheet: current rules only, in the imperative. The reasoning and history behind each rule live in the factory log; read it when a rule surprises you, not before work.
Commands
Tool setup and command inventory
- Fresh checkout:
bun installfirst. Cloud sessions do this in a hook (.claude/README.md). - Use the package scripts, never raw binaries (
bun run steiger, notbunx steiger ./src).
bun run dev # app on :3000 with the managed Convex process
bunx convex dev # Convex alone
bun run storybook # Storybook on :6006 (manual); :6007 is the managed preview
bun run verify:changed # default finalisation: selected gates for the current change
bun run verify:changed --list | --thorough | --keep-going | --backend | --base <ref> | --files <paths>
bun run verify:changed --keep-going # every gate, all failures reported at the end (cloud sessions)
bun run test:changed # only the stories the change can have broken
bun run test:unit # Node + Convex unit tests, no browser
bun run test:stories [path] # browser stories (accepts a path)
bun run story:shot <story-id> [--measure <css>] # screenshot one story with the bundled Chromium
bun run test # full pre-push gate
bun run biome | biome:fix # lint/format check | fix
bunx tsc --noEmit # app TypeScript
bunx convex dev --once # Convex typecheck, codegen and push
bun run docs:trace # plan/docs/code traceability
bun run preflight --close # local close check, no upload
bun run factory:ledger # how heavy the rulebook and ceremony are (report)Only run what the change can have broken
- Verify once, at landing. Batch related edits; do not restart typecheck, unit or browser suites after each visual adjustment. An intermediate run is for diagnosing an unknown failure or owner-requested test-first work only.
- Multi-step work with no visible change (a refactor, a split, a move)
takes only
bunx tsc --noEmit -p .between steps and oneverify:changed(or the full suite) at the end (owner, 2026-09-28). Briefs to delegated agents follow the same rule: never ask for a full test run before every commit. - Known environment failures are not re-diagnosed. The
storybook-advisoryproject ("Armed Ladder" among it) reports without blocking. It does not count against a landing; any other red does. bun run verify:changedis the finalisation command. It reads the staged, unstaged and untracked change, prints the tier, the selected gates and the story list, then runs them.--listpreviews without running.- Freeze the tree while it runs. The gate fingerprints the candidate at
the start and refuses a result for a tree that moved, naming what moved.
Editing a file, staging, or filing a task (
task:newwrites underbacklog/) during the run costs the whole run again. Finish every edit and file every task first, then run the gate once; the candidate is then exactly what you commit. The gate's own records (gate-times.jsonl,standing-reds.json) are excluded and never count as drift. - Tiers, by blast radius. The script decides; the table says what follows.
| Tier | What qualifies | Stories at landing | Review |
|---|---|---|---|
| cosmetic | frontend, story or docs files only; ≤8 source files; ≤12 stories reached; nothing under src/app/ and no shared brick | the reached set | none required; gate output is the record |
| shared-ui | frontend only, but a shared brick or more than 12 stories reached | nearest 12 by import distance; the rest with --thorough (default in a cloud session) | on request, or when a reusable contract changed |
| cross-cutting | backend, domain package, config, dependencies, src/app/, more than 8 source files, or a selection that fell back to all stories | all affected | fresh-context settlement review required |
-
A shared brick is anything an outer layer can reach, which Feature-Sliced Design makes four layers and not one folder:
src/shared/,src/entities/,src/features/,src/widgets/, plus any slice's published interface — a<layer>/<slice>/index.tsor a file in its@x/door — wherever the slice sits. A Page's internals are the one exception: a Page has a single consumer, the router, and a change to the router is caught by thesrc/app/row below, which is stronger.In plain terms: a standard clause is one other documents rely on. Which folder it is filed in does not change that.
This read
src/shared/alone until 2026-09-14 (GB-266), which is how GB-264 moved a panel between Widget slices and added a public export under the cosmetic tier, taking no fresh-context review. Change this row andclassifyTiertogether:verify.node.test.mjspins each layer named here. -
Story selection follows imports. A deleted file selects nothing (TypeScript proves nothing imports it). A CSS edit selects one smoke story per layer plus colocated stories. An unresolvable dynamic import selects all.
-
Frontend edits run
vitest relatedon the Node project only; the Convex project never importssrc/. -
Worker counts calibrate from memory and cores;
VITEST_MAX_WORKERSoverrides. Never enlarge a test deadline to make a starved run pass; compare worker counts and isolated runs first.test:gentleruns the full suite at low priority on an in-use machine. -
The full story suite (8–16 minutes) is a pre-push gate and belongs in a cloud session, never an edit-loop habit. Give the story project a path.
-
Backend gates (
convex dev --once,convex:generated) run only with--backend; without it the command reports them outstanding and exits 2. Read Convex development environments before opting in. -
Every run appends timings to
tmp/verify-runs.jsonl; the ledger reports the median per tier. -
Current limits: selection is conservative and file-based, including barrel re-exports. The eight-file threshold counts changed
src/paths, including stories and public exports; crossing it runs all affected stories locally. The command neither dispatches cloud jobs nor reuses successful gates from an earlier run.--listreveals the actual selection before execution. -
Cloud results count only for the exact candidate commit and selected checks. A launched agent or a run with zero executed tests is not a passing gate. Codex cloud setup and the latest readiness evidence live in the Codex environment notes; Claude-specific setup lives in .claude/README.md.
The table below is generated from scripts/verification-rules.mjs; regenerate
it with bun run verify:changed --write-matrix.
| Change | Local checks | Explicit backend checks |
|---|---|---|
| Documentation and agent guidance: content/docs, plans, backlog Markdown; agent/skill files; .claude Markdown; root Markdown; skills-lock.json | bun run biome; bun run docs:trace | None |
| Verification tooling: scripts: verify, verification-rules, changed-files, affected-stories, test-changed, session-preflight, standing-reds, factory-ledger, factory-reading and check-… modules with their tests; package scripts only | bun run biome; bun run docs:trace; bunx vitest run --project node; bunx tsc --noEmit | None |
| Story tests: src/**/*.stories.tsx | bun run biome; bunx tsc --noEmit; bun run stories:geometry; bun run knip; bunx vitest run --project storybook (affected stories) | None |
| Frontend (including styles): src/** | bun run biome; bunx vitest related --run --project node --passWithNoTests; bunx tsc --noEmit; bun run steiger; bun run naming; bun run colour:tokens; bun run stories:geometry; bun run knip; bun run lint:shadcn (advisory); bunx vitest run --project storybook (affected stories) | None |
| Test and lint configuration: vitest.config*.ts, vitest.shims.d.ts, biome.json | bun run biome; bun run test:unit; bunx tsc --noEmit; bunx vitest run --project storybook (smoke stories) | None |
| Storybook configuration: .storybook/** | bun run biome; bun run test:unit; bunx tsc --noEmit; bunx vitest run --project storybook | None |
| Backend tests only: convex/**/*.test.ts | bun run biome; bun run test:unit; bunx tsc --noEmit; bunx tsc --project convex/tsconfig.json --noEmit; bun run knip | None |
| Backend: convex/, convex-dev/ | bun run biome; bun run test:unit; bunx tsc --noEmit; bun run naming; bun run knip; node scripts/check-convex-modules-bound.mjs; bun run goldie:instructions | bunx convex dev --once; bun run convex:generated |
| Shared domain, configuration or unknown paths: packages/**, dependencies/lockfiles, other scripts and all unmatched paths | bun run biome; bun run docs:trace; bun run test:unit; bunx tsc --noEmit; bun run steiger; bun run naming; bun run colour:tokens; bun run stories:geometry; bun run knip; node scripts/check-convex-modules-bound.mjs; bun run goldie:instructions; bunx vitest run --project storybook | bunx convex dev --once; bun run convex:generated |
Verifying UI changes
- Confirm which tree a dev server serves before trusting it:
ps -o command= -p $(lsof -ti:6007 -sTCP:LISTEN); worktrees have nonode_modules, and orphaned processes keep serving old code. bun run story:shot <story-id> --measure <css>screenshots one story with the bundled Chromium and prints the matched elements' boxes; it starts a Storybook on:6007if none is listening and stops only the one it started.- Measure the DOM (
getBoundingClientRect,getComputedStyle) and state the numbers; never diagnose layout from a screenshot. Probe the property that encodes state (aria-expanded,data-state), notoffsetParent. - Panel width is not viewport width: gate panel-sized things on the container
(
@containeror aResizeObserver);md:anduseIsMobile()are for window-level chrome only. Name a shared threshold in one constant. - Port 3000 is the app's port; identify an existing listener before touching it.
- A story run started seconds after an edit can fail in milliseconds with
Vite unexpectedly reloadedorPreload assets timed out; re-run before diagnosing.Failed to fetch dynamically imported moduleunder a running preview is a stale index: clearnode_modules/.cache/storybookand restart. - Deploy only when explicitly required:
bunx convex deploy --cmd 'bun run build'.
Which checks a cloud session may run
Story suites and their browser setup are the slowest checks, and they strain
the owner's laptop (owner ruling, 2026-09-15; GB-272). The setup for cloud
browser tests and the Convex key is in .claude/README.md;
this section only says which check goes where.
| Check | Cloud? | Why |
|---|---|---|
Story suites (vitest --project storybook) | Yes, when they are the slowest step | They need no live backend. Push first: a cloud session reads the remote, not your working tree. |
Backend checks (verify:changed --backend, convex dev --once) | Not by default | The cloud can reach the dev deployment, so this is about permission, not ability: a push to the shared dev deployment needs the owner's go-ahead. |
| The owner's live walk-through | Never | It needs a browser on the live Convex connection, which cloud sessions do not carry, and it has caught defects no suite did (GB-269). |
A cloud run counts as evidence only if it names the commit it ran against. A green run on an older commit proves nothing about the current tree.
In plain terms: the slow browser tests can go to a cloud clerk; the database checks need the owner's say-so, and the owner's own walk-through stays local. A clerk's report only counts if it says which version it checked.
Git workflow
Local session startup sync
Every local session, resumed work included, before diagnosis, dev servers or edits (a user request to inspect history or stay read-only takes precedence):
git status --short --branch, thengit fetch origin. If the fetch fails, report freshness as unverified and do not start dependent work.- On a clean
main:git merge --ff-only origin/main, without asking. Never autostash, reset, force or fall back to a merge/rebase. - On a feature branch or worktree: keep it. Fast-forward only when clean and
HEADis an ancestor oforigin/main; a diverged branch needs a deliberate integration step. Never move another worktree's branch. - If local changes or divergence block a safe sync, report the exact state, preserve it, and get direction before dependent work.
- Confirm
git merge-base --is-ancestor origin/main HEAD, report the commit, and re-read the task against the updated tree.
Working branches and landing
- Commit directly to
main; branch only for speculative or deliberately incomplete work. Parallel work runs in isolated worktrees only. - Running stories in a worktree (
.claude/worktrees/<name>): runbun install --frozen-lockfileonce inside it (about two seconds; bun links from its cache), thenbunx vitest run --project storybook <story file>. Without its ownnode_modulesthe browser cannot load packages from outside the worktree folder, and every file fails withFailed to fetch dynamically imported module … setup-file.js. A symlinkednode_modulesdoes not work: Node follows the link back to the main checkout.vitest.config.ts(setupFilesFromThisCheckout) covers the other half: Vitest looked up Storybook's setup files starting one folder above the worktree, so it found the main checkout's copy first. Fixed and proven under GB-307. - Stage explicit paths; never
git add -Aorgit add .. Checkgit diff --cached --statbefore committing. - Reconcile on landing: every pushed commit leaves its task and any authoritative plan accurate as at that commit.
- Remove out-of-scope edits before committing. Finish with
bun run preflight --close. Confirm a push by reading the remote (git rev-parse origin/<branch>), not from the command's output.
Concurrent sessions
Several sessions share this checkout; treat the tree as unstable within a turn.
- Never
git stash,git checkout --,git reset --hardorgit clean. Inspect withgit diff,git diff HEAD -- <path>,git show <ref>:<path>. - Re-run
git status/git diffimmediately before a write, not only at start. - Before claiming a plan or handoff step, check whether its files are already modified: another session probably took it.
- Scope gate claims to your own files; a full-repo run reflects everyone's in-flight work.
- Give delegated agents a keep-out list and have them stop on conflict.
Verification gates
-
Keep Biome and app TypeScript at zero errors.
tsc --noEmitis not the backend gate: onlybunx convex dev --oncetypechecksconvex/**/*.test.tsand proves functions push. -
Biome formats only what this repository writes; tool-written files stay in
biome.jsonexclusions. Never leave a tool-written file inside its reach. -
preflight --closechecks local state only: bindings, instruction constants, strict traceability, task authorities and dangling references. -
docs:tracevalidates anchors as well as routes: a link to/docs/<page>#<heading>fails when that page has no such heading. Follow a renamed heading to its callers, or keep the old anchor alive with an explicit<span id="…" />on the page. -
Dangling references cover code comments and
content/docsalike: a backticked path that does not resolve in this tree. Fix each one. Two honest exits, both by wording rather than by an exclusion list:- name the citation as external — "the corpus
dsl_spec.md", a branch, a dependency; - name it as history — a path a docs page marks gone, removed, retired, deleted, superseded, no longer, formerly, replaced or since moved in the same paragraph or list item is a record, not a pointer. A decision record may say what a file was; it may not imply the file is still there. The marker is per unit, so one forgiven bullet never forgives the next.
- name the citation as external — "the corpus
-
Backend work runs
verify:changed --backend;convex:generatedfails only when codegen would change a binding. Stage regenerated bindings with their change. -
A bug fix records red/green: observe the regression test fail with the fix reversed, then pass. A broken import is not evidence. The TDD skill owns test design.
-
A refactor records what it preserved. Moving behaviour needs the mirror of red/green: name the behaviours the move carries, and show a test that fails if one is dropped. Testing the moved part is not that test — a helper passes its own unit test perfectly while nothing calls it any more.
In plain terms: when a clause moves to a new agreement, it is not enough to prove the clause still reads correctly. Prove the new agreement still does what the old one did.
This is the gap no tool covers. Biome, TypeScript, Steiger and the dead-code survey all read a helper with a passing test as live code; so did every gate on 2026-09-14, when moving Project Home onto the shared screen dropped the navigation that opens a newly created Document and left
blankDocumentNavigationand its green unit test behind (GB-50.1, found by settlement review on 2026-09-14, restored under GB-266 with the test that now fails without the wiring). Cheapest form: before moving, list what the screen does; after moving, point at a test for each line. -
A re-run is evidence only when the failure is not yours. Re-run a red story or test suite once, and only when both hold: the diff does not touch the failing file or anything it imports, and the same suite was green on the tree before the change. Then a second red is real and gets root-caused before landing. If either condition fails, the first red is real. Record which case applied in the task receipt. "Re-run until green" with no stated reason is how a real intermittent defect lands (GB-276, 2026-09-15).
In plain terms: one re-hearing is allowed when the fault is plainly someone else's file; a second failure is a finding against you.
-
A gate a landing left red is named at the next session start, and cannot ride through another landing (GB-291, 2026-09-17):
verify:changedrecords the verdict of every-landing gates (knip, Biome, app TypeScript) inbacklog/standing-reds.json;preflightreports each one still failing with the commit it was first seen red at, andpreflight --closerefuses until it is fixed or an open Backlog task labelledstanding-redcarries a linestanding-red: <gate>declaring it.In plain terms: the landing that saw a check break writes it down, so the next person is told rather than discovering it — and nobody closes while a break is nobody's.
"Fix or record" already covered a red that was not yours. What it did not cover is the same red being recorded again and again: knip's unused
cn, a failingpreflight --close, and a project-folio story suite that hung for over 250 seconds rode through GB-287, GB-289 and GB-288, each landing paying to re-diagnose all three and then writing a paragraph saying they were not its own. The hang was the expensive one — the blocking story gate could never finish, so three further failures (GB-292) were invisible for as long as it stood.The landing writes and preflight only reads, which is deliberate: preflight stays local and spawns nothing, the boundary
session-preflight.node.test.mjsrecords, and the gate costs a session start nothing at all. The ledger is committed, because the memory has to outlive the session that wrote it — a fresh clone is exactly where a red gets re-diagnosed for the fifth time. It holds a gate only while that gate is failing, so a healthy tree carries{}and the file moves only when a gate crosses between red and green. Declaring a gate, rather than mentioning it, is what makes a task its owner: a task that names a gate in passing, or to say it does not own it, must not silently acquire the red. -
Dead-code survey, a gate on every code change since 2026-09-14 (GB-266):
bun run knip. Slice barrels, shared's two segment barrels and@xfiles are entry points, so a slice's published API counts as used and the survey reports only what no entry reaches (GB-179, owner's choice, 2026-09-13).It was optional until 2026-09-14. What changed is not its power — it would have caught none of the defects the container-screen review found, because an unused property is not an export, and a barrel makes a page's exports count as used. What changed is the arithmetic: it costs three to five seconds depending on load, it has no findings to clear, and an optional check is one nobody runs. A gate that is free and silent is cheaper to run always than to remember. GB-179's entry-point trade is unchanged, and so is its ruling that the strict variant below stays a sweep rather than a gate.
The trade is deliberate and one case is left uncovered: a component still listed in a barrel but imported by nothing reads as used. Nothing else catches that on its own — the story-tag sweep reads story files, so a component with no story is invisible to it too. Catch it by running the survey with those barrels out of the entry list and reading what appears; that is a periodic sweep, not a gate. A component's own folder barrel is deliberately not an entry, so a file orphaned from its segment barrel is still reported.
-
Component-styling lint, advisory (GB-358, owner ruling 2026-10-07):
bun run lint:shadcnruns@shadcn/lintthrough oxlint (.oxlintrc.json) on everysrc/change with three rules at warning level:no-restyle(a call site overriding a shared component's own styling; layout is always allowed, and Stack-family components may also take spacing and colour),no-unknown-classes(a class Tailwind does not know), andrequire-static-classes(class lists assembled at runtime). It never fails verification: the script exits 0 andverify:changedtreats the leg as advisory. Every other rule, arbitrary values included, is off. Rules shown accurate after review will be promoted to blocking one at a time.In plain terms: a second reader now flags house-style departures in the margin, but cannot yet hold up the filing; once we trust its notes on a point, that point becomes binding.
Corpus changes
Before touching the legal corpus, compile and format a clean baseline and hash
the seed (bun scripts/compile-legal-zebra-seed.mjs && bun run biome:fix).
Make structural and semantic changes separately; the generated diff must hold
exactly the intended change. Finish with bunx convex dev --once, a reseed,
the relevant Convex suite and bun run eval:interview.
Convex development environments
- Development (
bunx convex dev) and production (bunx convex deploy, explicit only) are separate projects. Neverconvex deployin ordinary development. - Deployment secrets live on the deployment;
bun run devrestores them from the gitignored.env.dev.secrets(copy from.env.dev.secrets.example). - The dev deployment is shared by the laptop and every cloud session; every push replaces the last. Check this first when cloud and laptop disagree.
- Post-hoc diagnosis reads an export (
bunx convex export --path /tmp/convex-export.zip), not the live log tail. - Cloud sandbox mechanics (deploy key, browser revision, hooks) are in
.claude/README.md.
Required reading by task type
AGENTS.md routes reading by scope and phase; the ledger measures this
manifest. Profiles are bounded routine cases (docs correction, isolated story,
existing component adjustment, existing query/mutation). Task, product
contract, skill and naming reading is additional and never waived. Use
bun run factory:ledger --reading path[#heading] to include supplements.
Profiles name sections, not whole pages. A rule page's remaining sections are conditional reading: procedures for work most changes never do (promoting upstream source) and value tables that are looked up rather than read (the Tailwind token reference). AGENTS.md scope gates name the trigger for each. Conditional does not mean optional — it means read on the trigger, not on every change.
| Profile | Phase | Required sections |
|---|---|---|
| common | startup | router, entrypoint |
| common | before-code | planning tier, tool setup, startup sync, concurrency |
| docs | before-code | — |
| story | before-code | lifecycle and tags, sidebar titles, assertion discipline |
| frontend | before-code | page router, folder layout, landing zone, class order, token rules, typography, shadows, responsive, extraction, cn and cva, spacing and hit targets, opacity, named groups |
| convex | before-code | validators, registration, calling, references, queries, mutations, schema, TypeScript, schema, functions, scope |
| common | landing | selection, gates, branches and landing, risk review, ceremony |
| story | landing | UI verification |
| frontend | landing | UI verification |
| convex | landing | review, reviewer, environments |