Ask AI

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.md and CLAUDE.md are pointers to Agents: start here and hold no rule of their own: CLAUDE.md is an @import of that page, so Claude Code loads it; AGENTS.md names 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:status prints 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 run lists them, in whichever repository you are in; an app has the same ones. Owner, 2026-09-26, after an app was upgraded by hand past npm 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, searching node_modules or 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:

  1. List the realistic candidates, including newer ones found on npm and GitHub.
  2. Score them against fixed, weighted criteria, with a source for each score.
  3. Name the runner-up and what would make us switch to it.
  4. 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:

  1. Scaffold its own template and run its own generators (CLI features) in a scratch app.
  2. Build it and request every route it makes; list what works and what does not.
  3. 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).
  4. 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 from npm run ui:theme (shadcn's default Nova style, neutral, Geist: no preset, no branding of our own). npm run ui:verify fails the release on any hand edit. The repository uses shadcn's monorepo layout, so shadcn add run 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 with shadcn 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), --all only 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.

LibraryStatus hereRule
Start, Router, QueryIn use everywhereThe app, its pages and all data fetching (with oRPC)
FormIn use: the reservation form onlyEvery form uses it (login, signup, settings); no hand-rolled form state
PacerNot in use (the docs search is Fumadocs')Any debounce, throttle, rate limit or queue in the browser
DevtoolsIn use in developmentKeep the Router, Query and Form panels in the one Devtools
TableNext: with the admin screensEvery list with sorting, filtering or paging, through shadcn's data table
VirtualWhen a list gets longLists of hundreds of rows, starting with the time zones
DB (beta)Not yetOnly if we need offline use or live sync; Query covers today's needs
AI (beta)Not usedThe 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 yetCandidate for a search shortcut once it leaves alpha
Store (alpha), ChartsNot neededNo 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>.md in docs/content) or another locale's catalog.
  • Translation is its own step, on main after the merges: dev:land runs remy i18n translate there, last, when something is stale; nobody runs it by itself. The Claude agent, pinned in the task, translates exactly what remy i18n check lists, 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:land then says the translation is still owed, and the next landing tries again. tests/unit/i18n-docs.unit.spec.ts holds 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's null was 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 check only reads, offline, so it is safe anywhere, in any number of worktrees, and as a depends. remy i18n translate is never a depends.
  • Stale or missing is a warning while pumping (remy check prints it) and an error at release (dev:release runs remy i18n check with I18N_STRICT=1 first).
  • 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 as en.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.

StepCommandWho runs it, and whenWhat runsTime
Looknpm run dev:statuseveryone, first: every agent session opens with it (a hook), a developer when they sit downmain'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.mdseconds
Before a piece of worknpm run dev:start -- <name>whoever does the work, for every piece, a one-line edit includeda worktree of its own from main, installed, its own ports, the guard~1 min
After every changenpm run dev:changewhoever is coding; the only step waited forthe 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 machinenpm run dev:land -- "<what changed>"whoever did the work, on their own, as soon as it is finished and the check is greenthe 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
Productionnpm run dev:promotethe owner, or an agent the owner asked in that sessionremy cloudflare deploy and the docs Worker, from a pushed main, no gate: staging and GitHub already ran the checks~1 min
A releasenpm run dev:releasethe owner, or an agent the owner asked in that sessionremy packages release: every check, every language, locally, on purpose, then the tag~5 min
When landednpm run dev:donewhoever landed, when the piece of work is over; a worktree lives through as many landings as the work takesthe worktree and branch goseconds
  • 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) refuses remy 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 bare playwright test and remy i18n translate outside the flow, and names the step instead. Every one still runs by hand from a terminal, with REMY_FLOW=hand in 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=hand in front or named without npm run; tests/unit/dev-flow.unit.spec.ts reads every English page and every task for one run bare, on every check (2026-10-10: eight lines on six pages did, and npm run setup ended 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"). Only dev:change (seconds) is run and waited for.

  • Every step says how long it took. dev:land ends 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:change starts by saying what the last run on main did; dev:promote refuses a commit whose run is not green or not finished. To reproduce a red job, a person runs REMY_FLOW=hand npm run test -- <level>, the same command GitHub ran, since the workflow holds no logic of its own (remy verify tooling checks 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:land still 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 grep or tail in a chain: the pipe hides its exit code. This once released a version whose checks had failed, and on 2026-10-10 hid a failed dev: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 remy command 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 shots to look. (The owner's words above say mise, which the scripts replaced on 2026-10-11; the rule is the same.) No curl loops, 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.md to 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, a scripts/*.ts of that app, never bash logic or untyped JavaScript: what the compiler cannot see, an agent misses (owner, issue #9). A one-line tool is a package.json script (the plan).

Branches: short-lived, deleted after merge

  • Work happens on branches in git worktrees (one per agent); they merge into main and are deleted, with their worktree, right after the merge. Only main and 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: main and 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:start makes the worktree.
  • Each worktree has its own PREVIEW_PORT and DOCS_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=2 for 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

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.

CommandWhat it does
Looknpm run dev:statuswhat 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
Startnpm 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
Codenpm run dev:changethe check, in seconds, after every change
Landnpm 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
Finishnpm run dev:donethe landed worktree and branch go; refuses while anything is unlanded
Productionnpm run dev:promotefrom 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:land sees 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:promote or a GitHub run; it starts them in the background and acts on the result. Only dev:change is waited for.
  • Everything asynchronous comes back to everyone (the plan): a red GitHub job writes on the commit, naming itself; dev:land says when main is red; dev:promote writes on the commit what went live; dev:status shows all of it to whoever asks, and every agent session asks first.

On this page