Code and docs written for the agent as reader
The reader your repo has most often is now an agent. It starts every session cold, can't ask the author, takes every sentence at face value, and treats anything unusual as a candidate for cleanup. Write for that reader: the reason next to the odd choice, one shape to copy, and docs checked against the code.
- Code that looks odd on purpose needs its reason written down, in an ADR, and a one-line comment that points to it.
- Agents imitate well, so give them one reference shape to copy, and make every convention match it.
- Docs drift. A skill,
/update-docs, checks each claim against afile:line, a commit or a test, and marks it TRUE, STALE or UNVERIFIABLE.
Lesson 2: Claude Code stops loading your AGENTS.md. What is the likeliest cause?
If a CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md exists in the working folder or above, Claude Code reads that instead. You watched it happen with CLAUDE.local.md.
Lesson 8: why does .mcp.json name @upstash/context7-mcp@4.1.1, not the bare package?
A bare name runs whatever version is newest that day. Pinning means the code you vetted is the code that runs, until you choose to move.
What happened
Failed run
A developer's project kept a document of decisions. One line said JWT authentication was "rejected, not requested". One minute after that line was committed, JWT was committed. Nothing checked the document against the code.
In another session, the tests couldn't run the atomic stock update, so the agent rewrote it as read-then-write and called it "still correct against concurrent writes". Nothing in the repo said why the original looked the way it did. A live test of 12 concurrent orders for 3 units oversold.
Two real sessions, anonymised
With the harness
The odd-looking update carries one comment line, // … see docs/decisions/0001, and that file says what breaks if it is "simplified". A skill checks every doc claim for evidence:
| claim | verdict | evidence | | JWT: "rejected" | STALE | git log: JWT added |
The agent reads the reason before it edits. You read the audit before you trust a doc.
The reader changed
Lesson 1: the agent knows only what is in its context window. Every new session starts empty and fills it from your repo: the rules file, then whatever it opens. A human colleague who finds odd code asks around. The agent asks nobody: what is on the page is all it has. So the question for this lesson is what the page should say. Four habits, each tied to the failure it prevents:
| Habit | Failure it prevents | In your repo after today |
|---|---|---|
| Record each deliberate oddity, with its reason | The agent "fixes" a choice that looks wrong | docs/decisions/0001-atomic-guarded-update.md |
| Short comments that point at the why | Long comments that go stale, or none at all | // … see docs/decisions/0001. |
| One reference shape to copy | Five endpoints in five styles | ProductsController and its test file, via new-endpoint |
| Claims checked against the code | Docs that describe last month's repo | The update-docs skill |
One habit you already have: where a rule can be a test, make it one. ConcurrencyTests (lesson 7) fails when stock oversells, however convincing the prose around it.
Record the decision: an ADR
An ADR (architecture decision record) "captures a single AD [architectural decision] and its rationale" (adr.github.io): a short Markdown file with the context, the decision and its consequences. Teams write them so the next person doesn't undo a decision without knowing why. An agent needs that more than a person does. Watch what one does with your stock update:
Here is the ADR you will write. It is short on purpose: what was decided, why, and what breaks if someone undoes it.
# 0001: Stock changes use one guarded UPDATE, not read-then-write - Status: accepted - Date: 2026-09-30 ## Context Orders reserve stock. Under concurrency, a read-then-write (load `Product`, check `Stock >= qty`, assign, `SaveChanges`) lets two requests read the same value and both succeed. This is how a real session's code oversold: 12 concurrent orders for 3 units succeeded more than 3 times. It was introduced to make EF Core InMemory tests pass, because InMemory can't translate `ExecuteUpdateAsync` (and it rejects transactions by default, so the rewrite loses those too). On SQLite, read-then-write inside `BeginTransaction()` happens to be serialised: Microsoft.Data.Sqlite starts a non-deferred transaction, which takes the write lock up front ([docs](https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/transactions)). That is an accident of one provider, not a design. We measured it: read-then-write *inside* the transaction passes the concurrency test, and read-then-write *without* it sells 12 of 3. ## Decision - Decrement with one statement: `UPDATE Products SET Stock = Stock - @q WHERE Id = @id AND Stock >= @q` (`ExecuteUpdateAsync`). SQLite runs one writer at a time, so the check and the write can't interleave. - 0 affected rows means 404 (no product) or 409 (not enough stock). - The decrement and the order insert share one transaction. - Tests run on real SQLite. `ConcurrencyTests` is the guard: 12 parallel orders for 3 units give exactly 3 successes. ## Consequences - EF InMemory can't be used for order tests. That is accepted; see docs/ai/testing.md. - Anyone "simplifying" this to tracked entities breaks the invariant, and `stock-invariant-reviewer` blocks it, even when the concurrency test is still green. The test can't see portability; the reviewer can. - If the database changes (for example to SQL Server), the same statement stays correct. Revisit only the busy-timeout setting.
No tool loads docs/decisions/ by itself, so an ADR nobody points to is never read. Point to it from wherever the agent will be when it matters: a one-line comment at the code, a Why: line in docs/ai/invariants.md, the stock reviewer's first instruction, and a "More context" line in AGENTS.md. Keep the comment to one line. The reasoning lives in one file, so it can't go stale in five.
Your AGENTS.md already forbids read-then-write, so a fresh session may decline even without the ADR. What the ADR adds is the why, which keeps the rule standing when someone argues "it is shorter", and a way to change the decision on purpose.
One shape to copy
Agents are good at imitation. "Copy the shape of ProductsController and its tests" works better than a page of description, and the new-endpoint skill says exactly that. docs/ai/conventions.md lists the rules around the shape, each one checkable against the code:
# Conventions - Layout: `Controllers/` (HTTP; simple reads may use `StockDbContext` directly, like `ProductsController`), `Services/` (rules, transactions and every stock change), `Domain/` (entities), `Data/` (DbContext). - The reference shape for a resource is `ProductsController` + its tests. Copy it (skill `new-endpoint`). - Errors are `ProblemDetails`: 400 validation, 404 missing, 409 conflict (duplicate SKU, insufficient stock). - Everything is async, with a `CancellationToken` passed through. - Nullable is enabled, and warnings are errors (`Directory.Build.props`). - Versions: `net8.0`, EF Core 8.0.x. For any library API you are not sure of, look it up (context7 MCP) instead of relying on memory. - SQLite can't ORDER BY or compare `DateTimeOffset` in SQL; store it with `DateTimeOffsetToBinaryConverter`. See the [EF Core SQLite limitations](https://learn.microsoft.com/en-us/ef/core/providers/sqlite/limitations). - Comments: one or two lines, and the "why" points to docs, e.g. `// see docs/decisions/0001`.
The conventions must match the shape. An early draft said "controllers are HTTP only" and "never return entities", while ProductsController uses the DbContext and returns Product. An agent given both picks one, and you can't predict which. The fix is to describe what the reference really does.
One line names a file your practice repo doesn't have: Directory.Build.props. You will add it rather than drop the line. Your quality gate already builds with -warnaserror, so the file changes nothing the gate checks, and a plain dotnet build now agrees with it.
Going deeper: the two skills in full
--- name: new-endpoint description: Add a new endpoint or resource by copying the reference shape (ProductsController and its tests). Use when a plan slice adds or changes an HTTP endpoint. --- # New endpoint The reference shape is `src/StockApi/Controllers/ProductsController.cs` plus `tests/StockApi.Tests/ProductsTests.cs`. Read both first, then copy their shape. Do not invent a new one. 1. **Tests first**: add the failing integration tests to the reference test file, in its shape, using `StockApiFactory`: the happy path, 404, 409 and 400. If the endpoint changes stock, also add a case to `ConcurrencyTests`. Run them and show they fail for the right reason. 2. **Controller**: validate the input and map results to 200/201/404/409 `ProblemDetails`. Simple reads and creates may use `StockDbContext` directly, as `ProductsController` does; anything that changes stock calls a service. 3. **Service**: rules and transactions. Any stock change follows docs/ai/invariants.md (one guarded UPDATE). 4. **DTOs**: `record` request types next to the controller. Responses may return the entity, as `ProductsController` does; add a response record only when the entity would leak something. 5. **Check**: unless the `ship-slice` skill called this one (it runs these steps itself), run the `quality-gate` skill, then ask for a review by `test-reviewer`, plus `stock-invariant-reviewer` if stock changed.
---
name: update-docs
description: Audit docs/ai/*.md, AGENTS.md and README.md against the actual code and propose corrections. Use after a feature lands, before a submission, or when the user asks whether the docs are still true.
---
# Update docs
Docs written from memory drift. This skill compares every claim with the code.
1. List each factual claim in `AGENTS.md`, `docs/ai/*.md`, `docs/decisions/*.md` and `README.md`:
commands, endpoints, invariants, file paths, versions, "we decided X".
2. For each claim, find the evidence: the file:line, a `git log` entry or a test. Classify it:
**TRUE** (with evidence), **STALE** (with what the code does now) or **UNVERIFIABLE**.
3. Print a table: claim · verdict · evidence.
4. Propose edits for STALE claims only, as a diff. Do not apply them until the human approves.
5. Never add narrative ("we carefully considered…"). Docs here are instructions for the next agent.
Neither skill restates the conventions; each points at them, so there is one place to edit.
One skill for the whole loop
docs/ai/workflow.md describes the loop: one slice, gate, review, commit. Typed out each time, it's a prompt again, and prompts drift. A workflow skill turns the loop into one command that calls the skills and reviewers you already have (new-endpoint, quality-gate, the two reviewers, update-docs, commit), in order, and stops wherever a human decides. Because it commits, it's user-only (disable-model-invocation: true): you start each slice; the agent never starts the next one by itself.
--- name: ship-slice description: Build the next unchecked slice of ai-session/PLAN.md end to end (test, code, quality gate, reviews, commit), stopping wherever a human decides. Use when the plan is approved and the user says to build, continue or ship the next slice. disable-model-invocation: true --- # Ship one slice This is `docs/ai/workflow.md` steps 3 to 6 (slice, gate, review, docs and commit) as one command. Steps marked **STOP** wait for the human. 0. **Check the start.** `ai-session/PLAN.md` must be committed on its own (`git log --oneline -- ai-session/PLAN.md` shows a `docs(plan): …` commit) and `git status` must be clean. If not, say which and stop. 1. **Pick the slice.** The first unchecked one in `PLAN.md`. Name it in one line. If the user asked for something the plan doesn't have, **STOP**: that is a requirement change (workflow.md, "When requirements change"). 2. **Test first.** For an endpoint, use the `new-endpoint` skill for its shape and its steps 1 to 4; this skill runs the gate and reviews. Otherwise write the failing test, run it, and show it fail for the right reason. 3. **Code.** The smallest change that makes that test pass. Nothing outside the slice. 4. **Gate.** Run the `quality-gate` skill. Fix the cause, never the test. Same failure twice: hand over a 1-3-1. **STOP** if a fix would touch an invariant. 5. **Review.** Dispatch `test-reviewer`, plus `stock-invariant-reviewer` if stock or orders changed. Report each finding with its file:line. **STOP** on any BLOCK: the human decides. 6. **Docs.** If the slice added or changed an endpoint or a documented behaviour, run the `update-docs` skill. **STOP**: show its STALE rows and apply only the fixes the human approves. 7. **Commit.** Tick the slice in `PLAN.md`, then run the `commit` skill. It asks before staging and before committing. **STOP** there. 8. **Report and stop.** The commit hash, what the reviewers said, and the next unchecked slice. Never start the next slice yourself.
Notice how little of it the model does on its own. Each step goes to whichever part of the harness does it most reliably:
| Step | Who does it | Why that one |
|---|---|---|
| Plan committed? Tree clean? | A command (git log, git status) | A fact, not a judgement: check it, don't ask the model to remember |
| Write the test and the code | The agent | The part that needs a model |
| Format, build, test | A script (gate.sh) | Same result every run; the model only reads the output |
| Is the change safe? | Reviewer subagents | Judgement, but from a fresh context that didn't write the code |
| Are the docs still true? | The update-docs skill | A model checks each claim against the code, so the README can't quietly fall behind |
| Secrets, junk files, message format | Git hooks (lesson 7) | Must hold even if every model gets it wrong |
| Approve the plan, decide a BLOCK, approve the commit | You | Decisions with consequences stay human |
That split is the rule of thumb for any skill you write: a step with one right answer goes to a script or a hook; a step that needs judgement goes to the model, and a second model checks it; a decision goes to you.
Docs drift: check claims against the code
Docs are written once and the code keeps moving. Your README.md still lists the four endpoints the starter had; lessons 4 and 8 added two more. A doc that is wrong is worse for an agent than no doc, because the agent believes it. update-docs makes the check a procedure:
Three choices make it work. Every verdict needs evidence, so the skill can't approve a claim just by agreeing with it. It proposes edits and waits, because changing shared docs is your call. And it forbids narrative: a docs folder is instructions for the next agent, not a story about the last one. On a trial run against a copy of the exercise repo it found the README endpoints and several more drifted claims, including a line in invariants.md that names a .env.example the repo doesn't have.
Going deeper: the same files in Codex and Antigravity
| Claude Code | Codex | Antigravity | |
|---|---|---|---|
| Rules index | AGENTS.md when no CLAUDE file exists | AGENTS.md, git root down to the working folder | AGENTS.md or GEMINI.md |
| Skills | .claude/skills/<name>, here a symlink to .agents/skills; /name | .agents/skills/<name>; $name | .agents/skills/<name>; /name |
| ADRs | Plain Markdown in docs/decisions/. None of the three loads it by itself, so the rules file, the invariants, the reviewer and a code comment point to it. | ||
Sources: Claude Code memory · Codex AGENTS.md · Antigravity rules verified 2026-10-01
Check yourself
You add // … see docs/decisions/0001 above the stock update. What does it buy you?
The comment sends the agent to the reason before it edits, so it is more likely to decline and cite 0001. Nothing loads the ADR automatically and nothing guarantees obedience, which is why the reviewer and ConcurrencyTests stay.
/update-docs finds the README lists 4 endpoints and the code has 6. What should it do?
STALE with evidence (the controller lines), a proposed diff, and no edit until you approve. The code is the truth here; the doc moves to match it.
A doc says "we carefully considered SQL Server", and nothing in the repo mentions it. The verdict?
No evidence either way, so it is neither TRUE nor STALE. The honest fix is usually to delete the sentence: it tells the next agent nothing it can act on.
Practice first
Three quick drills with instant feedback, before you touch the repo.
These lines come from one repo whose controllers now have six routes. Click the lines that would mislead a fresh agent, or tell it nothing it can act on.
- // Atomic guarded UPDATE, never read-then-write: see docs/decisions/0001.
- // Subtract q from Stock where Stock is at least q.
- // Done this way for concurrency reasons. Don't change it.
- conventions.md: Errors are ProblemDetails: 400, 404, 409.
- README.md: "The API has four endpoints:" followed by four routes.
- ADR 0001, Consequences: EF InMemory can't be used for order tests.
- AGENTS.md, More context: Deliberate decisions, with the why: docs/decisions/.
- new-endpoint: Read ProductsController and its tests first, then copy their shape.
What would you do?
Your code calls a stored procedure where every other service uses EF Core, on purpose. Fresh agents keep "tidying" it into EF calls. What do you add?
The reason lives in one file, and the comment sends the agent there before it edits. A long comment goes stale, nothing loads the README on its own, and a pasted reminder is gone in the next session.
You want a fresh agent to add GET /api/suppliers in the same style as the rest of the API. Which instruction works best?
Agents are good at imitation. A real file to copy beats a page of description, and that is exactly what the new-endpoint skill says.
conventions.md says "never return entities", but ProductsController, the shape to copy, returns Product. What is the fix?
An agent given two conflicting sources picks one, and you can't predict which. Describe what the reference really does, so the rules and the shape agree.
Exercise: give the odd code its reason, then test it
Work in your practice repo (~/projects/stockapi), with this course's repo cloned beside it. After lessons 1 to 8 it has AGENTS.md, docs/ai/, a plan, the commit and quality-gate skills, two reviewers, guards and ConcurrencyTests. Windows: use WSL2 or Git Bash (setup).
- Start clean.
git statusshould say nothing to commit, working tree clean, onmain. - Write the ADR. Run
mkdir -p docs/decisions, then createdocs/decisions/0001-atomic-guarded-update.mdfrom the listing above, with today's date. Read every line: if your repo differs (a different status code, say), the ADR must say what your code does. - Point at it, four times.
- In
src/StockApi/Services/OrderService.cs, replace// Atomic guarded UPDATE: no read-then-write. See docs/decisions (added in lesson 9).with// Atomic guarded UPDATE, never read-then-write: see docs/decisions/0001. - In
docs/ai/invariants.md, add a last bullet to invariant 1:- Why: docs/decisions/0001-atomic-guarded-update.md(lesson 2 had you delete it). - In
.claude/agents/stock-invariant-reviewer.md, change "docs/ai/invariants.md first." to "docs/ai/invariants.md and docs/decisions/0001-atomic-guarded-update.md first." (the sentence wraps after "Read") (lesson 6 left it out). - Under "More context" in
AGENTS.md, add- Code layout, errors, the shape to copy: docs/ai/conventions.mdand- Deliberate decisions, with the why: docs/decisions/.
grep -rn "decisions/0001" src docs/ai .claude/agents grep -rn "added in lesson 9" src
Expect three lines from the first (the comment, theWhy:line, the reviewer) and nothing from the second. - In
- Conventions, and the file they name. Create
docs/ai/conventions.mdfrom the listing above. Check each line against your code, for examplegrep -rn DateTimeOffsetToBinaryConverter srcshould find lesson 8's converter. Then make the warnings line true:cp ../agent-harness-course/reference-solution/Directory.Build.props . cat Directory.Build.props # Nullable, ImplicitUsings, TreatWarningsAsErrors
Run/quality-gate: expect quality gate: green. - Add the three skills. Write both
SKILL.mdfiles from "Going deeper: the two skills in full";new-endpointnamesProductsTests.cs; change that mention to the test file that holds your lesson 4 low-stock tests (ApiTests.csif you kept them in the starter's file). Then link them as in lesson 5:mkdir -p .agents/skills/new-endpoint .agents/skills/update-docs # write .agents/skills/new-endpoint/SKILL.md and .agents/skills/update-docs/SKILL.md, then: ln -s ../../.agents/skills/new-endpoint .claude/skills/new-endpoint ln -s ../../.agents/skills/update-docs .claude/skills/update-docs cp -r ../agent-harness-course/reference-solution/.agents/skills/ship-slice .agents/skills/ ln -s ../../.agents/skills/ship-slice .claude/skills/ship-slice ls -l .claude/skills
Five lines with->. Readship-slicebefore you keep it: it is the loop you will run in lesson 11. (Git Bash without Developer Mode: copy the folders instead, as in lesson 5.) - Commit three times, through
/commit. First the docs and the pointer comment:docs: add ADR 0001, conventions and pointers(the ADR,conventions.md,invariants.md,AGENTS.md, the reviewer, andOrderService.cs, whose only change is a comment). Then the build settings on their own:build: add Directory.Build.props. Thenchore(harness): add new-endpoint, update-docs and ship-slice skills. - Run the audit. Start a fresh session (
claude --permission-mode default) and type/update-docs. Expect a table, and at least one STALE row:README.mdis missingGET /api/products/low-stockandGET /api/orders. It may flag more, such as the.env.exampleline ininvariants.md. For each STALE row, open the evidence it cites and decide yourself. - Fix what is stale. Approve the README change and any other row you checked and agree with; say no to the rest, with a reason. Run
/update-docsagain: the README rows should now be TRUE. Commit through/commit:docs: fix stale README endpoints. - Test the ADR. Start a new session in Manual mode (
claude --permission-mode default) and ask: "Simplify OrderService.PlaceAsync: load the product, check Stock, decrement and SaveChanges. It is shorter." Pass: it citesdocs/decisions/0001(orinvariants.md), declines or hands you options, and edits nothing;git statusstays clean. If it proposes an edit, answer No: the ADR didn't hold this time. Sharpen its Consequences and theWhy:line, then try again in another new session. Any edit that does land still meets the stock reviewer andConcurrencyTests.
Codex: skip the symlinks; it reads .agents/skills/ directly, so type $update-docs, and ask the reviewer by name. Antigravity: the same folder works; type /update-docs, and start a new conversation for the ADR test.
-
docs/decisions/0001-…mdis committed, andgrep -rn "decisions/0001" src docs/ai .claude/agentsfinds the comment, theWhy:line, the reviewer and the example inconventions.md -
docs/ai/conventions.mdandDirectory.Build.propsare committed, and/quality-gateis green -
ls -l .claude/skillsshows five links, and/update-docsflagged the README endpoints as STALE before your fix - A fresh session asked to simplify
OrderServicecited the ADR or the invariants and edited nothing
- adr.github.io: ADR templates and guidance from the ADR community.
- Example ADRs: A large, maintained collection of real ADR examples and templates.
Each link has its own install steps. Install only what you will use, check how recent it is, and read what a plugin adds before you enable it. Links checked 2026-10-01.
Add /update-docs to your definition of done, so the docs are checked after every feature instead of when someone notices.
adr.github.io, for the ADR idea and its templates. Then agents.md: "Think of AGENTS.md as a README for agents". Compare its suggested sections with your own file.
"Read my docs/ai folder. Which line, if deleted, would not change how you behave? Then name one place in the code that looks odd to you and has no reason written down."