point the editor preview at the demo, and shorten the skill name

Four small things from one sitting, none of which would be reverted without the
others:

- .claude/launch.json describes the only dev server this repo has: make demo on
  localhost:8080. Deliberately not autoPort — examples/demo-site declares
  base: http://localhost:8080, so canonical, hreflang, OpenGraph and sitemap URLs
  are built from that port, and a reassigned one would make the absolute URLs on
  the page wrong while still rendering fine.
- The skill is .claude/skills/feature-loop/, without the khosra- prefix. ADR-0054
  records it, because ADR-0030 had named the prefixed form while settling the
  project name and decisions.md is append-only.
- Makefile .PHONY was missing quiet and surface, added two commits ago. A file of
  either name in the repo root would have silently shadowed the target.
- A latent row: the root listing's title reads "A Khosra Demo · A Khosra Demo",
  because base.html joins page title and site title unconditionally and at the
  root they are the same string. Found by looking at the served page — no test
  asserts a title. Theme layer, one if, and it waits for Phase G4.
This commit is contained in:
Claude Opus 5
2026-08-01 02:46:30 +06:00
committed by bdeshi
parent aad7d64270
commit 36194a16d8
7 changed files with 33 additions and 3 deletions
+218
View File
@@ -0,0 +1,218 @@
# 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
`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.
For code, read the compressed form before the source: `docs/surface.md` locates every declaration,
`go doc` gives a package's surface, a test states its contract. Disciplines: `docs/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 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 --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: `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 '<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. 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.
## 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 | `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 |
| Ending a turn with the work only in the working tree | The one copy is the one that gets lost | Commit before reporting |
| Opening a file to find out what is in it | The generated surface already answers it | `docs/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 |