# 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 §6` stop 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. 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 against` line, 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 '' 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 |