Stage 9 of 11

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.

12 min read 45 min exercise Plaque: Agent's Author
In 30 seconds
Recall

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:

HabitFailure it preventsIn your repo after today
Record each deliberate oddity, with its reasonThe agent "fixes" a choice that looks wrongdocs/decisions/0001-atomic-guarded-update.md
Short comments that point at the whyLong comments that go stale, or none at all// … see docs/decisions/0001.
One reference shape to copyFive endpoints in five stylesProductsController and its test file, via new-endpoint
Claims checked against the codeDocs that describe last month's repoThe 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:

Code alone Code + a one-line pointer + an ADR OrderService.csvar updated = await db.Products  .Where(p => p.Stock >= q && …)  .ExecuteUpdateAsync(… Stock - q …);if (updated == 0) return …;no entity loaded, no Stock -= q: it looks odd “simplify this”the agent’s rewritevar p = await …FindAsync(id);if (p.Stock < q) return …;p.Stock -= q;await db.SaveChangesAsync();shorter, and it oversells OrderService.cs// … see docs/decisions/0001.var updated = await db.Products  .Where(p => p.Stock >= q && …)  .ExecuteUpdateAsync(… Stock - q …);if (updated == 0) return …; follows the pointerdocs/decisions/0001-…mdDecision: one guarded UPDATEload, check, save oversells Declined: 0001 forbids load-check-save.Options: keep it, or a new ADR you approve. Likelier, not guaranteed: the stock reviewer and ConcurrencyTests still back it up.

Here is the ADR you will write. It is short on purpose: what was decided, why, and what breaks if someone undoes it.

docs/decisions/0001-atomic-guarded-update.md
# 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:

docs/ai/conventions.md
# 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
.agents/skills/new-endpoint/SKILL.md
---
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.
.agents/skills/update-docs/SKILL.md
---
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.

.agents/skills/ship-slice/SKILL.md
---
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:

StepWho does itWhy 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 codeThe agentThe part that needs a model
Format, build, testA script (gate.sh)Same result every run; the model only reads the output
Is the change safe?Reviewer subagentsJudgement, but from a fresh context that didn't write the code
Are the docs still true?The update-docs skillA model checks each claim against the code, so the README can't quietly fall behind
Secrets, junk files, message formatGit hooks (lesson 7)Must hold even if every model gets it wrong
Approve the plan, decide a BLOCK, approve the commitYouDecisions 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:

What the docs say What the repo shows AGENTS.mdBuild: dotnet build -warnaserror quality-gate/gate.sh:12dotnet build -warnaserror TRUE README.mdEndpoints: 4 listed Controllers/*.cs6 routes, incl. low-stock, GET orders STALE a decisions doc · real projectJWT: “rejected, not requested” git log“add JWT”, one minute later STALE a notes file“we carefully considered SQL Server” no file, commit or test says so UNVERIFIABLE /update-docs lists each claim, then looks for a file:line, a commit or a test. Proposed diff, STALE rows only+ GET /api/products/low-stock · + GET /api/ordersyou approve

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 CodeCodexAntigravity
Rules indexAGENTS.md when no CLAUDE file existsAGENTS.md, git root down to the working folderAGENTS.md or GEMINI.md
Skills.claude/skills/<name>, here a symlink to .agents/skills; /name.agents/skills/<name>; $name.agents/skills/<name>; /name
ADRsPlain 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.

  1. // Atomic guarded UPDATE, never read-then-write: see docs/decisions/0001.
  2. // Subtract q from Stock where Stock is at least q.
  3. // Done this way for concurrency reasons. Don't change it.
  4. conventions.md: Errors are ProblemDetails: 400, 404, 409.
  5. README.md: "The API has four endpoints:" followed by four routes.
  6. ADR 0001, Consequences: EF InMemory can't be used for order tests.
  7. AGENTS.md, More context: Deliberate decisions, with the why: docs/decisions/.
  8. 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

Exercise · about 45 min · you add: an ADR, conventions.md, Directory.Build.props, two skills, pointers to the ADR

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).

  1. Start clean. git status should say nothing to commit, working tree clean, on main.
  2. Write the ADR. Run mkdir -p docs/decisions, then create docs/decisions/0001-atomic-guarded-update.md from 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.
  3. 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.md and - Deliberate decisions, with the why: docs/decisions/.
    Then check:
    grep -rn "decisions/0001" src docs/ai .claude/agents
    grep -rn "added in lesson 9" src
    Expect three lines from the first (the comment, the Why: line, the reviewer) and nothing from the second.
  4. Conventions, and the file they name. Create docs/ai/conventions.md from the listing above. Check each line against your code, for example grep -rn DateTimeOffsetToBinaryConverter src should 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.
  5. Add the three skills. Write both SKILL.md files from "Going deeper: the two skills in full"; new-endpoint names ProductsTests.cs; change that mention to the test file that holds your lesson 4 low-stock tests (ApiTests.cs if 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 ->. Read ship-slice before 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.)
  6. 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, and OrderService.cs, whose only change is a comment). Then the build settings on their own: build: add Directory.Build.props. Then chore(harness): add new-endpoint, update-docs and ship-slice skills.
  7. 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.md is missing GET /api/products/low-stock and GET /api/orders. It may flag more, such as the .env.example line in invariants.md. For each STALE row, open the evidence it cites and decide yourself.
  8. 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-docs again: the README rows should now be TRUE. Commit through /commit: docs: fix stale README endpoints.
  9. 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 cites docs/decisions/0001 (or invariants.md), declines or hands you options, and edits nothing; git status stays clean. If it proposes an edit, answer No: the ADR didn't hold this time. Sharpen its Consequences and the Why: line, then try again in another new session. Any edit that does land still meets the stock reviewer and ConcurrencyTests.

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.

Done when: tick all four to earn the Agent's Author plaque
Worth installing or reading

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.

Harness it

Add /update-docs to your definition of done, so the docs are checked after every feature instead of when someone notices.

Read next

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.

Ask your agent

"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."