stop the handoff outranking the log, and gate the pointers into it

A session picked up work from .scratch/continue.md and offered the human a doc
trim that was already in HEAD. The handoff was written at 01:06 saying the trim
awaited a yes; the commit containing the trim was amended at 01:12, underneath
it. Five defects made that possible, and four of them are the harness's.

The read order inverted trust: docs/README.md sent the next session to the
handoff "if present, otherwise git log" — consulting the one ungated file
instead of the record that cannot lie, which is backwards from every other rule
here, where the code beats state.md and generated surface.md beats memory. The
handoff is now step 4 of CLAUDE.md's read order, explicitly after the harness
and never instead of it, and anything it calls pending is checked with
git log -- <path> first.

The tick fired too early. "A planned item completed → tick it in continue.md"
sat in the Document step, so it recorded the plan's version of events while the
commit could still move; an amend, a squash or a late fix moves it. Reconciling
now happens after the commit exists, and a completed item is deleted rather than
ticked, because a ticked item still reads as an item.

HARNESS.md authorised the drift outright — it said the file holds "what is
done", which is exactly what continue.md's own header promises it never records.
It is now described as what it is: a temporary handoff, uncommitted, ungated,
discardable, holding the continuation point and the carried findings.

state.md still pointed at .scratch/build-queue.md, replaced two commits earlier.
The dangling-path gate missed it because .scratch was absent from its
alternation, so no pointer into the handoff directory was ever checked. It is
included now, guarded on the directory existing — a fresh clone has no handoff
and must stay green. Both directions were run: present with a stale pointer
fails, absent with docs naming it passes. The gate then flagged its own
explanatory comment, which is why that one path is written without backticks.

No gate can compare an uncommitted file against anything, so the file's job is
narrowed instead of enforced, and HARNESS.md now says which mechanism stands in
for the missing gate rather than implying one exists.

6 files, +47/-9. No rule, threshold or counter moved. CLAUDE.md 138 → 142 of 150.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 16:53:58 +06:00
co-authored by Claude Opus 5
parent 1510a5ff7a
commit 8f5d479f06
6 changed files with 47 additions and 9 deletions
+13
View File
@@ -29,6 +29,12 @@ a layout or markup decision dressed as an engine feature. |
## 1. Clarify
Read `docs/state.md`, then `docs/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: `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,
@@ -174,6 +180,11 @@ before this change. "Which of these would I want to undo alone?" answers it fast
- 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
@@ -212,6 +223,8 @@ Swept: the old form you grepped for after a rename, or "n/a"
| 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 | `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 |
+6 -2
View File
@@ -14,8 +14,12 @@ as aesthetic, comprehensibility by one person.
1. This file.
2. `docs/state.md` — what exists **right now**, plus the earn-it counters.
3. `docs/README.md` — always. The map: how you find which doc owns your topic. Not "exceeding".
4. Every doc owning a topic your change asserts a rule about (ownership table in `docs/README.md`).
5. Only the source files you will edit, plus their direct callers.
4. `.scratch/continue.md`**after** the harness above, never instead of it, and only when picking work up
rather than answering a named request. A *temporary* handoff: uncommitted, ungated, discardable, holding
the continuation point and nothing else. Anything it calls pending is a claim about the past — check it
against `git log` first, and where they differ the log wins the way the code wins over `state.md`.
5. Every doc owning a topic your change asserts a rule about (ownership table in `docs/README.md`).
6. Only the source files you will edit, plus their direct callers.
No reading the repo "for context", no speculative greps. Where `docs/state.md` and the code
disagree, the code wins — say so, fix the doc in Document.
+14 -2
View File
@@ -8,8 +8,10 @@ constitution (always loaded). `docs/` is what the agent needs to build the
engine — anything in it may be pulled into context on demand. `ideas/` and `reference/` sit outside
`docs/` deliberately: storage, opened only when you name a file, swept by nothing.
`.claude/` holds the feature-loop skill (`skills/feature-loop/`), the commands, and `launch.json`.
`scripts/` holds the gate. `.scratch/continue.md` is uncommitted and holds the plan: what is done, what is
next, and the findings worth carrying that no doc owns.
`scripts/` holds the gate. `.scratch/continue.md` is a **temporary** handoff — uncommitted, ungated,
discardable — holding the continuation point and the findings worth carrying that no doc owns. It is read
after the harness rather than instead of it (`CLAUDE.md` §1), and never records what landed: that is
`git log`'s alone, for the reason under "Why the pieces exist".
## How you use it
@@ -176,6 +178,16 @@ lives in its own repository and is passed to the binary with `-site` (ADR-0011),
is the engine's own job rather than the gate's. The gate skips `ideas/`, `reference/` and the uncommitted
`.scratch/`, so exploratory code parked there never has to compile.
**The handoff is the one state-describing file no gate can check, so its job is narrowed instead.**
`state.md`'s currency is compared against the last `.go` commit (ADR-0057), `surface.md` is generated and
diffed, the theme contract and this file are coupled to what they describe. `.scratch/continue.md` can have
none of that: it is gitignored, so there is no commit to compare it against. Two things stand in for the
gate it cannot have — it holds the plan and the carried findings but never what landed, and the read order
puts `git log` first with the handoff read against it (`docs/README.md`). The one check that *is* mechanical
now runs: a `.scratch/` path named in a doc must exist, guarded on the directory being present so a fresh
clone with no handoff still passes. A handoff that recorded what landed was believed for a whole session —
it said a doc trim awaited a yes while the commit being amended around it had already done the trim.
**The architecture gate.** `verify.sh` enforces the layering in `conventions.md` from `go list`
output: content imports no sibling, render imports neither web nor ext, web imports no ext, nothing
imports `cmd`. One convenient sibling import is what turns a layered engine into a ball of mud, and
+2 -2
View File
@@ -40,7 +40,7 @@ that is the finding to report.
| language, translation, fallback | ADR-0004 + ADR-0009 |
| deploy, containers, external services | ADR-0010 |
| whether a future feature is worth building | `ideas/exploration.md` + `roadmap.md` |
| what to build next | `.scratch/continue.md` if present, otherwise `git log` and `roadmap.md` |
| what to build next | the harness first (`CLAUDE.md` §1 read order) and `git log` for what landed, **then** `.scratch/continue.md` for the continuation point and `roadmap.md` for the arc. The handoff is temporary and ungated: check anything it calls pending with `git log -- <the path it names>` before planning from it |
| 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) |
| how to read, search, or report without wasting context | `context-economy.md` (+ ADR-0053) |
@@ -86,7 +86,7 @@ Update in the Document step of the change that caused them. Not later.
| New dependency approved | `scripts/allowed-deps.txt` + ADR |
| Budget raised | `scripts/budgets.env` + ADR — once the first feature has shipped. While the harness is still being tuned, edit in place. |
| A latent item fixed | remove from the Latent list in `state.md` |
| A planned item completed | tick it in `.scratch/continue.md` and note anything learned that changes a later one — uncommitted, so the commit message carries what landed |
| A planned item completed | **after** the commit lands, delete it from `.scratch/continue.md` and note anything learned that changes a later one. Never during Document: a handoff written mid-change describes the plan, and an amend moves the commit under it — which is how one was written at 01:06 saying a trim awaited a yes that the 01:12 commit had already done |
| Arc finished | `roadmap.md` + a retro line in `state.md` |
| Catalog item accepted or rejected | verdict line in `ideas/exploration.md` |
| `CLAUDE.md`, `scripts/` or `.claude/` changed | `HARNESS.md`, same change. Enforced by `verify.sh`. |
+3 -2
View File
@@ -141,8 +141,9 @@ Every ADR in `decisions.md` is accepted; none is open or proposed.
## Build queue
Working plan lives in `.scratch/build-queue.md`, which is deliberately not committed — `git log` is the
record of what actually landed. If that file is absent, read the log and rebuild the plan from it.
Working plan lives in `.scratch/continue.md`, which is deliberately not committed — `git log` is the
record of what actually landed, and the handoff is read *against* the log rather than instead of it
(`docs/README.md`). If that file is absent, read the log and rebuild the plan from it.
## Arc retro log
+9 -1
View File
@@ -132,7 +132,15 @@ if [ -d .git ] && command -v git >/dev/null 2>&1; then
fi
# Dangling references. Every one of these found a real stale pointer when run by hand.
refs=$(grep -rhoE '`(docs|scripts|ideas|reference|\.claude)/[A-Za-z0-9_./-]+`' \
# `.scratch/` is included only when it exists: it is gitignored, so on a fresh clone the handoff is
# legitimately absent (docs/README.md says "if present") and checking it there would fail correct
# work. Once the directory is present, a named file missing from it is a stale pointer like any
# other — which is how state.md's pointer to a deleted build-queue outlived it by two commits. That
# path is named without backticks on purpose: this gate reads its own file, so the example would be a
# reference, and the gate would flag the comment explaining it.
scratchpat=""
[ -d .scratch ] && scratchpat='|\.scratch'
refs=$(grep -rhoE "\`(docs|scripts|ideas|reference|\.claude$scratchpat)/[A-Za-z0-9_./-]+\`" \
docs CLAUDE.md HARNESS.md ideas reference .claude scripts 2>/dev/null | tr -d '`' | sort -u)
dangling=""
for f in $refs; do [ -e "$f" ] || dangling="$dangling $f"; done