commit as part of the loop, and announce it when unasked

The loop gains a seventh step. Work was being committed all session on a
verbal instruction, which means it was a property of one conversation
rather than of the harness: a fresh session would have left everything in
the working tree and reported success.

Why automatic: a commit is one `git revert` from undone, and work that
only ever existed in the working tree is not recoverable — this session
already lost uncommitted work to a `git checkout` while signing was
broken. Why announced: the safety argument covers the loss, not the
surprise, so a session that did not ask for this gets one prominent line
before the first commit and the off switch, once.

Ownership split so nothing is stated twice: conventions.md "Git" owns
what one commit contains — now one *revertible* unit rather than one
feature, which is the human's correction to the "never bundle two passes"
reading — and CLAUDE.md §4 owns when a commit happens. ADR-0052 records
both, including that no gate can check this: no script can see whether a
sentence was said.

Docs 6 files, +72/-6 lines. No code, no counters moved.
This commit is contained in:
Claude Opus 5
2026-08-01 01:41:11 +06:00
committed by bdeshi
parent de1ce73430
commit 8256c72180
6 changed files with 92 additions and 9 deletions
+40 -2
View File
@@ -1,6 +1,6 @@
# The feature loop # The feature loop
Six steps, in order: **Clarify → Plan → Implement → Verify → Document → Report.** Seven steps, in order: **Clarify → Plan → Implement → Verify → Document → Commit → Report.**
`CLAUDE.md` holds the rules; this file holds the procedure. `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 Scale, do not skip. A one-sentence request gets one line of plan and a short report — and still gets
@@ -135,7 +135,41 @@ grep -rn '<old form>' docs CLAUDE.md HARNESS.md ideas reference .claude scripts
a sentence that is merely now untrue, or a concept renamed in one place — that sweep is the author's, 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. and skipping it is how a doc ends up contradicting the file it points at.
## 6. Report ## 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 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. that are genuinely n/a rather than padding them.
@@ -145,6 +179,7 @@ Did: what now works, in the user's terms
Evidence: the command you ran and its result Evidence: the command you ran and its result
Diff: files touched, ±LOC Diff: files touched, ±LOC
Propagated: content / code / templates / docs — each done or n/a, with content files named 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 Earned: counters after this change; anything now due for extraction
Skipped: what you deliberately did not do, and the latent items you logged 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" Conflicts: hard ones surfaced and how they were settled, soft ones deviated from, or "none"
@@ -171,3 +206,6 @@ Swept: the old form you grepped for after a rename, or "n/a"
| Made a field optional, left the ADR mandating it | Contradicted, not stale | Supersede the ADR | | 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 | | 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 | | 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 |
| 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 |
+15 -2
View File
@@ -79,8 +79,8 @@ the test: `docs/architecture.md`.
## 4. The loop (every request, no exceptions) ## 4. The loop (every request, no exceptions)
`Clarify → Plan → Implement → Verify → Document → Report`. `Clarify → Plan → Implement → Verify → Document → Commit → Report`.
Procedure: `.claude/skills/khosra-feature-loop/SKILL.md`. The two gates people skip: Procedure: `.claude/skills/khosra-feature-loop/SKILL.md`. The three gates people skip:
**Clarify.** Only questions whose answer changes the code or the bytes on disk. Max three, **Clarify.** Only questions whose answer changes the code or the bytes on disk. Max three,
batched, up front, each with a **bold** default so silence answers. Never about naming, formatting, batched, up front, each with a **bold** default so silence answers. Never about naming, formatting,
@@ -90,6 +90,18 @@ or anything `docs/conventions.md` decides. None to ask? State assumptions in one
ran (golden file, `curl`, test name, benchmark). Never report success from reading your own diff. ran (golden file, `curl`, test name, benchmark). Never report success from reading your own diff.
"Should work" is not a result. "Should work" is not a result.
**Commit.** Work lands in git without being asked, every time, before you report. What one commit
contains is `docs/conventions.md` "Git": the changes that would be reverted together, bundled
together. Never `git push`, and never `--no-verify` without saying why in the body. Asymmetry is the
whole reason — a commit is one `git revert` away from undone, while work that only ever existed in
the working tree is not recoverable at all.
**Announce it once per session, prominently, when it was not asked for.** If the human has not
already asked for automatic commits in *this* session, the first commit is preceded by a visible
line of its own — not a clause inside the report — saying that this change is being committed, that
later changes will be too, and that saying so turns it off. Silence is consent only after the notice
exists; before it, an unannounced commit is a surprise in someone else's repository.
## 5. Definition of done ## 5. Definition of done
- [ ] Plan's success criteria demonstrated with real output. - [ ] Plan's success criteria demonstrated with real output.
@@ -97,6 +109,7 @@ ran (golden file, `curl`, test name, benchmark). Never report success from readi
- [ ] Diff contains nothing outside the planned files. - [ ] Diff contains nothing outside the planned files.
- [ ] `docs/state.md` updated (inventory, counters, latent items, verified-at line). - [ ] `docs/state.md` updated (inventory, counters, latent items, verified-at line).
- [ ] ADR in `docs/decisions.md` if a load-bearing choice was made. - [ ] ADR in `docs/decisions.md` if a load-bearing choice was made.
- [ ] Committed — one revertible unit per commit, announced if this session did not ask for it.
- [ ] Report states LOC delta, what is now earned, what you deliberately did not do. - [ ] Report states LOC delta, what is now earned, what you deliberately did not do.
## 6. Stop conditions — halt and ask ## 6. Stop conditions — halt and ask
+11 -3
View File
@@ -12,7 +12,7 @@ engine — anything in it may be pulled into context on demand. `ideas/` and `re
## How you use it ## How you use it
You ask for a feature. The skill fires by itself and runs You ask for a feature. The skill fires by itself and runs
`Clarify → Plan → Implement → Verify → Document → Report`. `Clarify → Plan → Implement → Verify → Document → Commit → Report`.
This runs for casual requests too — the skill fires on "the default-language files do not need the This runs for casual requests too — the skill fires on "the default-language files do not need the
`.en` part" exactly as it does on "add tag pages", with a one-line plan instead of a full one. A short `.en` part" exactly as it does on "add tag pages", with a one-line plan instead of a full one. A short
@@ -26,7 +26,7 @@ need to run there rather than pretending to have done it.
3. **Agent:** a short plan — goal, primitive, success criteria, files, ±LOC, deps, earn-it check, 3. **Agent:** a short plan — goal, primitive, success criteria, files, ±LOC, deps, earn-it check,
and what it is deliberately *not* doing. and what it is deliberately *not* doing.
4. **You:** "go", or edit the plan. "Just do it" skips the gate on small changes. 4. **You:** "go", or edit the plan. "Just do it" skips the gate on small changes.
5. **Agent:** implements, runs `./scripts/verify.sh`, shows real output, updates docs, reports. 5. **Agent:** implements, runs `./scripts/verify.sh`, shows real output, updates docs, commits, reports.
Occasional maintenance, by you: Occasional maintenance, by you:
@@ -70,6 +70,14 @@ change detection. Treat a *second* raise as evidence that something belongs in `
two ceilings exist so that "core stops growing, ext rises" is observable, and that stops being true the moment two ceilings exist so that "core stops growing, ext rises" is observable, and that stops being true the moment
leaves are allowed into core. leaves are allowed into core.
**The agent commits by itself, and tells you the first time.** Work is committed as part of the loop
rather than on request (ADR-0052) — one commit per revertible unit, never a `push`. In a session where
you have not asked for that, the first commit is preceded by a line saying so and naming the off
switch ("stop committing"), because a commit appearing in your repository unannounced is a surprise
even when it is harmless. The asymmetry is the argument: a commit you did not want is one `git revert`
away, and work that existed only in the working tree when something went wrong is gone. If you want
the tree left dirty, say so and the loop stops at Document.
**What is staged has to build, not just the working tree.** Every other gate looks at your files; this one **What is staged has to build, not just the working tree.** Every other gate looks at your files; this one
looks at the commit you are about to make, by building a throwaway checkout of the index. It exists because looks at the commit you are about to make, by building a throwaway checkout of the index. It exists because
staging a subset — a changed signature without its caller — produced a commit that did not compile while staging a subset — a changed signature without its caller — produced a commit that did not compile while
@@ -90,7 +98,7 @@ In order, cheapest first:
`docs/state.md` has fallen behind the code. `docs/state.md` has fallen behind the code.
2. `docs/state.md` — what exists, the earn-it counters, the latent list, the commit it was last verified 2. `docs/state.md` — what exists, the earn-it counters, the latent list, the commit it was last verified
against. This is the only doc that describes the present. against. This is the only doc that describes the present.
3. `git log --oneline` — one feature per commit, each body saying *why* (`conventions.md`). This is the 3. `git log --oneline` — one revertible unit per commit, each body saying *why* (`conventions.md`). This is the
real map of how the code got here. real map of how the code got here.
4. `/refresh-docs` — reconciles every doc against the actual code and reports drift, which is exactly the 4. `/refresh-docs` — reconciles every doc against the actual code and reports drift, which is exactly the
question you have after a year. question you have after a year.
+1
View File
@@ -40,6 +40,7 @@ that is the finding to report.
| whether a future feature is worth building | `exploration.md` + `roadmap.md` | | whether a future feature is worth building | `exploration.md` + `roadmap.md` |
| what to build next | `.scratch/build-queue.md` if present, otherwise `git log` and `roadmap.md` | | what to build next | `.scratch/build-queue.md` if present, otherwise `git log` and `roadmap.md` |
| how the harness works — a gate, counter, budget, the loop | `HARNESS.md` + `scripts/verify.sh` + `CLAUDE.md` | | how the harness works — a gate, counter, budget, the loop | `HARNESS.md` + `scripts/verify.sh` + `CLAUDE.md` |
| what one commit contains, and when one happens | `conventions.md` "Git" (shape) + `CLAUDE.md` §4 (cadence, ADR-0052) |
| a tool version, or why agent tooling stopped working | `toolchain.md` | | a tool version, or why agent tooling stopped working | `toolchain.md` |
| templates, layout, presentation, what a theme can rely on | `theme-contract.md` (+ ADR-0019, ADR-0023) | | templates, layout, presentation, what a theme can rely on | `theme-contract.md` (+ ADR-0019, ADR-0023) |
+7 -2
View File
@@ -100,5 +100,10 @@ Page-specific styles and scripts come from the bundle (`styles`/`scripts` frontm
this project, not an optimisation. this project, not an optimisation.
## Git ## Git
One feature per commit. Imperative subject under 72 characters; body says *why*. Doc updates ride in One revertible unit per commit: what would be undone together belongs together, what would be undone
the same commit as the code that made them true. alone gets its own commit. Usually that is one feature — code, test, `state.md` row and ADR in one
commit, because reverting the code without the doc leaves a lie. A correction to something that was
already wrong beforehand is a separate unit even when it lands in the same sitting.
Imperative subject under 72 characters; body says *why*, and carries the numbers the change earned.
Committing happens by itself and needs no request (`CLAUDE.md` §4); pushing never does.
+18
View File
@@ -770,3 +770,21 @@ JPEGs, and a feature added without a case in the coverage test is still invisibl
what "done" means. what "done" means.
Revisit if: the example grows big enough to slow the test suite, or someone wants several examples — then this Revisit if: the example grows big enough to slow the test suite, or someone wants several examples — then this
is `examples/<name>/` with the coverage test parameterised, not a second mechanism. is `examples/<name>/` with the coverage test parameterised, not a second mechanism.
## ADR-0052 — Committing is part of the loop, and unasked-for automation is announced once
Date: 2026-08-01 · Status: accepted
Decision: the loop gains a seventh step — the agent commits its own work before reporting, one revertible
unit per commit, never pushing. In a session where the human has not asked for it, the first commit is
preceded by a prominent line naming what is happening and how to stop it; after that, silence is consent.
Why: the human's reason, and it is the right one — a commit is trivially reverted, while work that only ever
existed in the working tree cannot be recovered with certainty. This session lost uncommitted work once
already, to a `git checkout` while signing was broken. The notice exists because the safety argument covers
the *loss*, not the *surprise*: committing into someone's repository without having said you would is a
different failure from committing too much.
Consequence: cheap — every step of a long autonomous run is bisectable, and a bad turn costs one revert
instead of a reconstruction. Expensive — `git log` now carries the agent's pacing, so a sloppy unit boundary
is permanent noise; and this is behaviour no gate can check, since no script can see whether a sentence was
said. `conventions.md` "Git" owns what one commit contains; `CLAUDE.md` §4 owns when one happens.
Revisit if: the log fills with commits nobody would revert separately — then the unit is wrong, not the
automation; or the human wants a review gate before anything lands, which is a different default, not a
tweak to this one.