Files
khosra/.claude/skills/feature-loop/SKILL.md
T
bdeshiandClaude Opus 5 ec6e9838f0 rename docs/ to harness/, and reserve docs/ for the reader
docs/content-model.md opens with "Engine specification". It is also where the
rule lives that a leading underscore makes a file unaddressable — and the human
who owns this site did not know that rule, because nothing in this repository is
addressed to an author. Twelve documents named docs/ while being exclusively
about building the parser is a signpost pointing at the wrong room.

Naming the directory for its audience makes the gap visible instead of hiding it.
docs/ is now reserved and deliberately absent: an empty docs/ is an honest
statement that end-user documentation does not exist, where docs/ full of parser
specs was a claim that it did.

HARNESS.md stays at the root. Root holds the three entry points — README.md for a
human, CLAUDE.md for an agent, HARNESS.md for whoever maintains the machine — and
harness/README.md is the map of the directory, so moving the guide inside would
have collided with it for nothing.

Mechanical and wide: 100 path references across 24 files. Every verify.sh gate
that names a doc by path, the directory lists the dangling-path and ADR-number
gates scan, surface.sh's output target, the Makefile, CLAUDE.md's read order, the
skill, four commands, and two Go package comments. A first pass with a shell loop
silently edited only four files and the rest still said docs/; the fix was to
write the file list out and check the remaining count was zero rather than trust
the loop's exit status.

No rule, threshold, gate or obligation moved — this is a rename, and the gates
demonstrated it twice: they stayed green on the new paths, and the ADR-number gate
caught ADR-0082 before the entry existed.

Deferred, both on the human's call: the end-user documentation site itself, which
wants its own decision about where it lives and whether its claims are gated; and
moving examples/ under docs/, since demo-site is a live site root that verify.sh,
the coverage test and make demo all point at, and moving it would couple a rename
to a design nobody has made.

31 files, +146/-106.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:11:06 +06:00

233 lines
14 KiB
Markdown

# The feature loop
Seven steps, in order: **Clarify → Plan → Implement → Verify → Document → Commit → 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
`harness/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 `harness/state.md`, then `harness/README.md` — the map, always, before deciding what else to open.
**A handoff is not evidence.** If `.scratch/continue.md` says something is pending, waiting, or unfinished,
run `git log --oneline -- <the path it names>` before planning from it. The file is uncommitted, so no gate
compares it to anything, and it is written *during* a change whose commit can still move underneath it. The
log is the only record of what landed. Offering the human work that is already in `HEAD` wastes the turn
they asked the question in.
For code, read the compressed form before the source: `harness/surface.md` locates every declaration,
`go doc` gives a package's surface, a test states its contract. Disciplines: `harness/context-economy.md`.
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 | a `_WARN` threshold, a style-floor preference, a latent item's trigger, a shape sketched in `ideas/` | 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 more of something a doc already describes as built | 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 `harness/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 --quiet` — doc coupling, format, vet, build, test, dependency allowlist,
budgets. Quiet prints only failures, warnings and the verdict; use the loud form when a gate fails and
you want its neighbours, or when auditing the gates themselves.
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: `harness/README.md`. Walk the propagation table above — every surface
done or explicitly n/a. Minimum:
- `harness/state.md`: inventory rows, counters, latent items added/removed — in *this* commit, never a
trailing one, which is what `verify.sh` compares (ADR-0057).
- `harness/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>' harness 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. Commit
Not optional and not asked for: the work is committed before the report, every time. `CLAUDE.md` §4
holds the rule; this is how.
**The notice, if this session did not ask for automatic commits.** Before the first commit only, on
its own line, visible:
```
**Committing this to git** — and every later batch of changes in this session. Say "stop committing"
and I will leave the tree dirty instead.
```
Once said, it is not repeated. If the human asked for automatic commits — in this session or by
having put the rule here — skip the notice entirely; repeating a notice for something already agreed
is noise that trains them to skim the next one.
**What goes in one commit.** The changes that would be reverted together (`conventions.md` "Git").
Bundle freely inside one unit: code with its test, its `state.md` row and its ADR are one commit
because reverting the code without the doc leaves a lie. Split when a second pass is independently
revertible — a fix to something you noticed afterwards, or a doc correction that was already wrong
before this change. "Which of these would I want to undo alone?" answers it faster than any rule.
**How.**
- `git status` first: an unexpected file is a stop condition, not something to sweep into `git add -A`.
- Stage what the change touched, then check `git diff --cached --stat` against the plan's file list.
- The pre-commit hook runs `verify.sh`, so a red gate blocks the commit. That is the design — fix the
gate or the code, do not reach for `--no-verify`.
- Subject imperative, under 72 characters. Body says *why*, plus the numbers the change earned
(LOC, counters, a benchmark) since `git log` is the map when no agent is available.
- Never `git push`. Publishing is the human's; committing is bookkeeping.
- Scratch files, `ideas/`, `reference/` and anything under the scratchpad stay out.
**Then reconcile the handoff — once the commit exists, never before.** If `.scratch/continue.md` carried a
planned item this change completed, delete that item now and note anything learned that changes a later
one. Doing it during Document records the plan's version of events instead of the result: an amend, a
squash or a late fix moves the commit under a handoff that already claims what it contains.
## 7. 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
Committed: the commit subjects this produced, one line each
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 | `harness/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 |
| Ending a turn with the work only in the working tree | The one copy is the one that gets lost | Commit before reporting |
| Offering work a handoff calls pending | The file states intent; only the log states what landed | `git log -- <path>` before trusting it |
| Ticking the handoff during Document | The commit can still move under it | Reconcile it after the commit exists |
| Opening a file to find out what is in it | The generated surface already answers it | `harness/surface.md`, then read the range |
| Grepping for callers before changing a signature | The compiler enumerates them exactly | Change it, then `go build ./...` |
| A shorter report that dropped a caveat, case or number | Truncation wearing compression's clothes | Restore it; cut words, never findings |
| One commit per file, or per doc touched | Shredding a single revertible unit | Bundle what would be undone together |
| Committing without ever saying you would | Consent assumed, not given | The notice, once, before the first one |