liteagents 2.22.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +170 -1
- package/README.md +7 -5
- package/package.json +1 -1
- package/packages/ampcode/commands/branch-review.md +180 -14
- package/packages/ampcode/commands/docs-builder/docs-builder.cjs +25 -8
- package/packages/ampcode/commands/refactor.md +71 -3
- package/packages/ampcode/commands/release.md +49 -9
- package/packages/ampcode/commands/remember/AGENT_RULES.md +12 -3
- package/packages/ampcode/commands/remember.md +40 -7
- package/packages/ampcode/commands/ship.md +16 -0
- package/packages/claude/commands/branch-review.md +180 -14
- package/packages/claude/commands/docs-builder/docs-builder.cjs +25 -8
- package/packages/claude/commands/refactor.md +71 -3
- package/packages/claude/commands/release.md +49 -9
- package/packages/claude/commands/remember/AGENT_RULES.md +12 -3
- package/packages/claude/commands/remember.md +40 -7
- package/packages/claude/commands/ship.md +16 -0
- package/packages/droid/commands/branch-review.md +180 -14
- package/packages/droid/commands/docs-builder/docs-builder.cjs +25 -8
- package/packages/droid/commands/refactor.md +71 -3
- package/packages/droid/commands/release.md +49 -9
- package/packages/droid/commands/remember/AGENT_RULES.md +12 -3
- package/packages/droid/commands/remember.md +40 -7
- package/packages/droid/commands/ship.md +16 -0
- package/packages/opencode/command/branch-review.md +180 -14
- package/packages/opencode/command/docs-builder/docs-builder.cjs +25 -8
- package/packages/opencode/command/refactor.md +71 -3
- package/packages/opencode/command/release.md +49 -9
- package/packages/opencode/command/remember/AGENT_RULES.md +12 -3
- package/packages/opencode/command/remember.md +40 -7
- package/packages/opencode/command/ship.md +16 -0
|
@@ -56,20 +56,48 @@ separate command that must have run first.
|
|
|
56
56
|
A review must have run on this branch **at the current HEAD SHA**.
|
|
57
57
|
|
|
58
58
|
**Compare the SHAs yourself; do not settle for an answer.** Run `git rev-parse
|
|
59
|
-
HEAD` and compare it against the
|
|
60
|
-
|
|
61
|
-
puts the question to the one party with an
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
HEAD` and compare it against the `sha:` line in
|
|
60
|
+
`.claude/remember/last-review.md`, which `/branch-review` writes. Asking the
|
|
61
|
+
orchestrator "did a review run?" puts the question to the one party with an
|
|
62
|
+
incentive to say yes, so its word is not evidence — and neither is a SHA
|
|
63
|
+
quoted from a chat message, which is the same claim in another costume and is
|
|
64
|
+
gone after a compaction or a handover. Read the file; match the two strings.
|
|
65
|
+
**No such file, or no `sha:` line in it = no review**, never a pass. A review
|
|
66
|
+
that predates this file's introduction has no record, so it does not count.
|
|
64
67
|
|
|
65
68
|
- **No review**, or no recorded SHA obtainable → **stop**: "No review at
|
|
66
69
|
`<sha>`. Run `/branch-review medium` (or `/code-review medium`) first."
|
|
67
70
|
- **Stale** — recorded SHA ≠ `git rev-parse HEAD`, i.e. commits landed after
|
|
68
71
|
the review (including fix commits) → **stop** and ask for a re-review. This
|
|
69
72
|
is what makes "all findings fixed" checkable instead of promised.
|
|
73
|
+
**No exceptions — including the fix ledger.** It is normally gitignored, so
|
|
74
|
+
appending to it moves nothing and this never comes up. A repo that tracks
|
|
75
|
+
`.claude/` instead will see a ledger commit land after the review and make
|
|
76
|
+
it stale. That is the rule working, not a case to carve out: re-review, or
|
|
77
|
+
leave the ledger uncommitted until after the release.
|
|
78
|
+
- **`coverage:` naming any stage `NOT RUN`** → **stop**. A `ready` from a run
|
|
79
|
+
that skipped the security stage is not the same fact as one that did not,
|
|
80
|
+
and this line is the only place the difference is visible to you.
|
|
81
|
+
- **`verdict: blocked` in the record** → **stop**, even when the SHA matches.
|
|
82
|
+
Read that line as mechanically as the `sha:` one. A matching SHA proves a
|
|
83
|
+
review ran here; it says nothing about what the review concluded, and
|
|
84
|
+
leaving the conclusion to the orchestrator's recollection restores exactly
|
|
85
|
+
the unverified claim this file replaced. Only `verdict: ready` with a
|
|
86
|
+
matching SHA is a pass.
|
|
70
87
|
- **Reviewed at this SHA with findings outstanding** → **stop**. Findings are
|
|
71
88
|
resolved before a release is cut.
|
|
72
89
|
|
|
90
|
+
This phase runs **before** `/release` writes anything, so the docs-and-bump
|
|
91
|
+
commit it makes later cannot invalidate the review it just checked. That
|
|
92
|
+
If Phase 2's docs sweep happens to correct a line that a fix-ledger bullet
|
|
93
|
+
also names, that is ordinary sweep work — the doc changed with the feature,
|
|
94
|
+
so it was already yours to update. **Do not delete the bullet.** `/refactor`
|
|
95
|
+
is the only deleter, and its revalidation will drop that bullet on its next
|
|
96
|
+
run when it finds the finding no longer holds. Deleting it here would make
|
|
97
|
+
`/release` a second writer on state that has exactly one owner, and the whole
|
|
98
|
+
value of the ledger's one-append-one-delete split is that it stays readable
|
|
99
|
+
as a log.
|
|
100
|
+
|
|
73
101
|
Report the comparison you actually ran: recorded `<sha>` vs HEAD `<sha>`,
|
|
74
102
|
match yes/no.
|
|
75
103
|
|
|
@@ -141,15 +169,27 @@ orchestrator can run them on the user's named go:
|
|
|
141
169
|
> Ready when you are:
|
|
142
170
|
> 1. `git push -u origin <branch>`
|
|
143
171
|
> 2. `gh pr create` into `main`
|
|
144
|
-
> 3. `gh pr
|
|
172
|
+
> 3. `gh pr checks <pr> --watch` — **merge only on green.** Every gate before
|
|
173
|
+
> this one ran on the same machine; CI is the only differently-configured
|
|
174
|
+
> instrument in the chain, and this is the first time it sees the branch.
|
|
175
|
+
> A test that passes locally because of a path, a fixture, or a tool that
|
|
176
|
+
> exists only on your box fails here and nowhere earlier. Read the exit
|
|
177
|
+
> code off the bare command. Red → stop, fix, re-review, and start again.
|
|
178
|
+
> 4. `gh pr merge --admin --squash --delete-branch` (main is PR-protected;
|
|
145
179
|
> owner-authorized admin merge on a solo repo). **Keep `--squash`** — `gh`
|
|
146
180
|
> requires an explicit merge-method flag (`--squash` / `--merge` /
|
|
147
181
|
> `--rebase`); drop it and the command will not squash-merge.
|
|
148
|
-
>
|
|
149
|
-
>
|
|
182
|
+
> 5. `git tag vX.Y.Z` on `main` and push the tag
|
|
183
|
+
> 6. Publish **if this project has a publish path** (e.g.
|
|
150
184
|
> `gh workflow run publish.yml`) — manual by design
|
|
151
|
-
>
|
|
185
|
+
> 7. Verify it is actually live (`npm view <pkg> version`, and the published
|
|
152
186
|
> tarball's contents), not the working tree
|
|
153
187
|
|
|
188
|
+
**Every exit code in this sequence is read off the bare command, including
|
|
189
|
+
the ones you type yourself.** `/ship`'s rule is not just for the worker: a
|
|
190
|
+
pipeline reports its last element's status, so `gh run watch --exit-status |
|
|
191
|
+
tail -2; echo $?` prints `0` for a failed run. That has already turned a red
|
|
192
|
+
CI into a green reading in a real release.
|
|
193
|
+
|
|
154
194
|
Final line: **Cut ✅ (vX.Y.Z — ready to push)** or **Blocked 🛑** with the
|
|
155
195
|
specific reason.
|
|
@@ -105,11 +105,14 @@ Before adding any external dependency, all of these must be true:
|
|
|
105
105
|
|
|
106
106
|
- **Open-source only.** Always use open-source solutions. No vendor lock-in
|
|
107
107
|
- **Lightweight over complex.** If two solutions solve the same problem, use the one with fewer moving parts, fewer dependencies, and less configuration
|
|
108
|
-
- **Every line
|
|
108
|
+
- **Every line earns its place.** If you can't say what breaks when it's deleted, delete it. No speculative code, no "might need this later", no abstractions for one use case. One function, one concern, one owner — small blocks beat spaghetti
|
|
109
109
|
- **Simple > clever.** Readable code that a junior can follow beats elegant code that requires a PhD to debug
|
|
110
|
+
- **One writer per piece of state.** One function assigns each field; everything else calls it. Grep who writes it before you write it. Ownership says *where*, not *when* — if a write can land from a callback, thread, or lifecycle, the reader must tell stale from fresh
|
|
111
|
+
- **Split the decision from the machinery.** A branch whose outcome matters, tangled with a framework, IO, or UI object, moves into a pure function; the framework class applies the result. Extract to pin a branch, not to raise coverage — a one-line delegation in its own file buys a test that cannot fail
|
|
112
|
+
- **Claims in comments must be checkable.** "The only place that writes X" is a claim — run the grep first, and expect the next reader to re-run it. A name search proves an edge exists, never that one doesn't
|
|
110
113
|
- **Containerize only when necessary.** Start with a virtualenv or bare metal. Docker adds value for deployment parity and isolation — not for running a script
|
|
111
114
|
- **Responsive web UI is mandatory in dev projects.** Any web UI must be usable on mobile by default — fluid layouts, viewport meta tag, breakpoints for narrow screens, no horizontal scroll. Test in DevTools device emulation before declaring a UI task done. POCs are exempt (validate the idea first), but the moment a POC graduates to a real project this becomes a hard requirement
|
|
112
|
-
- **Surgical changes only.** Touch what the task requires; nothing else. Don't "improve" adjacent code, comments, or formatting. Match existing style even if you'd do it differently. Only clean up orphans your own change created — leave
|
|
115
|
+
- **Surgical changes only.** Touch what the task requires; nothing else. Don't "improve" adjacent code, comments, or formatting. Match existing style even if you'd do it differently. Only clean up orphans your own change created. Dead code, nits, bugs you pass on the way: if it's inside or affects the code you're already changing, and the fix changes no behavior, fix it and say so. Otherwise report it — say what it costs to leave it. "It would be nicer" is not a cost. Every changed line traces to the request or to a fix you named
|
|
113
116
|
|
|
114
117
|
### Red Flags — Stop and Flag These
|
|
115
118
|
- Over-engineering simple problems
|
|
@@ -121,6 +124,8 @@ Before adding any external dependency, all of these must be true:
|
|
|
121
124
|
- Authoring a fixture/corpus that *guarantees* the result (a test that can't return the negative), or trusting a degenerate-looking number without auditing the harness for confounds — use real uncrafted data; the test must be able to fail
|
|
122
125
|
- Fitting a POC to pass (narrowed input, moved threshold, shrunk scope) instead of reporting the failure; starting module N+1 while module N is unproven
|
|
123
126
|
|
|
127
|
+
A problem you see and don't fix goes in the report, never in a comment. Comments are where findings go to be forgotten.
|
|
128
|
+
|
|
124
129
|
---
|
|
125
130
|
|
|
126
131
|
## Testing Standards
|
|
@@ -291,7 +296,11 @@ Copy this to any project's AGENT.md. These are mandatory rules, not suggestions.
|
|
|
291
296
|
|
|
292
297
|
**Lightweight over complex.** Fewer moving parts, fewer deps, less config. Express over NestJS, Flask over Django, unless the project genuinely needs the framework. Simple > clever. Readable > elegant.
|
|
293
298
|
|
|
294
|
-
**Open-source only.** No vendor lock-in. Every line of code
|
|
299
|
+
**Open-source only.** No vendor lock-in. Every line of code earns its place — if you can't say what breaks when it's deleted, delete it. No speculative code, no premature abstractions.
|
|
300
|
+
|
|
301
|
+
**One writer per piece of state.** One function assigns each field; everything else calls it. Grep who writes it before you write it — and if a write can land from a callback, thread, or lifecycle, the reader must tell stale from fresh.
|
|
302
|
+
|
|
303
|
+
**Surgical changes only.** Touch what the task requires. Dead code, nits, bugs you pass: if it's inside or affects the code you're already changing and the fix changes no behavior, fix it and say so — otherwise report it and say what it costs to leave it. A problem you don't fix goes in the report, never in a comment.
|
|
295
304
|
|
|
296
305
|
**Responsive web UI is mandatory.** Any web UI must work on mobile by default — fluid layouts, viewport meta, breakpoints, no horizontal scroll. Verify in DevTools device emulation before claiming a UI task is done. POCs exempt; real projects are not.
|
|
297
306
|
|
|
@@ -133,6 +133,14 @@ Reads all raw material (`.amp/stash/*.md` + `.amp/remember/friction/antigen_clus
|
|
|
133
133
|
Re-processing a stash whose episode is already filed must not create a near-duplicate
|
|
134
134
|
pair. Every older episode is **folded, then deleted**: its lesson becomes a fact (handed
|
|
135
135
|
to the rewrite above); the narrative is removed. No archive — git has the history.
|
|
136
|
+
**Specify the operation once.** The keep-10 rule is the rule; the set to remove is
|
|
137
|
+
*derived* from it, never supplied alongside it as a second list. Given both, an agent
|
|
138
|
+
applies both and removes their union — observed in the field: a run told to keep 10 and
|
|
139
|
+
handed a 5-entry delete list removed 7, and the 2 extras were never folded, so one
|
|
140
|
+
lesson left memory with nothing carrying it. **No episode is removed whose lesson has
|
|
141
|
+
not been folded into a fact first**, and the two sets must match: state the count
|
|
142
|
+
before, the count after, and name each episode removed. Removed-but-not-folded is a
|
|
143
|
+
defect to report, not a tidy-up.
|
|
136
144
|
- **Antigens section**: only update from friction output (step 4)
|
|
137
145
|
- Write merged result to `.amp/remember/MEMORY.md` in the format under step 5.
|
|
138
146
|
|
|
@@ -276,7 +284,11 @@ Reads all raw material (`.amp/stash/*.md` + `.amp/remember/friction/antigen_clus
|
|
|
276
284
|
recurrence; a single occurrence has none to track yet. Friction re-scans every
|
|
277
285
|
session log every run, so a later run matches it back to 2+ sessions and seeds it
|
|
278
286
|
then — this does not change matching against an EXISTING entry, which is recurrence
|
|
279
|
-
regardless of the matching cluster's own session count.
|
|
287
|
+
regardless of the matching cluster's own session count. **A match is not an
|
|
288
|
+
increment.** Whether it counts as a new conversation is decided in 4c by
|
|
289
|
+
`friction.cjs count`, which is a no-op when that session hash is already stored — so
|
|
290
|
+
several matches against one entry routinely produce zero increments, and that is
|
|
291
|
+
correct, not a miscount.
|
|
280
292
|
- For `new:<theme>` groups with no ledger match: distinct conversations = distinct
|
|
281
293
|
cluster indices in the group (within one classify batch, no two cluster indices
|
|
282
294
|
share a session hash). `sessions < 2` → writes nothing. `sessions >= 2` → new entry,
|
|
@@ -346,18 +358,39 @@ Reads all raw material (`.amp/stash/*.md` + `.amp/remember/friction/antigen_clus
|
|
|
346
358
|
inline duplication is needed
|
|
347
359
|
- If `.amp/remember/AGENT_RULES.md` exists (bootstrapped in step 1), compose a second,
|
|
348
360
|
independent section between `<!-- AGENT_RULES:START -->` and `<!-- AGENT_RULES:END -->`
|
|
349
|
-
markers. Unlike MEMORY.md above,
|
|
350
|
-
|
|
351
|
-
|
|
361
|
+
markers. Unlike MEMORY.md above, the file itself is **never `@`-referenced** — an
|
|
362
|
+
`@`-reference hot-loads all ~300 lines into every session, and it is a standards guide
|
|
363
|
+
to consult when designing/building something new, not hot context. The section carries
|
|
364
|
+
a path pointer plus exactly two inline rules: the ones that change what you TYPE, which
|
|
365
|
+
you cannot look up because you do not know you need them. Everything else stays behind
|
|
366
|
+
the pointer. Write the section verbatim, rules first:
|
|
352
367
|
```
|
|
353
368
|
<!-- AGENT_RULES:START -->
|
|
369
|
+
**One writer per piece of state.** One function assigns each field; everything else
|
|
370
|
+
calls it. Grep who writes it before you write it — and if a write can land from a
|
|
371
|
+
callback, thread, or lifecycle, the reader must tell stale from fresh.
|
|
372
|
+
|
|
373
|
+
**Surgical changes only.** Touch what the task requires. Dead code, nits, bugs you
|
|
374
|
+
pass: if it's inside or affects the code you're already changing and the fix changes
|
|
375
|
+
no behavior, fix it and say so — otherwise report it and say what it costs to leave
|
|
376
|
+
it. A problem you don't fix goes in the report, never in a comment.
|
|
377
|
+
|
|
354
378
|
Standards guide (read when designing/building something new, not hot context):
|
|
355
379
|
.amp/remember/AGENT_RULES.md
|
|
356
380
|
<!-- AGENT_RULES:END -->
|
|
357
381
|
```
|
|
358
|
-
- Each marker pair is independent: if AGENT.md
|
|
359
|
-
|
|
360
|
-
containing whichever section(s) apply
|
|
382
|
+
- Each marker pair is independent: if AGENT.md lacks a given pair, append it at the
|
|
383
|
+
end; if a given pair already exists, replace its content in place; if no AGENT.md
|
|
384
|
+
exists, create one containing whichever section(s) apply.
|
|
385
|
+
- **An existing AGENT_RULES pair is left alone — bootstrap once, never overwrite.** The
|
|
386
|
+
block above is what to write when creating it, not a template to re-impose every run.
|
|
387
|
+
Users trim this section deliberately (a pointer-only variant is common), and rewriting
|
|
388
|
+
it silently re-adds text they removed, on every single run, forever. Observed in the
|
|
389
|
+
field: a run restored the inline rules into a AGENT.md whose owner had cut them, and
|
|
390
|
+
the edit had to be reverted by hand. This matches how `AGENT_RULES.md` itself is
|
|
391
|
+
handled — bootstrapped once, never overwritten after.
|
|
392
|
+
- If an existing pair is present but its **path pointer** is missing or wrong, that is
|
|
393
|
+
load-bearing: **report it and stop**, do not silently rewrite the section around it.
|
|
361
394
|
|
|
362
395
|
```markdown
|
|
363
396
|
# Project Memory
|
|
@@ -20,6 +20,22 @@ its exit code**. A check you did not run is a **fail**, never a pass. **N/A
|
|
|
20
20
|
requires a stated reason** ("no build script in `package.json`") — N/A must
|
|
21
21
|
never stand in for "didn't get to it."
|
|
22
22
|
|
|
23
|
+
**Capture the exit code of the command itself, never of a pipeline.** Run the
|
|
24
|
+
bare command, then read `$?` on the next line:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
node "$f" > /tmp/out 2>&1; e=$?
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`$?` after a pipe is the *last element's* status, so the common shape
|
|
31
|
+
`out=$(cmd 2>&1 | tail -1); echo "exit=$?"` reports `tail`'s success — `0` —
|
|
32
|
+
for a suite that exited `2`. Reproduced: a check printing "exit 2:
|
|
33
|
+
prerequisites missing" was recorded as a pass by exactly that loop. Nor does
|
|
34
|
+
`${PIPESTATUS[0]}` rescue it inside a command substitution; it is empty by
|
|
35
|
+
the time you read it. This is the rule the whole gate rests on, and the
|
|
36
|
+
piped form is the natural way to write a multi-suite loop, so it fails
|
|
37
|
+
silently and in the unsafe direction.
|
|
38
|
+
|
|
23
39
|
## Checklist
|
|
24
40
|
- [ ] **Tests pass** — run the project's real test command (`npm test`,
|
|
25
41
|
`pytest`, `go test ./...`, `cargo test`, `make test`).
|
|
@@ -10,6 +10,15 @@ audit** — followed by an adversarial verify pass. It **never edits code**: it
|
|
|
10
10
|
reports findings and hands them back. Fixing is a separate, separately
|
|
11
11
|
authorized action.
|
|
12
12
|
|
|
13
|
+
Only **Critical** and **High** findings block the merge. Everything else is
|
|
14
|
+
appended to the **fix ledger** (`.claude/remember/fix-ledger.md`) — a local,
|
|
15
|
+
cumulative list, living beside `MEMORY.md`, that `/refactor` (no arguments)
|
|
16
|
+
works through between features. Like its neighbours it is a private working
|
|
17
|
+
artifact, usually gitignored; it persists across reviews, it is not a
|
|
18
|
+
deliverable. The report is blockers plus the ledger count, so a review
|
|
19
|
+
converges instead of surfacing fresh nits every run. This command never runs
|
|
20
|
+
`/refactor` itself — it nudges, the way `/stash` nudges `/remember`.
|
|
21
|
+
|
|
13
22
|
Run this **before** `/release`. `/release` will refuse to run without a review
|
|
14
23
|
at the current HEAD SHA.
|
|
15
24
|
|
|
@@ -33,13 +42,29 @@ at the current HEAD SHA.
|
|
|
33
42
|
executed X" from a sub-worker is hearsay, and replacing hearsay with evidence
|
|
34
43
|
is the entire point of this command. A review that delegates its work is a
|
|
35
44
|
review of a report. (Same rule `/security` carries inside stage 2.)
|
|
36
|
-
- **No edits.** You have no authorization to change code,
|
|
37
|
-
you are certain about. Report it.
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
- **No edits — two exceptions.** You have no authorization to change code,
|
|
46
|
+
even for a finding you are certain about. Report it. The only files you may
|
|
47
|
+
write are `.claude/remember/fix-ledger.md` (append bullets; never rewrite or
|
|
48
|
+
delete) and `.claude/remember/last-review.md` (overwrite; the review record
|
|
49
|
+
described at the end of this file).
|
|
50
|
+
- **Prove it with two checks, because neither sees what the other does.**
|
|
51
|
+
`git status --porcelain`, at start and again before you report, proves no
|
|
52
|
+
**tracked** file changed — that is the "never edits code" guarantee, and it
|
|
53
|
+
is the one that matters. It cannot police your own two writes: `.claude/`
|
|
54
|
+
is normally gitignored, so porcelain stays empty whether you wrote the
|
|
55
|
+
allowed files, wrote nothing, or overwrote `MEMORY.md`. `git status
|
|
56
|
+
--ignored` does not close it either — it collapses to `!! .claude/`, the
|
|
57
|
+
directory, not the files. So also take `md5sum .claude/remember/*` before
|
|
58
|
+
you start and again before you report, and show the comparison: only
|
|
59
|
+
`fix-ledger.md` and `last-review.md` may differ.
|
|
40
60
|
|
|
41
61
|
## Target — check the tree first, then interpret `$ARGUMENTS`
|
|
42
62
|
|
|
63
|
+
**The orchestrator runs this check before spawning anyone**, so a dirty tree
|
|
64
|
+
costs no worker; the worker then re-runs it as its own first act, because a
|
|
65
|
+
review that takes the tree's state on trust is the thing this command exists
|
|
66
|
+
not to do. Both, not either.
|
|
67
|
+
|
|
43
68
|
**Before resolving anything, run `git status --porcelain`.** If it prints any
|
|
44
69
|
line — modified, staged, or untracked — **stop and report it**. Say all three
|
|
45
70
|
things, not just the first: (a) the tree is dirty, listing the uncommitted
|
|
@@ -70,6 +95,30 @@ Record the **HEAD SHA** you reviewed, and **report the target you resolved**
|
|
|
70
95
|
(the literal range or path) in your output, so the orchestrator can see what
|
|
71
96
|
was actually read rather than assuming.
|
|
72
97
|
|
|
98
|
+
**Re-review after fixes: read `.claude/remember/last-review.md` first.** Its
|
|
99
|
+
`sha:` line is the previously-reviewed commit and its `blockers:` list is what
|
|
100
|
+
you owe an answer on — take both from the file, never from the orchestrator's
|
|
101
|
+
recollection, for the same reason `/release` does. Then:
|
|
102
|
+
|
|
103
|
+
- **`sha:` ≠ HEAD** → this is a re-review. Target the range
|
|
104
|
+
`<that sha>..HEAD`. Stage 1 reads only the commits since, and stage 3
|
|
105
|
+
re-verifies each recorded blocker as fixed, unfixed, or dismissed with a
|
|
106
|
+
reason. The rest of the branch is **not** re-judged: a full re-read of an
|
|
107
|
+
already-reviewed branch produces fresh findings every run and never
|
|
108
|
+
converges. The range still ends at HEAD, so `/release`'s precondition is
|
|
109
|
+
satisfied and the new record replaces the old one.
|
|
110
|
+
- **`sha:` = HEAD** → nothing has changed since the last review. Say so and
|
|
111
|
+
stop; re-running against an identical tree can only produce noise. If the
|
|
112
|
+
recorded verdict was `blocked`, its blockers are still unfixed by
|
|
113
|
+
definition — repeat them rather than re-deriving them.
|
|
114
|
+
- **No file** → no prior review to build on. Review the whole branch.
|
|
115
|
+
|
|
116
|
+
**On a re-review, sweep the open ledger bullets for liveness first.** Their
|
|
117
|
+
anchors may sit in the part of the branch you are no longer reading, and the
|
|
118
|
+
fix commits you *are* reading can invalidate them. `grep -F` each open
|
|
119
|
+
snippet against its path; report any whose anchor is gone so `/refactor` can
|
|
120
|
+
drop them. Cheap, and it stops dead bullets accumulating unseen.
|
|
121
|
+
|
|
73
122
|
## Effort level
|
|
74
123
|
`low | medium | high | max` — default **medium** if not given. The level
|
|
75
124
|
governs **stage 1 only**:
|
|
@@ -92,7 +141,10 @@ judging.
|
|
|
92
141
|
not a fact to accept. Branches are commonly AI-authored now — including the
|
|
93
142
|
fixes to the fixes — so a review that trusts the message is reviewing prose.
|
|
94
143
|
Run the test suite and the typecheck/build yourself and cite the command and
|
|
95
|
-
its exit code.
|
|
144
|
+
its exit code. Read that code off the bare command (`cmd > /tmp/out 2>&1;
|
|
145
|
+
e=$?`), never off a pipeline — `$?` after a pipe is the last element's
|
|
146
|
+
status, so piping into `tail` reports `0` for a suite that failed. `/ship`
|
|
147
|
+
carries the reproduction.
|
|
96
148
|
|
|
97
149
|
- **Bugs needing a fix.** Logic errors, off-by-one, null/undefined paths,
|
|
98
150
|
races, wrong defaults, broken edge cases.
|
|
@@ -104,6 +156,14 @@ its exit code.
|
|
|
104
156
|
"temporary" names, abandoned feature flags.
|
|
105
157
|
- **Correctness.** Edge cases, error handling, type / contract violations,
|
|
106
158
|
broken invariants.
|
|
159
|
+
- **State ownership.** Two or more functions assigning the same field, flag, or
|
|
160
|
+
view property. A finding on its own — no failing case required. `git grep`
|
|
161
|
+
every assignment to that name repo-wide, not just in the diff; the second
|
|
162
|
+
writer is usually in a file the diff never touched. Name both writers with
|
|
163
|
+
`file:line` — an unnamed second writer is a hunch, not a finding. Count
|
|
164
|
+
ordering, not just writers: a write arriving from a callback, thread, or
|
|
165
|
+
lifecycle event is the dangerous one, and one app writer racing a framework
|
|
166
|
+
one still counts as two.
|
|
107
167
|
- **Performance.** N+1, blocking calls in hot paths, unbounded loops, indexes
|
|
108
168
|
the diff actually touches.
|
|
109
169
|
- **Test quality, not just test presence.** For every test the diff adds or
|
|
@@ -156,21 +216,127 @@ verdict first, then repeat it at the end.
|
|
|
156
216
|
|
|
157
217
|
Then the findings, ordered most severe first.
|
|
158
218
|
|
|
159
|
-
### 🚨 Critical (blocks merge)
|
|
160
|
-
|
|
161
|
-
|
|
219
|
+
### 🚨 Critical / High (blocks merge)
|
|
220
|
+
A **reproduced** failure only: a failing test, a broken build, a security
|
|
221
|
+
exposure, or a bug with a written failure scenario you confirmed in stage 3.
|
|
222
|
+
A finding about **style, wording or structure** is **never** a blocker —
|
|
223
|
+
including in a doc or spec. But prose is not automatically harmless: in a repo
|
|
224
|
+
whose deliverable *is* a specification, a **normative requirement stated two
|
|
225
|
+
incompatible ways** is a reproduced defect, because two conforming
|
|
226
|
+
implementations built from it diverge. Judge by whether a behaviour changes,
|
|
227
|
+
not by whether the file holds code — and judge it **per finding, not per
|
|
228
|
+
repo**, since a diff mixing code and specification is the normal case. A finding already dismissed with evidence in this project's stash
|
|
229
|
+
or memory cannot come back at a higher severity without **new** evidence —
|
|
230
|
+
check before escalating.
|
|
231
|
+
|
|
232
|
+
### Ledger (non-blocking — medium / low)
|
|
233
|
+
Not in the report. **Append** each one as a single bullet to
|
|
234
|
+
`.claude/remember/fix-ledger.md` (create the file with the header below if
|
|
235
|
+
missing):
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
# Fix ledger
|
|
239
|
+
> Non-blocking review findings. One bullet per item. Delete the bullet when
|
|
240
|
+
> fixed, or when its anchor no longer exists. Written by /branch-review;
|
|
241
|
+
> consumed by /refactor (ledger mode).
|
|
242
|
+
>
|
|
243
|
+
> A bullet's path may be a glob when the same finding exists in every kit —
|
|
244
|
+
> `git grep -F "<snippet>" -- <path>` accepts one.
|
|
245
|
+
|
|
246
|
+
- `path/file.js` · "verbatim snippet from the line" · what's wrong · failure
|
|
247
|
+
scenario · YYYY-MM-DD @ <short sha>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**A ledger bullet's failure scenario is subject to stage 3 like any other.**
|
|
251
|
+
Ledger items skip the report, so they are easy to skip verifying too, and an
|
|
252
|
+
unverified consequence written in the bullet's voice reads as established
|
|
253
|
+
fact to whoever fixes it later. Either confirm it, or prefix the scenario
|
|
254
|
+
with `UNVERIFIED:` so `/refactor` retests before acting.
|
|
162
255
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
256
|
+
The **snippet is the anchor**: 20–60 verbatim characters from the line,
|
|
257
|
+
unique enough for `git grep -F` to find it after lines shift. No line
|
|
258
|
+
numbers, no TODO comments in code — the ledger is the single writer. Before
|
|
259
|
+
appending, dedupe with **plain `grep -F "<snippet>" .claude/remember/fix-ledger.md`**;
|
|
260
|
+
if it is already there, skip it. Do not touch existing bullets.
|
|
166
261
|
|
|
167
|
-
|
|
168
|
-
|
|
262
|
+
**A bullet you disprove is deleted, not annotated.** If you establish that an
|
|
263
|
+
existing bullet's finding no longer holds — or never did — remove the line and
|
|
264
|
+
say why in your report. The ledger is a work list, not an archive: an
|
|
265
|
+
annotated bullet still reads as work, and a bullet arguing with itself is
|
|
266
|
+
worse than none. Deleting on disproof is the one case where a reviewer may
|
|
267
|
+
remove a line, and it is the same judgement `/refactor` makes at
|
|
268
|
+
revalidation. Use plain
|
|
269
|
+
`grep`, never `git grep`, on the ledger: the ledger is normally gitignored,
|
|
270
|
+
and `git grep` searches tracked content only, so it reports "not found" for a
|
|
271
|
+
snippet that is sitting right there — the dedupe would pass every time and
|
|
272
|
+
the same finding would be appended on every run.
|
|
273
|
+
|
|
274
|
+
Each blocking finding: **Location** (`file:line`) · **What's wrong** ·
|
|
275
|
+
**Failure scenario** (inputs/state → result) · **Why it matters** ·
|
|
276
|
+
**Suggested fix** (described, not applied) · **Verdict** (confirmed /
|
|
277
|
+
uncertain).
|
|
278
|
+
|
|
279
|
+
Then a coverage line: stage 1 at level `<level>`, stage 2 full, stage 3 —
|
|
280
|
+
each `ran ✓/✗` with its evidence. A stage you did not actually run is a **✗**, never an
|
|
169
281
|
assumed pass.
|
|
170
282
|
|
|
283
|
+
**Write the review record** to `.claude/remember/last-review.md`, overwriting
|
|
284
|
+
it. `/release` reads this file; a SHA that lives only in a chat message is
|
|
285
|
+
gone after a compaction or a handover, and the only remaining source is the
|
|
286
|
+
orchestrator — the one party this command already refuses to take a review's
|
|
287
|
+
word from. **Write it at the end of every run, unconditionally** — not after
|
|
288
|
+
someone decides what to do about it. The information exists now, and the file
|
|
289
|
+
earns its keep only by surviving a compaction, an abandoned session, or a
|
|
290
|
+
handover to someone who never saw the report.
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
sha: <full HEAD sha>
|
|
294
|
+
branch: <branch>
|
|
295
|
+
target: <resolved range or path>
|
|
296
|
+
level: <low | medium | high | max>
|
|
297
|
+
verdict: <ready | blocked>
|
|
298
|
+
date: <YYYY-MM-DD>
|
|
299
|
+
coverage: stage1 <ran|NOT RUN>, stage2 <ran|NOT RUN>, stage3 <ran|NOT RUN>
|
|
300
|
+
blockers:
|
|
301
|
+
- <file:line> · <one-sentence claim, no scenario, no suggested fix>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`blockers: none` when the verdict is ready. One line per blocker and nothing
|
|
305
|
+
more: the reasoning belongs in the report, and the non-blocking findings
|
|
306
|
+
belong in the ledger. This exists so a session that never saw the report can
|
|
307
|
+
learn *what* is blocked, not just *that* something is — otherwise the next
|
|
308
|
+
run rediscovers it by re-reviewing the branch, which is the
|
|
309
|
+
non-convergence this command exists to stop.
|
|
310
|
+
|
|
311
|
+
`coverage` is recorded because a `ready` from a run whose security stage did
|
|
312
|
+
not execute is not the same fact as one where it did, and the reader of this
|
|
313
|
+
file cannot tell them apart otherwise.
|
|
314
|
+
|
|
315
|
+
**There is no override field, and no `verdict: overridden`.** A SHA is
|
|
316
|
+
checkable by anyone; consent is not, so a consent line in a file is forgeable
|
|
317
|
+
by whatever writes the file — and a persisted override is reusable, silently
|
|
318
|
+
covering the next release as well as this one. Releasing over a blocked
|
|
319
|
+
review is a live decision made at `/release`'s hand-back, in conversation.
|
|
320
|
+
|
|
321
|
+
**Nothing clears this file.** It is overwritten whole on the next run, and the
|
|
322
|
+
`sha:` line is what expires it: fix something, commit, and the recorded hash
|
|
323
|
+
no longer matches HEAD, so the gate reports *stale* and asks for a
|
|
324
|
+
re-review rather than *blocked*. A blocked verdict can only persist while HEAD
|
|
325
|
+
does not move — which means nothing was fixed, which is the correct outcome.
|
|
326
|
+
|
|
171
327
|
End with:
|
|
172
328
|
- **Reviewed at HEAD `<sha>` on `<branch>`, target `<resolved range or path>`,
|
|
173
|
-
tree clean at start
|
|
329
|
+
tree clean at start; at exit clean or the two `.claude/remember/` paths
|
|
330
|
+
only.**
|
|
331
|
+
- **Fix ledger: N open, M added this run** (N = bullet count). When N > 0,
|
|
332
|
+
add: "N fixes waiting — run `/refactor` between features." The ledger is a
|
|
333
|
+
local artifact; in the usual case it is gitignored, so writing it moves
|
|
334
|
+
nothing and leaves HEAD untouched.
|
|
174
335
|
- One-line verdict: **Ready to merge? Yes / No / Not until these are fixed.**
|
|
336
|
+
- **A run that produces no record is not a review.** If you die mid-flight —
|
|
337
|
+
a rate limit, a crash, a cancelled turn — there is no report and no
|
|
338
|
+
`last-review.md`, and silence must never be read as a pass. `/release`
|
|
339
|
+
already treats a missing record as no review; state it here too so nobody
|
|
340
|
+
fills the gap from memory of a run that never finished.
|
|
175
341
|
- **Escalate to the orchestrator** with the findings. It decides what gets
|
|
176
342
|
fixed and by whom. Say plainly what you could not verify.
|
|
@@ -103,6 +103,18 @@ function fenceMask(lines) {
|
|
|
103
103
|
// prose), sentences()'s own regex strip, and checkCitations/checkLinks doing none at all — so
|
|
104
104
|
// a page documenting the citation/link syntax INSIDE a fence got its own example flagged as
|
|
105
105
|
// a real violation. One mechanism: mask with fenceMask(), drop the masked lines.
|
|
106
|
+
// `text.split('\n')` returns a trailing EMPTY element for any file ending in a newline —
|
|
107
|
+
// which is nearly every file — so `lines.length` is real_lines + 1. That phantom line reached
|
|
108
|
+
// the index row's "N lines", the last H2's range (one line past EOF) and scan()'s outline.json.
|
|
109
|
+
// Use this wherever lines are COUNTED or a range is BOUNDED. The raw `.split('\n')` is still
|
|
110
|
+
// correct where the array is mapped and re-joined back into file text (stripFences,
|
|
111
|
+
// replaceOutsideFences): dropping the element there would strip the file's final newline.
|
|
112
|
+
function splitLines(text) {
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
if (lines.length && lines[lines.length - 1] === '') lines.pop();
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
function stripFences(text) {
|
|
107
119
|
const lines = text.split('\n');
|
|
108
120
|
const mask = fenceMask(lines);
|
|
@@ -229,7 +241,7 @@ function scan(files) {
|
|
|
229
241
|
if (!files.length) die('usage: docs-builder.cjs scan <file.md...>');
|
|
230
242
|
const records = [];
|
|
231
243
|
for (const f of files) {
|
|
232
|
-
const lines = read(f)
|
|
244
|
+
const lines = splitLines(read(f));
|
|
233
245
|
const mask = fenceMask(lines);
|
|
234
246
|
const { h1, heads } = headings(lines, mask);
|
|
235
247
|
const h2s = heads.filter(h => h.lvl === 2);
|
|
@@ -522,8 +534,13 @@ const MIN_PAGE_LINES = 10;
|
|
|
522
534
|
function pageStatus(file) {
|
|
523
535
|
if (!fs.existsSync(file)) return 'TODO';
|
|
524
536
|
const txt = fs.readFileSync(file, 'utf8');
|
|
525
|
-
const lines = txt
|
|
526
|
-
|
|
537
|
+
const lines = splitLines(txt);
|
|
538
|
+
// splitLines() returns [] for a 0-byte file — 0 lines is the right COUNT, but it means
|
|
539
|
+
// lines[0] can be undefined, where the old raw split('\n') always yielded ['']. An empty
|
|
540
|
+
// page is reachable (a touched placeholder, or page-writing interrupted before it wrote
|
|
541
|
+
// anything) and must read as PARTIAL, not throw and take `plan` down with it.
|
|
542
|
+
const hasFrontmatter = lines.length > 0 && lines[0].trim() === '---'
|
|
543
|
+
&& lines.slice(1).some(l => l.trim() === '---');
|
|
527
544
|
return hasFrontmatter && lines.length >= MIN_PAGE_LINES ? 'done' : 'PARTIAL';
|
|
528
545
|
}
|
|
529
546
|
|
|
@@ -732,7 +749,7 @@ const ARCHIVE_WARN_ROWS = 100; // stated default, not measured — see docs-buil
|
|
|
732
749
|
// section a reader is being routed into, so its row stays H1 + line count + link only.
|
|
733
750
|
function indexRow(rel, dest, includeH2) {
|
|
734
751
|
const text = read(rel);
|
|
735
|
-
const lines = text
|
|
752
|
+
const lines = splitLines(text);
|
|
736
753
|
// Same headings()+fenceMask() path scan() uses -- no second parser -- so an H2 inside a
|
|
737
754
|
// ``` fence is masked out here exactly as it is there.
|
|
738
755
|
const mask = fenceMask(lines);
|
|
@@ -1398,7 +1415,7 @@ function ledger() {
|
|
|
1398
1415
|
const head = git(['rev-parse', 'HEAD'], 'reading HEAD (is this a git repo?)');
|
|
1399
1416
|
const docs = docFiles().map(f => ({
|
|
1400
1417
|
path: f,
|
|
1401
|
-
lines: read(f)
|
|
1418
|
+
lines: splitLines(read(f)).length,
|
|
1402
1419
|
sha256: sha(path.join(REPO, f)).slice(0, 16)
|
|
1403
1420
|
}));
|
|
1404
1421
|
const out = { sha: head, at: new Date().toISOString(),
|
|
@@ -1492,7 +1509,7 @@ function lint(files) {
|
|
|
1492
1509
|
if (!files.length) die('usage: docs-builder.cjs lint <file.md...>');
|
|
1493
1510
|
const sections = [];
|
|
1494
1511
|
for (const f of files) {
|
|
1495
|
-
const lines = read(f)
|
|
1512
|
+
const lines = splitLines(read(f));
|
|
1496
1513
|
const mask = fenceMask(lines);
|
|
1497
1514
|
let cur = null;
|
|
1498
1515
|
const close = i => { if (cur) { cur.e = i; cur.body = lines.slice(cur.s, i).join('\n'); } };
|
|
@@ -1665,7 +1682,7 @@ function isIncludeStub(lines) {
|
|
|
1665
1682
|
// but its size. Oversized is now orthogonal to sorting: a product doc that's too big is
|
|
1666
1683
|
// still a product doc.
|
|
1667
1684
|
function classifyDoc(rel, text) {
|
|
1668
|
-
const lines = text
|
|
1685
|
+
const lines = splitLines(text);
|
|
1669
1686
|
const mask = fenceMask(lines);
|
|
1670
1687
|
const { h1 } = headings(lines, mask);
|
|
1671
1688
|
const snip = snippet(lines, mask, 0, lines.length, 200);
|
|
@@ -2214,7 +2231,7 @@ function cleanup(files) {
|
|
|
2214
2231
|
+ `would overwrite that split's still-in-flight outline.json/labels.json. Finish it `
|
|
2215
2232
|
+ `first: write its remaining pages, then re-run \`cleanup-apply ${inFlight} ...\` until `
|
|
2216
2233
|
+ `it archives — THEN run \`cleanup ${file}\`.`);
|
|
2217
|
-
const lines = read(file)
|
|
2234
|
+
const lines = splitLines(read(file)).length;
|
|
2218
2235
|
const est = writeCostEstimate(1, lines);
|
|
2219
2236
|
console.log(`${file}: ${lines} lines`);
|
|
2220
2237
|
console.log(`est. write cost: $${est.toFixed(2)} (mid tier, floor assuming 1 page — the `
|