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:
@@ -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 |
|
||||
Reference in New Issue
Block a user