The %w advisory counted every fmt.Errorf without %w, including calls that create an error rather than wrap one — so it fired permanently on correct code. It now looks for a call that passes an err and drops the %w, which is the actual rule. Recorded the general form in the loop's Verify step and in HARNESS.md: a warning that fires on correct code and keeps firing is a defect in the check or the code, resolved in that change. Two advisories have now been narrowed after firing on code the harness itself mandates, and the count creeping from one to two was the only signal either time. verify.sh is at zero warnings, which is what makes the next one legible.
10 KiB
The feature loop
Six steps, in order: Clarify → Plan → Implement → Verify → Document → Report.
CLAUDE.md holds the rules; this file holds the procedure.
Scale, do not skip. A one-sentence request gets one line of plan and a short report — and still gets
the conflict check, propagation, verification, the state.md touch and the stale-reference sweep.
Casual phrasing is not a smaller change: "default-language files do not need the .en part" is one
unambiguous sentence that rewrites filenames on disk, makes a suffix optional in code, and falsifies
every doc calling it required.
Propagation — a change is not done until every surface agrees
One request can touch four surfaces. Name each as done or n/a before reporting; the ones people miss are the first and the last.
| Surface | What it means |
|---|---|
| the site root | Out of reach: it lives in its own repo (ADR-0011). A disk-contract change ships a written migration step the author runs, plus a note on whether any bundle key or URL moves. Never claim to have migrated files you cannot see. |
| code | The engine, plus a test for the new behaviour. |
| the theme | Out of reach, like the site root (ADR-0023). This repo ships a contract extension in |
docs/theme-contract.md plus a written note of what a theme must do — never the theme itself, and never |
|
| a layout or markup decision dressed as an engine feature. | |
| fixtures and emitted output | Fixture sites in testdata/, the embedded default templates (a reference implementation of the contract), and anything reading a field or path shape you altered. |
| harness docs | Every doc, ADR, marker or example that assumes the old form. An ADR that mandates what you just made optional is not stale, it is contradicted — supersede it, do not quietly reword it. |
1. Clarify
Read docs/state.md, then docs/README.md — the map, always, before deciding what else to open.
Use its topic-ownership table to list the docs owning anything this change asserts a rule about,
and read those. That list is a floor: skipping it is how a rule gets written twice and the second
copy contradicts the first.
Conflict check — every request
Before anything else, ask whether the request contradicts something already decided. Classify and act; do not average two positions into a compromise nobody chose.
| Kind | Examples | What to do |
|---|---|---|
| Hard — reverses a deliberate decision | an ADR, an architecture invariant, the permalink shape, the untrusted boundary, a _MAX budget, a frozen contract, the dependency policy |
Stop. Quote the line, name the file, give both paths: comply, or change the decision (new ADR, or supersede the old one). Wait. Never pick for them. |
Soft — exceeds a convention or a [spec] shape |
a _WARN threshold, a style-floor preference, an unbuilt [spec] section's suggested shape, a latent item's trigger |
State it in one line, proceed with the request, record the deviation where the convention lives. Conventions are a floor, not a decision. |
| Stale — a doc contradicts the code | state.md inventory, a STATUS line, an out-of-date example |
Auto-resolve: the code wins. Fix the doc in Document, mention it in the report. No question needed. |
| None — the request refines or extends what is written | asking for something a [spec] section already describes |
Proceed. Say nothing about it. |
Resolve intelligently where the answer is genuinely unambiguous — a stale doc, or a request that is a superset of what is written. Surface anything where a reasonable person could have meant either thing, and everything in the Hard row without exception. One conflict is worth one message; a silently reversed decision costs the trust in every other decision.
Then decide what you genuinely do not know. Ask only questions whose answer changes the code or the bytes on disk. Maximum three, one message, up front, each with a bold default.
Never ask about: anything conventions.md, content-model.md, or an ADR already decides; naming,
formatting, file placement, test style; permission to follow the constitution; "would you like me
to also…" — that is scope creep wearing a question mark.
Do ask when: the feature has two plausible disk formats, URL shapes, or authoring ergonomics; it
touches an open question in state.md; success criteria are not observable from the request;
it appears to need a dependency, a new package, or a frozen-contract change.
No such questions? One line — "No questions — assuming tag pages live at the section root and reuse the post list template." — and continue.
2. Plan
Post this before writing code. Under fifteen lines.
Goal: one sentence, observable from outside the program
Reduces to: Bundle | Stage | Query | View | Interaction | Effect | bundle-as-program
Success criteria: the checks that will prove it works (commands, URLs, expected output)
Files: exact paths, marked new / edit, with an estimated ±LOC each
New deps: none (anything else needs an ADR first — stop and ask)
Earn-it check: which counter this increments, and whether an extraction is due this change
Trust check: does any untrusted input reach this code? ("n/a" if not)
Docs read: the owning docs you opened ("none" only if the change asserts no rules)
Not doing: the two or three adjacent temptations you are declining
Wait for a go-ahead unless the user said "just do it" or the change is under ten lines in one file. If the plan reveals a trunk, say so instead of planning and offer the leaf.
3. Implement
- Smallest code satisfying the success criteria. Nothing for the version after next.
- Only the planned files. No renames, reordering, reformatting beyond
gofmt, or tidying of neighbouring code. - Something wrong outside the plan? One line on the Latent list in
docs/state.md, keep moving. That is the whole mechanism; use it instead of a drive-by fix. - Reuse the existing shape before inventing one. Copying eight lines twice beats an abstraction at use two; the third use is when it becomes a design.
- Halt and ask if any
CLAUDE.md §6stop condition fires. Halting costs one message.
4. Verify
Run ./scripts/verify.sh — doc coupling, format, vet, build, test, dependency allowlist, budgets.
Then at least one piece of feature-specific evidence you actually executed: the new test name
and its output, curl -s localhost:PORT/path | head, a golden-file diff, the before/after fragment
of real generated markup, or benchmark numbers.
Budget failure: shrink the change, or stop and propose an ADR raising it. Never raise it silently.
A standing advisory is a defect. If a warning fires on correct code and will keep firing, either the check is wrong or the code is — resolve which, in that change. Tolerated warnings are how the whole channel stops being read, and the count creeping from one to two is the only signal you get.
Never write "should work", "this will now…", or your own diff summarised as a result. If you could not run something, say which and why.
5. Document
Same change, not later. Triggers: docs/README.md. Walk the propagation table above — every surface
done or explicitly n/a. Minimum:
docs/state.md: inventory rows, counters,verified againstline, latent items added/removed.docs/decisions.md: an ADR if the choice is expensive to reverse. Six lines. First open the doc that owns the topic — if it already carries the rule, amend it there; an ADR restating an existing doc is a duplicate, not a decision.- Any other doc only if the change made it wrong. Never state one fact in two docs.
Doc edits are surgical too. Prefer deleting a stale line to appending a corrected one.
Then sweep for what you replaced. Any rule, value, name or path you changed may be described elsewhere in the old terms:
grep -rn '<old form>' docs CLAUDE.md HARNESS.md ideas reference .claude scripts
verify.sh fails on dangling file paths, ADR numbers and CLAUDE.md section refs. It cannot detect
a sentence that is merely now untrue, or a concept renamed in one place — that sweep is the author's,
and skipping it is how a doc ends up contradicting the file it points at.
6. Report
Short — one line each, no prose unless a conflict or a stop condition needs explaining. Drop the lines that are genuinely n/a rather than padding them.
Did: what now works, in the user's terms
Evidence: the command you ran and its result
Diff: files touched, ±LOC
Propagated: content / code / templates / docs — each done or n/a, with content files named
Earned: counters after this change; anything now due for extraction
Skipped: what you deliberately did not do, and the latent items you logged
Conflicts: hard ones surfaced and how they were settled, soft ones deviated from, or "none"
Swept: the old form you grepped for after a rename, or "n/a"
Failure modes to catch in yourself
| Symptom | What it means | Do instead |
|---|---|---|
| Interface with one implementation | Anticipating, not designing | Concrete type; wait for the second |
| A parameter no caller passes | Speculative flexibility | Delete it |
| Reaching for a dependency | Usually 40 lines of stdlib | Write the 40 lines, or stop and ask |
Wanting a switch on post type in the core |
A View or Query is the seam | Move it out |
| Needing a mock to test | Wrong seam | fstest.MapFS or a testdata dir |
| Renaming "for clarity" mid-feature | Drive-by refactor | Latent list |
| Explaining why the boundary can bend here | It cannot (ADR-0003) | Stop and ask |
| Plan grew while implementing | Scope drift | Stop, re-plan, continue |
| Writing a rule without reading its owning doc | Guessing where you could look | docs/README.md topic table, then amend that doc |
| Quietly doing what was asked against a recorded decision | The conflict was real and you hid it | Quote the line, give both paths, wait |
| Changed the rule in code, left the docs describing the old one | Propagation stopped at the first surface | Walk all four surfaces |
| Made a field optional, left the ADR mandating it | Contradicted, not stale | Supersede the ADR |
| Treated a one-sentence request as a narrow change | Articulation size is not blast radius | Walk the propagation table |
| Splitting the difference between request and policy | A compromise nobody chose | Surface it; the human picks |