One idea at a time: definition, necessity, priority, layer, recommendation. Every row is now shipped, scheduled, parked with a trigger, or dropped with a reason — so both tracking files are gone rather than left as a parallel backlog. Roughly a third already shipped. A quarter needed only a theme fragment or CSS, including several the list assumed were features: arbitrary `theme.*` frontmatter attributes already reach templates through Extra (verified), native lazy loading already ships and is deliberately absent on lead figures, and in-page timelines are a theme-defined container. roadmap.md gains an "Order of work" — nine bodies of work in sequence, because the sections this review added had accumulated without one. Item 0 is a decision, not work: core sits at ~2965 of 3000 and logging alone wants the remainder, so discover.go moves out *and* the ceiling rises with an ADR. feed.go and web/extras.go cannot move; they are resolver cases, and three parked items now wait on that same seam decision. Two ADRs came out of it. ADR-0083: the dependency rule described a practice it forbade. "Stdlib first, always" and "usually 40 lines of stdlib" would, read literally, have argued against all five modules here — every one is a format or algorithm somebody else specified, and their allowlist comments say so. Surfaced when this agent proposed hand-rolling Reed–Solomon for QR encoding. The test is now whether you can verify it, not whether you can write it, with compute-versus-hold as the counterweight. conventions.md and allowed-deps.txt contradicted it and were realigned. ADR-0084: the Effect primitive said every Effect is "idempotent and re-runnable from scratch" — true of derivatives, false of anything outbound, since a sent message cannot be recalled. Split into artifact-producing and outbound, the latter idempotent only against a delivery ledger, at-least-once with a dedupe key, and ledgers in a -state directory distinct from the disposable cache. Asked for as forward-looking design so webmentions need no redesign; recorded as a shape rather than built, the way extensions.md records the Extension struct. Also fixed a STATUS claiming Effects were "not buildable yet" when the derivative pass has inhabited them since ADR-0042. Findings that were defects rather than ideas: the Atom feed emits no <author>, which RFC 4287 requires. HTML comments in content are published verbatim — verified — so anything commented out is already public, and a check warning is scheduled rather than the engine deleting authored bytes. `Page` has no Date, so a theme cannot mark up dt-published or show an article's own date. A frontmatter naming rule after this agent proposed `archive` alongside the accepted `archived`: no near-homograph keys, and name the benefit rather than the vendor. reference/microformats-and-indieweb.md is new and is the reference asked for — microformats2 properties and IndieWeb rel values mapped to where a theme puts them, with spec URLs, marked scheduled. rel="me" is the highest-value lowest-cost item in that space and needs no endpoint at all. Four things this agent got wrong and the human caught: asserting "the engine never fetches" from a sentence scoped to the content repo, twice; inventing a data-sovereignty argument for an item he had filed as "a cool geeky thing", when the raw Markdown in his git already is the sovereignty; the archive naming; and claiming microformats could be fully implemented today. 15 files, +556/-385. No code changed. Nothing was scheduled that this agent could not name a consumer for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
237 lines
15 KiB
Markdown
237 lines
15 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 — but ask every one of those. Batched, one message, up front, each with a **bold**
|
|
default so silence answers. **There is no cap** (`CLAUDE.md` §4): a request carrying six real forks gets
|
|
six questions, and splitting them across turns to look brisk wastes more of the human's time than asking
|
|
once. This file said "maximum three" until the cap was lifted in `CLAUDE.md` and not here.
|
|
|
|
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 for something small | Usually 40 lines of stdlib | Write the 40 lines, or stop and ask |
|
|
| Hand-rolling something specified elsewhere | A bug farm whose test vectors you must also transcribe | Take the maintained module (ADR-0083), with its ADR |
|
|
| 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 |
|