Technology

652 Lines of rules before any code

25 September 2026•8 min read
652 Lines of rules before any code

Three days, fifteen commits, a pnpm monorepo with two apps, two packages, a strict TypeScript baseline, a middleware stack and CI. I typed almost none of the implementation. I also made every decision inside it, seventeen of which are written down with reasons.

The thing that made that split work was not the model. It was 652 lines of rules I wrote before any code existed.

Most writing about coding agents argues about output quality. That is the less interesting variable. The interesting one is what you put in the agent's way before it starts, because that determines which problems reach you and which get quietly decided without you.

What the rules actually are

Six files, none of them code.

  • CLAUDE.md, 88 lines: golden rules, hard boundaries, code standards, and the checks that define "done"

  • docs/PROJECT.md: what the thing is, the module curriculum, the feature roadmap

  • docs/ARCHITECTURE.md: layout, stack, data flow, and what is still only planned

  • docs/WORKFLOW.md: the seven-step loop every feature goes through

  • docs/DECISIONS.md: settled decisions with their reasons, never edited, only superseded

  • docs/features/: one spec per feature, with a status field

The status field is the load-bearing part. Rule one:

One feature at a time. Only work on a feature whose spec in docs/features/ has Status: Approved. Never start the next feature, even if it seems obvious or small.

An agent with a roadmap in front of it will happily keep going. F002 is right there under F001, the pattern is obvious, and finishing it looks like helpfulness. The status field is what makes stopping the default rather than a judgement call.

Rule two does the same thing one level down:

Plan before code. Before editing any file, present a numbered plan (files to create or change, tests to add, open questions) and wait for explicit approval.

Both of these cost me time. That is the point. They convert "generate a scaffold" into a sequence of small decisions I get to see.

Where the ratio actually lands

Across F001 the agent escalated roughly eight decisions to me. None of them were code problems. All of them were tradeoffs with consequences months out.

The install pulled TypeScript 7.0.2, the current release. typescript-eslint declares a peer range of >=4.8.4 <6.1.0 and only alpha builds go further. So the choice was the faster native compiler, or type-aware linting that catches unawaited promises. In an ingestion pipeline an unawaited promise does not crash, it silently loses a page. We pinned TypeScript 6.

The second one was worse, because it lied. From the decision record:

eslint-config-next declares eslint >=9.0.0, which is wrong given its own dependency tree caps at ^9.7. That false declaration is why pnpm installed it without a warning, so peer ranges in this area cannot be trusted.

ESLint 10 installed cleanly, then crashed on rule load because eslint-plugin-react calls context.getFilename(), removed in 10. The latest published version of that plugin still calls it. There was nothing to override to.

The third came from the database. pgvector is not a trusted extension, so CREATE EXTENSION vector requires a superuser. The application role is deliberately not a superuser, because superusers bypass row-level security and the tenant policies would have looked like they worked while enforcing nothing. Two correct decisions, in direct conflict, discovered when a migration failed.

These are the decisions I want to be making. Nothing about them is mechanical.

The rule that pays for itself

Stop on repeated failure. If the same check fails twice after your fixes, stop and report what you tried and what you think is wrong. Do not keep patching.

When that migration failed, this is what the tool gave us:

[⣷] applying migrations...
ELIFECYCLE  Command failed with exit code 1.

A spinner and an exit code. No error text at all. The actual message only appeared after piping the SQL through psql with ON_ERROR_STOP=1:

ERROR:  permission denied to create extension "vector"
HINT:  Must be superuser to create this extension.

Without rule five, an agent facing a silent failure starts trying things. Grant something. Reorder the statements. Wrap it in a conditional. Each attempt is locally plausible and the working tree drifts. With rule five, the second failure ends the attempts and the question comes to me with the evidence attached.

Boundaries force escalation to whoever can actually act

One line in CLAUDE.md:

corpus/ is read-only. Never modify, move, rename or delete anything in it.

The corpus is 66 public tender PDFs with a CSV manifest. Before designing the schema, the agent compared the manifest against the files on disk and found the manifest covered 38 of 66. Every missing row was from one agency, and that agency holds every corrigendum, every addendum, every scanned document in the corpus. The next feature would have imported 38 documents and reported success.

The agent could not fix it. The manifest lives under corpus/, and the missing rows needed real source URLs and listing dates that only I had. So it stopped and told me, and I filled them in.

Reading the completed manifest turned up something better. This comment now sits in the schema:

These describe what a document IS, not when it arrived. They must
never be used to order a tender's documents: kphealth_antimalaria-2025-26
contains a `revised` document dated 16-January-2026 and a
`bidding_document` dated 06-February-2026, so the one labelled revised
is the older of the two.

The document labelled "revised" is the older one. Any sane-looking implementation would sort by document type or by a version string and confidently serve an outdated deadline. Ordering is by publication date alone, and the reason is written where the next person to touch it will look.

Reviewing at the atomic level

This is the part that does not compress. Every step produced a diff, and I read them.

Twice I pushed back on structure. The agent put one ESLint config at the repo root; I asked for per-app configs, because the client needs React rules that have no business touching server code. It restructured into a shared base with per-package configs, which was better than either of our starting positions.

The more useful intervention was a question, not a correction. I asked why there was a packages/ folder at all, given the client and server deploy separately to different platforms. The answer was fine: the eval runner and the MCP server will need the same retrieval code as the API, and an eval that runs different code from production measures nothing.

But answering it surfaced something else. Those packages were exporting raw TypeScript from src/index.ts. That works under tsx, vitest and Next, all of which transpile on the fly. It breaks the moment a compiled production server imports them. We fixed it that afternoon, with two files written against those entry points. Fixing it after F005 through F009 would have been a different kind of day.

That is the argument for staying in the loop, and it has nothing to do with catching bad code. It is that the person holding the deployment model in their head asks questions the agent does not know to ask.

Green tests are not evidence

Two failures worth reporting, because both passed every check that people normally point at.

The first was a compatibility test proving Zod 4 works with the AI SDK. Three tests, all green. The mock model handed to them was malformed in three separate ways: finishReason is an object in the provider spec rather than a string, its raw field is required, and token usage arrives as per-category objects rather than numbers. The tests passed anyway, because generateObject only reads content[].text. Typecheck caught all three. The green suite hid them.

The second was mine to catch and the agent caught it first. The skeleton client existed for one reason: to prove the browser can reach the API across origins, since the two deploy separately. It was written as a server component. A server component's fetch runs in Node, where CORS does not exist. It rendered the health status perfectly and proved the opposite of what it claimed.

Both of these are arguments for the check list being longer than test. The order is fixed in CLAUDE.md and CI runs the same one:

pnpm build
pnpm typecheck
pnpm lint
pnpm test
pnpm format:check

What this does not do

It is not hands-off. I read every diff, and the review time is real. If you want to prompt freely and see what comes out, this setup will annoy you within an hour.

The docs are maintenance. When the branching model changed, three files had to change with it. When packages started exporting compiled output, CLAUDE.md's check list was wrong until it was updated, because from a clean tree nothing typechecks before a build.

It is also unproven past a scaffold. F001 shipped a monorepo, 40 tests and a health endpoint. Nothing in it answers a question about a tender yet.

F002 is in flight as I write this. The schema is applied, the extension permission fork is resolved, and the tests that prove a duplicate document gets rejected are not written. The ratio holds so far: the agent wrote the migration, and I decided that a byte-identical duplicate should be rejected by a unique constraint rather than by convention, because retrieval returning the same passage twice looks like corroboration.