The task was aligning types. Our mobile app talks to a backend I don’t own, and a good share of the data moving between them is passed around as opaque, unmodelled shapes — so when something needs to be typed properly, the only real source of truth is the backend’s own code. The loop is: open the API repo, find the endpoint, find the response object it actually returns, come back, fix the type on the mobile side, discover the next one, go back again. Multiply by a codebase of about 1,100 TypeScript files.
I’ve done that loop many times in eleven years of writing software. It is not hard. It is just long, and it cannot be parallelised across people, because the whole value is in one person holding both sides in their head at once.
The last time I did it, it took hours instead of weeks. Not because the model got smarter — though it did — but because by then I’d written down where the two sides meet, and it could make that round trip without me.
This is a post about the writing down.
July 2025, eight days. Two React Native apps that had been drifting apart in separate repos became one monorepo with a shared package: 1,513 files, 255,765 insertions, and a single commit in the middle that deleted 234,300 lines — the deduplication, which was the entire point.
I want to be precise about that one, because it’s the number people will want to quote. That was me. It was before I had any of the structure this post is about — no agents, no skills, no routing. I had a chat window and the Copilot plugin, and chat-era AI helps roughly the way a good rubber duck with a search engine helps. It was eight days of my own hard labour.
I include it because it’s the control. It’s what “fast” looked like for me without the thing I’m about to describe.
August–September 2026, eleven days. 108 commits, 1,126 files, +47,826 / −53,242 — net negative about five and a half thousand lines. In that window: a test harness with an enforced coverage ratchet, a full TypeScript cleanup, a lint config rebuilt with two new plugins, an import-optimisation pass across 587 files, every OS-native alert dialog replaced with in-app modals, and a queryable knowledge graph of the codebase.
That second list is the category that never wins a sprint-planning argument. It’s wide, it’s all judgment calls at the edges, and it can’t ship half-done. It had been “someday” for a year.
The difference between the two windows isn’t the model. It’s that by the second one, the repo could explain itself.
My working model of an AI coding agent used to be “a very fast junior who has read everything and remembers nothing about us.” That’s accurate, and it’s the whole problem. On a small project it doesn’t bite. On 220,000 lines with two apps sharing one package and years of “we do it this way because of that one incident,” it bites constantly.
The failure isn’t bad code. It’s generic code — perfectly idiomatic in the abstract, wrong for this repo. It reaches for the standard solution when we have a wrapper. It restyles the thing it was asked to fix. It re-derives, slightly incorrectly, a flow somebody already spent a week understanding.
Three layers fixed most of that.
A single AGENTS.md at the root, 441 lines: architecture, directory conventions, the environment model, and the house rules stated as absolutes — prefer the smallest possible diff, especially in UI; never call the OS alert, use the in-app modal; when the root cause is in the backend, don’t paper over it on the client.
The tool-specific file is a one-line pointer: CLAUDE.md is a single import line aimed at AGENTS.md, and nothing else. Copilot reads AGENTS.md natively; so does Codex. One file, three tools, no drift. If you take one thing from this post, take this one. It costs ten minutes.
Some flows have a control path no one file reveals. Authentication is the standard example: splash, an email “decide” step, then password or MFA or SSO or biometrics, a bootstrap screen, token storage, the 401 interceptor, and what has to be cleared on logout so the next user doesn’t inherit the last one’s state. That lives across a dozen files, and the invariants between them live nowhere.
So each such flow got a skill: a markdown file with the verified control path, the cross-file invariants, and the gotchas that cost somebody a day. Ten of them now — the auth and session lifecycle, a couple of gnarly multi-step creation flows, a screen whose two app-specific versions diverge deliberately in places that look like copy-paste, plus the cross-cutting ones: how to write a test here and what to mock, how type-checking is really wired, why the lint config looks the way it does, the whole upgrade-and-release pipeline.
They live in .github/skills/, symlinked into .claude/skills/. One set of files, two tools, no duplication.
Writing skills is the easy half. Getting them loaded before the agent starts exploring is the half that matters, and it does not happen on its own — the agent will happily read forty files to re-derive what one file already says.
So AGENTS.md carries a table: if the work touches X, load Y. The left column is symptoms in the language a ticket actually uses — “logged out randomly”, “stuck on splash”, “401 loop”, “filters leaked from the previous user”. The right column is the skill. Plus one line that does a lot of work: the match doesn’t have to be exact, and the skill costs little to consult.
Two more connections close the loop, and they matter less for what they fetch than for the rules attached to them. The agent can read a ticket directly — description, acceptance criteria, all of it — and it can open the design frame the ticket links to. Wiring that up is an afternoon’s work. Writing down how to treat what comes back is the part that needed thought.
The design rule is the one that earns its keep: a mockup is intent, not a specification. One designer covers the web app and both mobile apps, so those frames were never a pixel-accurate description of any of them. So the rule says: build it out of components the app already has rather than transcribing the frame’s raw values; implement what the ticket asks for and not the three adjacent frames showing next quarter’s ideas; and where the acceptance criteria and the mockup disagree on wording, the criteria win, because mockup copy is usually stale placeholder text. If the design needs a field the backend doesn’t return, don’t invent it on the client — say so on the ticket and stop.
Without those rules an agent will faithfully turn a frame into a brand-new one-off component that duplicates one you already have: technically correct, locally wrong, and more expensive to review than to write. With them, a routine ticket goes from a morning of tab-switching to a few minutes — and the ones that were never going to work get flagged as backend problems before anyone starts building.
Write the skill after you finish the hard thing, not before. Every good one I have is the residue of a week I don’t want to spend twice. Written speculatively, they came out as documentation-shaped filler.
Not every module earns one. Most of mine are the same shape — a list, a search bar, filters, a detail view. That shape is described once in the conventions file. A skill earns its place when there’s a real control path or cross-cutting state one file can’t show you.
State rules absolutely, and give the reason. “Prefer minimal changes” gets interpreted. “Don’t refactor or restyle surrounding code as a side effect of an unrelated fix — every unrequested UI change is surface area QA has to re-test, and can hold up the release” gets followed, and it generalises to the cases you didn’t enumerate.
Say what’s a reference and what’s a spec. Our design mockups are intent, not pixel truth. Writing that down explicitly killed a whole class of “transcribed the mockup’s raw values into a new one-off component instead of using the one we already have.”
Fix the skill in the same PR where you find it stale. They describe code that keeps moving.
Make verification part of the change. Type-check, lint and tests run on every change and again in CI. An agent is genuinely good at grinding through a wall of type errors — but only if “done” is defined as green, out loud, in the conventions file.
Shortly after this went up, I proposed the same structure to Ignite, the React Native boilerplate both of our apps started from. A collaborator there asked the obvious question, and asked it politely: Ignite already ships a good docs folder — aren’t agents smart enough to go and read it?
I decided to measure instead of answer. Ten runs against a freshly generated Ignite app, one agent each, in throwaway copies. Two arms, identical prompts except a single line telling one of them that an AGENTS.md existed and to read it. Three ordinary tickets — a settings screen with an empty state, a reusable status pill, a preference that survives a restart — phrased the way a ticket is phrased, with no mention of generators or components or theming. Scoring by grep, not by my opinion of the diffs: did it reuse the existing components, use the import alias, go through the theming helpers, add a dependency, inline a colour.
It came out null. Both arms scored essentially full marks. The weaker, cheaper model with no conventions file did as well as the stronger one with it. When I suspected the twenty demo screens were doing the work, I stripped them out and ran a harder task against the remainder; still null. On that codebase, the file I was advocating for changed nothing that the grep could see.
The reason is worth more than the result. A freshly generated Ignite app is twenty-odd screens that all do it the same correct way, and an agent that can copy a good neighbour does not need prose telling it what the neighbour already demonstrates. Ignite is exceptionally consistent — that consistency is the product — so it is close to the best possible case for “just read the code,” and close to the worst possible case for the argument I had been making.
Which sharpens the claim rather than killing it. Everything an agent got right in that test, it got right by imitation. Imitation cannot teach you that a generator exists — none of the ten runs used one, because nothing in a generated project hints that ignite generate screen is there. It cannot tell you which commands define “done.” It cannot tell you where a new route belongs when two plausible homes exist; that was the one place the arms genuinely diverged. And it cannot tell you what the product is. Those are the things worth writing down, and they are a much shorter list than the one I started with.
There was also a finding I liked considerably less. The runs that read the conventions file produced bigger diffs — four of five pairs — including one that translated a new screen into all seven locales and one that edited the conventions file itself. My file says, in as many words, prefer the smallest possible diff. An agent tries to satisfy everything it has read, so breadth in the file becomes breadth in the change. The rule needed to be stated far more sharply than I had stated it, and the file needed to be shorter, and I only know that because the measurement embarrassed it.
The honest summary: where the exemplars are uniform and good, the code is the conventions file. Where they have drifted — several neighbours, disagreeing, which is where every long-lived codebase ends up and where my own eleven days were spent — the file is doing something the code cannot do for itself. I have not measured that second case. It is the one I believe in, and it remains the one I cannot prove.
The harness, the prompts, the scoring scripts and the full results are in the experiments/ folder of the companion repo, including the two rubric mistakes I had to correct mid-run, both of which had been in my favour.
I can’t prove the ROI. The one controlled comparison I have is the narrow one above, on somebody else’s codebase, measuring a much smaller thing — and it came out against me. For the claim that actually matters here I have a before and an after that differ in more variables than one. My “this was a quarter of work” instinct is an estimate made by the person with the strongest interest in it being impressive. The commit statistics are real and you can compute the same ones on your own repo, but they measure scope and elapsed time, not value, and a large diff is not an achievement. Some of that eleven-day window would have gone faster with any decent tooling. Some of it I’d never have attempted at all, which makes it uncountable in the other direction.
What I’ll say instead, and can defend: routine work that used to cost me hours — a QA bug, a product tweak, a small feature — now often costs minutes, and the wide boring migrations moved from “someday” to “this week.” Whether that’s 3x or 10x or a number that doesn’t apply, I don’t know.
It’s also worth being clear about the scope: this is one developer’s experience. I haven’t rolled this out across a team, and I haven’t measured what happens when several people depend on the same set of skills — whether they stay accurate under more hands, or whether everyone actually reads them. Take it as a single data point, not a case study.
And none of this is an argument for using AI blindly. The speed is real only because the guardrails are. The conventions file, the skills, the routing table, the three checks that have to be green — that scaffolding is what makes the output safe to ship, and without it you get the same speed pointed in a slightly wrong direction, which is worse than slow. The configuration is the work. The eleven days weren’t free; they were paid for in advance, by writing things down.
I spent a while thinking of this as documentation for the AI. It isn’t. Every one of those skills is the document I wish I’d had the first time I opened that flow — and the person who benefits most is me, six months later, coming back to something I’ve completely forgotten.
Docs for humans and docs for agents turn out to be the same artifact. The agent just gives you a reason to finally write them, and an immediate, unforgiving signal when they’re wrong.
Everything above describes the structure in the abstract. The companion repo is that structure as actual files, built around a fictional app — small enough to read in ten minutes, and written so that what you copy is the shape rather than the content.
AGENTS.md — the
conventions file: architecture, layer boundaries, house rules stated as absolutes with the
reason attached, and the routing table.CLAUDE.md — the
one-line pointer, so three tools read one source and nothing drifts..github/skills/auth-flow/SKILL.md
— a flow skill: the verified control path, the invariants that live between files, and the
traps that cost somebody a day..claude/skills — the
symlink, so Copilot and Claude Code read the same files with no duplication.git clone https://github.com/lvlrSajjad/agent-ready-monorepo.git
The repo’s README has the checklist for copying it into a project of your own. Prose and examples are CC BY 4.0 — adapt them, no need to ask.