How we work
How people and agents work here: project tools first, surveys before tool choices, shadcn and TanStack, gates, plans and translations.
This document owns how people and agents work: the habits the owner has asked for, for developers and AI agents alike. What the code must be lives in development principles.
Where rules live
- Write down how we work here, in the repository, not in an agent's private memory. Anyone opening the repository, person or agent, should find the same rules. Agent memory is only for things that concern one agent, never for how the project works.
- Each fact has one home, as development principles require; that applies to these rules too.
- The root
AGENTS.mdandCLAUDE.mdare pointers to Agents: start here and hold no rule of their own:CLAUDE.mdis an@importof that page, so Claude Code loads it;AGENTS.mdnames it, for Codex and every other tool. A rule written into either is in the wrong place (owner, 2026-10-10: "Agents.md and Claude.md in the root must just be a pointer to docs area for agents").
See something: fix it, or write it down, in the same landing
Owner, 2026-10-09, after finding a gap each time he asked: "It's rather clear that the agent docs are wrong if you're seeing issues and not acting on them and informing yourself and devs." So the rule is general, not only for missing tools:
- An agent or developer who notices anything wrong, slow, missing or left over (a step that waits
for nothing, a check nobody runs, a branch nobody owns, a doc that says what is no longer true) has
two choices, both inside the landing they are on: fix it when it is small, or write it into
.plans/now.md("What is left, in order": what was seen, where, and what would fix it), a plan file when it is big. Mentioning it in a chat or a report alone is neither. - Everyone hears it through the repo:
dev:statusprints that list to every session (agents open with it), so what one person saw reaches all, and the next landing takes the top of it. - "Done" is the end of a sweep, not of a list. Before saying it: branches and worktrees, open
pull requests,
remy cloudflare versions,remy plans check, a search for the words the change retired, the docs the change touched. Show the sweep.
Use the project's own tools first
- Start with
npm run setup; developer tooling says what it installs. - Before doing any step by hand (installing, upgrading, releasing, deploying, translating), find its script:
npm runlists them, in whichever repository you are in; an app has the same ones. Owner, 2026-09-26, after an app was upgraded by hand pastnpm run upgrade: "you know we do not reinvent wheels". - Run commands through the package.json scripts, check the real page with the Chrome DevTools tools
(
npm run browser), and use the installed skills before reading upstream sources, searchingnode_modulesor writing one-off scripts. - Look at the real page with the browser tools before writing a test for it.
- Once the tools can answer a question, stop researching and use them.
- Say how long a step will take before starting anything slower than a few seconds. Each command's header comment says its duration and what may run at the same time; run post-deploy checks in the background and keep working.
Choose tools by survey, not by first find
Before a plan names a library or tool:
- List the realistic candidates, including newer ones found on npm and GitHub.
- Score them against fixed, weighted criteria, with a source for each score.
- Name the runner-up and what would make us switch to it.
- Prove the top two with small scratch builds.
Mark anything not checked as assumed.
Once a tool is chosen, take its shape
Bend our system to the tool, not the tool to our system. Before building on a chosen framework:
- Scaffold its own template and run its own generators (CLI features) in a scratch app.
- Build it and request every route it makes; list what works and what does not.
- Change our layout, files and what our code hands the GUI to match, keeping only what an owner rule requires (Paraglide's URLs, shadcn's UI).
- Delete whatever of ours the tool already does. A custom plugin or wrapper has to justify itself against the stock way, not the other way round.
Owner, 2026-09-26, after the Fumadocs docs were found half-custom: "It's funny how you don't realise until you're pushed!" (the Fumadocs decision).
UI: shadcn and TanStack all the way
UI is hard and never done, so we take what shadcn and TanStack have spent years getting right, and write only what they do not provide.
-
shadcn's CLI writes every component and the theme. Components come from
npm run ui:components, the theme fromnpm run ui:theme(shadcn's default Nova style, neutral, Geist: no preset, no branding of our own).npm run ui:verifyfails the release on any hand edit. The repository uses shadcn's monorepo layout, soshadcn addrun in the app writes into the shared package. -
Look for a shadcn block before building a layout or a screen:
./node_modules/.bin/shadcn search @shadcn -t registry:block. Add it withshadcn add, keep its structure, and replace only its sample data. Do not add constraints the block does not have (it is full width, so we are). -
Compose, do not restyle. Use variants and semantic tokens; follow the installed shadcn skill's rules (Separator, Skeleton, Badge, Empty, Alert, Field instead of hand-made equivalents).
-
Two kinds of page: site pages for Google and app pages under
/app, never mixed; paths.js defines both. -
Adopt the TanStack library instead of our own code (Form, Router's Zod adapter, Table, Devtools and so on), install its agent skills, then delete the code it replaces.
-
Before writing any UI code, ask: does shadcn, TanStack or Paraglide already do this? If yes, use it.
-
Look at it: before calling UI work done, screenshot the pages you changed with
npm run shots(desktop and phone, light and dark, English and Arabic) and look at the screenshots. Checks prove behaviour; only the screenshots show layout, spacing, overflow and mixed-direction text; name the pages you changed (npm run shots -- /formats --phone),--allonly for a sweep (owner, 2026-09-26: "It's really depressing how you can't see the look of the web site properly").
Which TanStack library for what
Checked against tanstack.com on 2026-09-26. Beta and alpha libraries change fast: read their current docs and installed skills before using them, not an agent's memory.
| Library | Status here | Rule |
|---|---|---|
| Start, Router, Query | In use everywhere | The app, its pages and all data fetching (with oRPC) |
| Form | In use: the reservation form only | Every form uses it (login, signup, settings); no hand-rolled form state |
| Pacer | Not in use (the docs search is Fumadocs') | Any debounce, throttle, rate limit or queue in the browser |
| Devtools | In use in development | Keep the Router, Query and Form panels in the one Devtools |
| Table | Next: with the admin screens | Every list with sorting, filtering or paging, through shadcn's data table |
| Virtual | When a list gets long | Lists of hundreds of rows, starting with the time zones |
| DB (beta) | Not yet | Only if we need offline use or live sync; Query covers today's needs |
| AI (beta) | Not used | The docs' Ask AI is its own page (/docs/ask, /dev/ask) built from Fumadocs' AI chat pieces on the AI SDK, not an overlay: one scroll, the question at the top (docs in your AI tools) |
| Hotkeys (alpha) | Not yet | Candidate for a search shortcut once it leaves alpha |
| Store (alpha), Charts | Not needed | No app-wide client state and no dashboards yet |
Moving a library from "Not yet" to in use follows choose tools by survey.
The docs and the demo move with the code
remy-auth is what every other Remy repo builds on, and its developer docs are what their agents and
developers read (through the remy skill, generated from these pages). So a feature is done when
four things are true: it is in the package for every app, remy-auth's own app uses it where anyone
can see it work, a check fails when it is skipped, and the page that describes it is updated in the
same change. Owner, 2026-10-09: "By make Remy-auth reuses all its features in its own demo it makes it
easy to onboard AI and devs ... the docs are part of what any other agent and dev in other repos get."
A page that describes what the code no longer does is a bug; fix it where you find it.
Language: Paraglide owns it
Paraglide owns all language behaviour: which language a request gets, through its strategies
url, cookie, preferredLanguage and baseLocale (set in
paraglide.mjs), and the localized links, with TanStack Router
carrying them. We do not write framework-neutral layers or our own language code; when Paraglide
lacks something, use its options first and record the gap in the owning plan.
Translations: one writer
English is the source; translations follow it, written by one writer at a time. Agents change these files all the time, so translating inside every feature branch collides and leaves languages half-updated.
- A feature agent writes English only: the English docs and the base catalog (
messages/en.json). It never edits a translation (<page>.<lang>.mdindocs/content) or another locale's catalog. - Translation is its own step, on
mainafter the merges:dev:landrunsremy i18n translatethere, last, when something is stale; nobody runs it by itself. The Claude agent, pinned in the task, translates exactly whatremy i18n checklists, and the task commits it; the commit is the mark (git decides what is stale: a translation is stale when its English changed after the translation's last commit). It refuses other branches, uncommitted English, uncommitted translations and a second run while one holds the lock, which every worktree shares, so parallel agents cannot set off translations or race on them. - The agent gets no tools: it is handed the English (and, for a stale page, the English diff and the current translation) and returns text; the task writes the files it asked for and nothing else.
- What the agent returns is written only when it is the page: every frontmatter field its English
has, and text after it (
tasks/i18n/docs/page.ts). A failed agent leaves the page as it was, says why and fails the run;dev:landthen says the translation is still owed, and the next landing tries again.tests/unit/i18n-docs.unit.spec.tsholds every translated page in the repository to the same rule, on every check and on GitHub, the only check a translation's own commit gets. On 2026-10-09 a failed agent'snullwas committed as a page and stopped the docs building on main. - A translation that is not a page is deleted, never restored by hand: a missing page is translated from the English by the next landing, and a restored one would be marked current by its commit.
remy i18n checkonly reads, offline, so it is safe anywhere, in any number of worktrees, and as adepends.remy i18n translateis never adepends.- Stale or missing is a warning while pumping (
remy checkprints it) and an error at release (dev:releaserunsremy i18n checkwithI18N_STRICT=1first). - Commit only real translation changes to a translation file: any commit to it marks it current. A
structural change (a rename, a sweep) that touches docs translations is followed by
REMY_FLOW=hand npx remy i18n translate docs <file>…, which re-checks those files against the English in full. Catalogs have no such redo: change a catalog's structure (every locale's file) in the same commit asen.json, so no changed English key is hidden.
The layout and the commands are in the command line.
Plans: few, short, closed
Owner, 2026-09-26: "how overwhelming and frustrating it is to have so much garbage plans". New work is
one line in .plans/now.md, in the order it closes. A plan file is written only for work that is big
enough to be parked or to run over weeks; research, analyses and reviews go into the plan they serve,
not a file of their own. A plan closes the day its work ships: a closing line, then .plans/done/. Big
features wait in .plans/parked/. Closing a plan needs no extra deploy or test run of its own: batch the
code into one deploy and one full run at the end.
The shared remy plans … tasks do the moves: remy plans status lists what is open, remy plans close, remy plans park and
remy plans open (a parked plan taken up again) add the line, move the file and repoint every link, and remy plans check (in the check, dev:change) keeps .plans/ tidy
(tasks).
When the owner delegates decisions
When the owner hands over decisions, for example to finish work unattended:
- Decide, and record each decision with its reasons in the plan that owns it.
- Keep every gate green; delegation never loosens a check.
- Leave a full report: what was decided, what was done, what was checked and what was not.
The flow: four steps, and a guard that refuses the rest
Owner, 2026-10-09: "95% of time has been checking and 5% coding. It can't go on", "It's a question of
what you run locally and what you run in GitHub asynchronously", and "The tooling formalises what
happens at development ... make sure the developer flow is also formalised so that this stupid
fuckups can't happen. And so you and developers use that same flow so that it's fixed forever." So
the flow is code, not a rule to remember: tasks/dev/flow.ts is the one place its decisions live.
Its four steps are dev:change, dev:land, dev:promote and dev:release (flow.ts's Step);
dev:start and dev:done bracket a piece of work; dev:status looks; a guard refuses what is not
one of them. Seven commands, nothing else. remy-auth gets it the way every app does, from the shared
tasks; npm run setup installs the guard.
| Step | Command | Who runs it, and when | What runs | Time |
|---|---|---|---|---|
| Look | npm run dev:status | everyone, first: every agent session opens with it (a hook), a developer when they sit down | main's last commits with GitHub's verdict, what each deployment runs, every worktree ahead and behind main, translations, pull requests, the top of .plans/now.md | seconds |
| Before a piece of work | npm run dev:start -- <name> | whoever does the work, for every piece, a one-line edit included | a worktree of its own from main, installed, its own ports, the guard | ~1 min |
| After every change | npm run dev:change | whoever is coding; the only step waited for | the check (remy check): plans, types with the tasks' own, the plain-function checks (tests/**/*.unit.spec.ts), translation status; a build only when a route file changed, the docs only when docs changed | ~6 s |
| The change leaves the machine | npm run dev:land -- "<what changed>" | whoever did the work, on their own, as soon as it is finished and the check is green | the check, commit, fast-forward main, push, deploy staging, then translation last when stale. GitHub then runs every language, Google's audits and the consumer fixture in parallel, while you keep coding (gh run list) | ~1 min to staging; translation after |
| Production | npm run dev:promote | the owner, or an agent the owner asked in that session | remy cloudflare deploy and the docs Worker, from a pushed main, no gate: staging and GitHub already ran the checks | ~1 min |
| A release | npm run dev:release | the owner, or an agent the owner asked in that session | remy packages release: every check, every language, locally, on purpose, then the tag | ~5 min |
| When landed | npm run dev:done | whoever landed, when the piece of work is over; a worktree lives through as many landings as the work takes | the worktree and branch go | seconds |
-
Who runs a step is in the table, and it is never a question. An agent lands its own finished work without asking: staging is part of landing, GitHub checks every landing, and nothing reaches production by it. The owner's word is for production and a release, the two steps whose column says so, and for provisioning (development principles). An agent that reads the table and still asks whether to land has stopped using the flow (owner, 2026-10-10, after one did: "Is it absolutely clear when each should be run by you?").
-
One area that looks wrong:
npm run test -- only <words>(the browser checks whose title matches, en and ar, ~30 s). It is the only browser run a step does not own. -
The guard (
remy guard, a Claude Code hook on every shell command an agent runs) refusesremy verify,npm run test -- every,npm run test -- quick,npm run test -- remote,npm run test -- google,npm run test -- cwv,npm run test -- consumers,npm run test -- consumers, a bareplaywright testandremy i18n translateoutside the flow, and names the step instead. Every one still runs by hand from a terminal, withREMY_FLOW=handin front (REMY_FLOW=hand npm run test -- every): the command itself asks who is running it (the gate) and refuses a bare run, on every machine. That is a person's decision; the guard refuses it from an agent even so, which is the fault it exists for. A task whose own job ends with the verification (npm run setup,npm run packages -- upgrade,npm run upgrade) says it is asking, so those run as written. -
These pages never give a command the tooling refuses. A heavy command is written with
REMY_FLOW=handin front or named withoutnpm run;tests/unit/dev-flow.unit.spec.tsreads every English page and every task for one run bare, on every check (2026-10-10: eight lines on six pages did, andnpm run setupended in the refusal). -
A rule that can be a function belongs in
tests/**/*.unit.spec.ts(no build, no Worker, no browser), so it runs on every check for free. The browser is for what only a browser shows. -
An agent never waits on a long-running step.
dev:land,dev:promote,dev:release, a translation, a remote suite: start it in the background, keep working or answer the owner, and act when the harness says it finished. Starting it in the background and then polling it in the foreground is the same fault (owner, 2026-10-09: "You just did it again for 2 long running things"). Onlydev:change(seconds) is run and waited for. -
Every step says how long it took.
dev:landends with its parts (the check; commit, main and push; staging; translation) and says how long after the start staging was up; the translator says for every agent call how long Claude Code took, how much of that was the API, and how many tokens it wrote. A slow step is a number in the log, not a feeling (owner, 2026-10-10, of a landing on staging after 40 seconds whose command ran six minutes: "how can it be 6 minutes"). -
GitHub's answer comes to you. A red run comments on the commit (GitHub tells its author), naming the run;
dev:changestarts by saying what the last run on main did;dev:promoterefuses a commit whose run is not green or not finished. To reproduce a red job, a person runsREMY_FLOW=hand npm run test -- <level>, the same command GitHub ran, since the workflow holds no logic of its own (remy verify toolingchecks every script a workflow names exists). An agent reads the run's own log (gh run view <id> --log-failed) and runs the one area (npm run test -- only <words>). Report what was tested and what was not; never call untested work verified. -
A landing that fails after the push still finishes what it can, and says where it stopped. Main is pushed before staging; when staging's deploy or its live smoke fails,
dev:landstill runs the translation (it is on main, owed regardless), then ends non-zero with the state in one line: main pushed at which commit, staging deployed or not, which smoke failed, translation done or not. Nothing half-done is left for the next person to discover (owner, 2026-10-10, after a landing stopped at the smoke with the translation silently skipped: "now you and other agents are screwed"). -
Never pipe a gating command through
greportailin a chain: the pipe hides its exit code. This once released a version whose checks had failed, and on 2026-10-10 hid a faileddev:land.
Manual work becomes scripts over real tools
Owner, 2026-09-26: "You need to get to the point that your checking uses mise and the underlying tool!" and "make sure you have something in docs about using your judgment about things that you do manually being turned into a mise task that uses a tool ... It's vital because all our repos will be using this."
- Every check runs through a script or a
remycommand that wraps the real tool: the flow's steps and the checks (npm run test, Playwright), the live smoke after a deploy,remy plans check,remy i18n check,npm run shotsto look. (The owner's words above say mise, which the scripts replaced on 2026-10-11; the rule is the same.) Nocurlloops, one-off scripts or ad-hoc greps to decide whether something works: they cannot be repeated, apps on the package do not get them, and nobody sees them later. - Use judgement on everything done by hand, not only checks: the second time you type the same commands
or reach for a throwaway script, it becomes a command of the package (
packages/ui/src/cli, so every app gets it) that calls the tool that does the job: Wrangler, Playwright, gh, npm. - No tool found? Do not write a script and move on. Add a line to
.plans/now.mdto survey for one (choose tools by survey); if the search or the change is big, write a plan in.plans/(or.plans/parked/) yourself. Agents create these lines and plans as they meet such things; the owner does not have to ask. - Anything more than one line of a tool is TypeScript: a command of
remy(packages/ui/src/cli, type-checked on every check) or, for what is one app's own, ascripts/*.tsof that app, never bash logic or untyped JavaScript: what the compiler cannot see, an agent misses (owner, issue #9). A one-line tool is apackage.jsonscript (the plan).
Branches: short-lived, deleted after merge
- Work happens on branches in git worktrees (one per agent); they merge into
mainand are deleted, with their worktree, right after the merge. Onlymainand release tags are pushed; a branch goes to GitHub only for a pull request, and GitHub deletes it when the PR merges ("Automatically delete head branches", on for every repo, 2026-09-26). - A leftover merged branch (local or on GitHub) is noise, not history:
mainand the tags hold it.
Sharing one machine between agents
The machine crashed on 2026-09-25 with about ten agents building and testing at once (load 188), and timing-sensitive checks failed well before that. So:
- An agent builds and tests only in its own git worktree, never in the main checkout: builds write
dist/, and two builds in one checkout delete each other's files.dev:startmakes the worktree. - Each worktree has its own
PREVIEW_PORTandDOCS_PREVIEW_PORT, computed from its name (never 4190, which browsers block);PREVIEW_PORT=4232 npm run …overrides them. - At most three agents run tests at the same time; research, writing and scratch spikes do not
count. When more are needed, set
PLAYWRIGHT_WORKERS=2for each. - Google's level (
npm run test -- google,npm run test -- cwv) takes a machine-wide lock, so a second run waits rather than skewing the first. - Never chain a deploy after a check with
;: the flow's steps run each in turn and stop at the first failure.
Reporting to the owner
- After every deploy, the report starts with the live URLs of what was deployed, whatever else it says
(owner, 2026-09-26: "I keep telling you ... give me the url when you deploy"): the app
https://remy-auth.gedw99.workers.dev, its staging https://remy-auth-staging.gedw99.workers.dev, and for the docs Worker its guide
https://remy-auth-docs.gedw99.workers.dev/docs, developer docs
https://remy-auth-docs.gedw99.workers.dev/dev and API reference
https://remy-auth-docs.gedw99.workers.dev/reference.
remy cloudflare deployandremy cloudflare docs-deployprint them last. - Every report about something the owner can look at gives its URLs: the live sites, the preview
(
npx remy cloudflare previewprints it) and a direct link to each page or feature discussed. - Say what was checked and what was not.
- What is live is asked, not remembered:
npm run dev:statusprints what each deployment is running and how far that is from your checkout. Say it from there, not from what you last deployed.
Many agents, one flow
Owner, 2026-10-09: "formalise the way we workflow the dev work in many agents to use the new dev flow". Each piece of work, whoever does it, is one worktree through the same commands; main is the only integrator, and GitHub checks every landing. Nothing here is done by hand.
| Command | What it does | |
|---|---|---|
| Look | npm run dev:status | what is going on, asked from the facts' owners: main's last commits with GitHub's verdict, what each deployment runs, every worktree ahead and behind main, translations, pull requests. Every agent session opens with it (a hook remy guard registers); a developer runs it when they sit down |
| Start | npm run dev:start -- <name> | a worktree on branch <name> from main (.claude/worktrees/<name>), installed (npm ci, the app's local script), with ports of its own and the flow's guard. Claude Code's own worktrees live at the same place and are only a folder: inside one, npm run dev:start with no name prepares it the same way |
| Code | npm run dev:change | the check, in seconds, after every change |
| Land | npm run dev:land -- "<what changed>" | main merged in first (a conflict stops with the files named), the check, commit, fast-forward main, push, staging, then translation last when stale (the Claude subscription on this machine, never GitHub). GitHub runs the heavy checks; a red run comments on the commit |
| Finish | npm run dev:done | the landed worktree and branch go; refuses while anything is unlanded |
| Production | npm run dev:promote | from main, only a commit GitHub passed, on the owner's word |
- Split by independence. An orchestrator splits work into parts that touch different files, and
starts one agent per part with
dev:start. A part that needs another's result waits for that landing; it does not share a worktree. - Spike first when the risk is unknown: one agent proves the risky point and lands or stops.
- Every part proves itself: it lands with its own check in
tests/, a plain function (tests/**/*.unit.spec.ts) when it can be one, in the browser when only a browser shows it. - Landings serialise themselves. Two agents landing at once: the second's
dev:landsees main moved, merges it in, checks again and lands. The translation step is one writer across worktrees. - Hands-on pass before production. Passing checks is not the end: use the piece on staging in a
real browser, on a phone's width, and fix what feels bad. Then
dev:promote. - An agent never waits in the foreground on
dev:land,dev:promoteor a GitHub run; it starts them in the background and acts on the result. Onlydev:changeis waited for. - Everything asynchronous comes back to everyone (the plan):
a red GitHub job writes on the commit, naming itself;
dev:landsays when main is red;dev:promotewrites on the commit what went live;dev:statusshows all of it to whoever asks, and every agent session asks first.
Architecture and development principles
What the code must be: ownership, a single source of truth, test gates, generated code and owner decisions.
Developer tooling reference
The developer tooling reference: the package.json scripts and the remy commands, pinned tools, secrets, agent skills and the Cloudflare services behind the docs.