liteagents 2.22.1 → 2.24.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +203 -1
  2. package/README.md +7 -5
  3. package/package.json +1 -1
  4. package/packages/ampcode/commands/branch-review.md +180 -14
  5. package/packages/ampcode/commands/docs-builder/docs-builder.cjs +47 -2
  6. package/packages/ampcode/commands/refactor.md +71 -3
  7. package/packages/ampcode/commands/release.md +49 -9
  8. package/packages/ampcode/commands/remember/AGENT_RULES.md +12 -3
  9. package/packages/ampcode/commands/remember/friction.cjs +32 -3
  10. package/packages/ampcode/commands/remember.md +40 -7
  11. package/packages/ampcode/commands/ship.md +16 -0
  12. package/packages/claude/commands/branch-review.md +180 -14
  13. package/packages/claude/commands/docs-builder/docs-builder.cjs +47 -2
  14. package/packages/claude/commands/refactor.md +71 -3
  15. package/packages/claude/commands/release.md +49 -9
  16. package/packages/claude/commands/remember/AGENT_RULES.md +12 -3
  17. package/packages/claude/commands/remember/friction.cjs +32 -3
  18. package/packages/claude/commands/remember.md +40 -7
  19. package/packages/claude/commands/ship.md +16 -0
  20. package/packages/claude/plugins/live-canvas-marketplace/plugins/live-canvas-channel/package-lock.json +19 -18
  21. package/packages/droid/commands/branch-review.md +180 -14
  22. package/packages/droid/commands/docs-builder/docs-builder.cjs +47 -2
  23. package/packages/droid/commands/refactor.md +71 -3
  24. package/packages/droid/commands/release.md +49 -9
  25. package/packages/droid/commands/remember/AGENT_RULES.md +12 -3
  26. package/packages/droid/commands/remember/friction.cjs +32 -3
  27. package/packages/droid/commands/remember.md +40 -7
  28. package/packages/droid/commands/ship.md +16 -0
  29. package/packages/opencode/command/branch-review.md +180 -14
  30. package/packages/opencode/command/docs-builder/docs-builder.cjs +47 -2
  31. package/packages/opencode/command/refactor.md +71 -3
  32. package/packages/opencode/command/release.md +49 -9
  33. package/packages/opencode/command/remember/AGENT_RULES.md +12 -3
  34. package/packages/opencode/command/remember/friction.cjs +32 -3
  35. package/packages/opencode/command/remember.md +40 -7
  36. package/packages/opencode/command/ship.md +16 -0
package/CHANGELOG.md CHANGED
@@ -7,7 +7,209 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
- ## [Unreleased]
10
+ ## [2.24.0] - 2026-09-03
11
+
12
+ ### Added
13
+ - **Friction clusters carry file referents through as a second matching signature.**
14
+ `friction.cjs` already computed a `files` list per candidate (74/101 populated on a
15
+ frozen 34-cluster / 101-candidate corpus) but silently dropped it at clustering, so no
16
+ cluster ever saw it. It is now unioned per cluster and capped at 8 sorted paths. Measured
17
+ before building: `preceding` (tool-name sequence + result) gave 13 distinct signatures
18
+ over 34 clusters at 19.3% collision, `tool_sequence` gave 10 at 25.8% — both close to the
19
+ coin flip the existing quote channel already is. File basenames gave 28 at 3.7% on a
20
+ per-candidate join; the per-cluster union does better still: 30 distinct signatures over
21
+ 34 clusters at 1.8% collision, 29/34 populated, a 10× reduction against `preceding`. A
22
+ prediction from the design notes turned out wrong in the favourable direction: paths are
23
+ real strings, not a distillation, so the channel needed no LLM judgement step to add.
24
+ **This channel is carried on the incoming side only.** No ledger entry stores `files` and
25
+ step 4a is not shown it, so ledger matching still runs on the single quote channel it
26
+ always has — shipping that half is gated on a future exact-label-agreement measurement,
27
+ documented in `docs/product/antigen-gate-prd.md` §13 and
28
+ `docs/product/remember-README.md` §6.
29
+
30
+ ### Fixed
31
+ - **Friction's `preceding.result` read the wrong signal — text-matched `'Exit code 0'`
32
+ instead of the `is_error` boolean.** The text match hit 1 of 2623 sampled result blocks;
33
+ `is_error` is present on 2065 of 2624. Before the fix, `preceding.result` was `unknown`
34
+ on 31 of 34 clusters; after, 18 claimed-success, 4 error, 12 unknown. Cluster hashes are
35
+ unchanged — this only corrects a field that was already there.
36
+ - **`docs-builder`'s cleanup output named counts ambiguously.** The advisory now says "N
37
+ file(s) with link rewrites" and the restore step says "restored N inbound reference(s)" —
38
+ the advisory counts files touched, the restore counts the references inside them, and the
39
+ two numbers are not the same unit.
40
+ - **`docs-builder` inbound references follow the core page back out of the archive.**
41
+ `cleanup-apply` archives the source, then relocates the core page back to its original
42
+ path. The archive step had already rewritten every inbound reference to point at
43
+ `docs/archive/` — correct at that moment, since the archive was briefly the only copy —
44
+ but nothing walked them back once the core page reoccupied the original path, leaving the
45
+ corpus telling readers the doc lived in the archive while the live page sat unreferenced.
46
+ A third restore step now walks them back. What must *not* be restored is the point of the
47
+ design rather than an edge case: the split's own pages cite the original by line number,
48
+ so their `sources:` and citations stay pinned to the frozen archive copy. Field-reported
49
+ from a real PRD split (33 references across 15 files), reproduced failing-first.
50
+
51
+ ### Security
52
+ - **`fast-uri` bumped 3.1.5 → 3.1.7 and `qs` bumped 6.15.2 → 6.16.0** in the
53
+ live-canvas-channel plugin lockfile (Dependabot #43/#44, folded into one commit since
54
+ both edit the same lockfile). `fast-uri` 3.1.7 closes five high-severity advisories:
55
+ authority injection via an unvalidated port in `serialize()` (GHSA-qw65-cvwx-89v3), host
56
+ confusion via unbalanced IP-literal brackets (GHSA-58mr-gqgx-xq4g) and skipped IDN
57
+ canonicalization (GHSA-5jgf-p345-68v8), and SSRF via repeated hostname percent-decoding
58
+ (GHSA-fph4-wmhf-6fwf) and malformed IPv6 normalization (GHSA-f65p-4m7j-42xc). Both
59
+ packages are transitive dependencies of `@modelcontextprotocol/sdk`.
60
+
61
+ ## [2.23.0] - 2026-09-02
62
+
63
+ ### Added
64
+ - **`/refactor` gains a no-argument ledger mode.** Bare `/refactor` works through
65
+ `.claude/remember/fix-ledger.md` instead of taking a target: it requires a clean tree and
66
+ a non-`main` branch, revalidates every bullet before fixing anything, drops the ones
67
+ whose anchor no longer resolves, fixes the survivors one at a time under the existing
68
+ no-behaviour-change constraints, and deletes each bullet as its fix lands — so the fix
69
+ commit is the done record and there is no second place to keep it in sync.
70
+ - **`/branch-review` gains State ownership as a stage-1 finding category.** Two or more
71
+ functions assigning the same field, flag or view property is a finding on its own, with
72
+ no failing case required. Both writers must be named with `file:line`, since an unnamed
73
+ second writer is a hunch. Ordering counts as well as writers: a write arriving from a
74
+ callback, thread or lifecycle event is the dangerous one, and one app writer racing a
75
+ framework writer still counts as two.
76
+ - **`AGENT_RULES.md` — four Build Rules, each naming something observable.** One writer per
77
+ piece of state; split the decision from the machinery, extracting a branch into a pure
78
+ function to pin it with a test rather than to raise coverage; claims in comments must be
79
+ checkable, because a name search proves an edge exists and never that one does not; and
80
+ every line earns its place, meaning if you cannot say what breaks when it is deleted,
81
+ delete it. "Surgical changes only" was rewritten to say what to do with a problem you
82
+ pass on the way: fix it if it is in the code you are already changing and the fix changes
83
+ no behaviour, otherwise report it with what it costs to leave. A problem you do not fix
84
+ goes in the report, never in a comment.
85
+ - **`/remember` writes two hot rules inline into the `AGENT_RULES.md` section.** The file
86
+ stays a plain pointer and is never `@`-referenced, since that hot-loads roughly 300 lines
87
+ of standards guide into every session. The section now carries the path plus exactly the
88
+ two rules that change what you type — the ones you cannot look up because you do not know
89
+ you need them.
90
+ - **`docs/product/branch-review-README.md`** — reference for the pre-merge gate: the three
91
+ stages, what blocks a merge, the ledger's anchor design, and the review → ledger →
92
+ `/refactor` loop. Follows the existing `remember` and `docs-builder` product-doc pattern.
93
+ - **`/refactor` gains a `## Guardrails` block, ported from `/branch-review`.** Spawn a
94
+ worker at your tool's mid tier, stated explicitly on the spawn; escalate anything you
95
+ cannot decide rather than assuming; the worker never sub-delegates; edit only what a
96
+ surviving ledger bullet names, one change per bullet. The three parts that do not
97
+ transfer verbatim were rewritten: "no edits" inverts into a scope rule since `/refactor`
98
+ edits by design, and **the three HITL gates belong to the orchestrator, not the
99
+ worker** — a subagent cannot hold a conversation, so it stops and hands back the options
100
+ with no choice made rather than picking revert/patch/update-test on the user's behalf.
101
+ The blast-radius proof is now two checks: `git status --porcelain` at exit must list
102
+ only bullet-named files, and `md5sum .claude/remember/*` must show only `fix-ledger.md`
103
+ differing — `last-review.md` is off-limits to the fixer, since writing it would forge
104
+ the gate that judges its own work.
105
+ - **`docs/product/remember-README.md`** gains a `## 6. Known limitations` section.
106
+ Matching semantics and evidence are the same channel — an entry's `class_hints` are
107
+ fragments of the quotes that proved it, so its identity and its proof of recurrence are
108
+ the same strings — with two consequences pulling in opposite directions: tautological
109
+ matching (bounded, not removed, by session-hash dedup) and over-matching on thin
110
+ ledgers (`Open item 2`). Separately: a run cannot tell that its own work invalidated a
111
+ standing fact, since detecting that would mean re-checking every fact against the
112
+ working tree on every run, which trades a stale fact for a confidently wrong one.
113
+
114
+ ### Changed
115
+ - **`/branch-review` writes a durable review record; `/release` reads it.** The reviewed
116
+ SHA previously existed only as prose in a chat message, so `/release`'s Phase 0.5
117
+ precondition resolved to the orchestrator's word — the one party the same paragraph
118
+ declares inadmissible — and vanished on a compaction or a handover. The review now
119
+ overwrites `.claude/remember/last-review.md` with sha/branch/target/verdict/date, and
120
+ Phase 0.5 matches against its `sha:` line. No record, or no `sha:` line, is no review.
121
+
122
+ **Migration:** a branch reviewed before this change has no record file, so the new
123
+ Phase 0.5 will correctly refuse it. That is migration, not a bug. Re-run
124
+ `/branch-review`; do **not** hand-write the record, which would turn the durable
125
+ artifact back into the unverified claim it exists to replace.
126
+
127
+ - **Only reproduced Critical/High failures block a merge.** Everything else is appended
128
+ to a local fix ledger at `.claude/remember/fix-ledger.md`, consumed by bare `/refactor`.
129
+ Re-review after fixes reads `<previously-reviewed-sha>..HEAD` rather than re-judging the
130
+ whole branch, which is what makes a review converge instead of surfacing a fresh nit
131
+ list every run. Style, wording and structure never block; a normative requirement stated
132
+ two incompatible ways still does, since conforming implementations built from it diverge.
133
+
134
+ - **`/release` Phase 0.5 is the SHA comparison alone.** A ledger-only exception was removed
135
+ rather than kept and narrowed: the ledger is gitignored, so it never reaches a commit
136
+ diff, and scoping a rule to a condition that cannot occur is how dead branches survive
137
+ review. A repo that does track `.claude/` will see a ledger commit make the review stale,
138
+ which is the gate working — re-review, or leave the ledger uncommitted until the release
139
+ is cut.
140
+
141
+ ### Fixed
142
+ - **`/branch-review` — a disproved ledger bullet is deleted, and a dead run is not a
143
+ pass.** The append-only rule left nowhere to record that a bullet's stated consequence
144
+ was wrong: editing it broke the rule, and a second bullet read as a second finding. A
145
+ field session hit this and invented an indented sub-bullet. Disproof now deletes the
146
+ line, with the reason going in the report — the ledger is a work list, not an archive.
147
+ Separately, a review that dies mid-flight writes no record, and nothing said whether that
148
+ silence counted as a pass; it does not.
149
+ - **`/branch-review` — the review record carries blockers, level and coverage.** Five lines
150
+ proved that a review ran and what it concluded, but not *what* was blocked, so a session
151
+ inheriting a `blocked` verdict had to re-review the branch to rediscover why — the
152
+ non-convergence this command exists to stop, displaced one level up. The record now lists
153
+ one line per blocker (claim only; scenarios stay in the report, non-blocking findings stay
154
+ in the ledger), the effort level, and per-stage coverage. `/release` stops on any stage
155
+ marked `NOT RUN`, since a `ready` from a run that skipped the security stage is a
156
+ different fact. Deliberately absent: any override field — a hash is checkable by anyone
157
+ and consent is not, so a consent line would be forgeable by whatever writes the file, and
158
+ a persisted override would silently cover the next release too.
159
+ - **`/release` Phase 0.5 wrote a `verdict:` line nobody read.** The record carried the
160
+ review's conclusion, but the gate compared only the `sha:` line, so a record saying
161
+ `verdict: blocked` passed the mechanical check whenever the hash still matched — leaving
162
+ the conclusion to the orchestrator's recollection, which is the unverified claim the
163
+ record was created to replace. Both lines are now read mechanically; only `ready` plus a
164
+ matching hash is a pass.
165
+ - **`/release` — the handoff sequence went from `gh pr create` straight to `gh pr merge`,
166
+ with no wait for CI.** Every gate in the chain runs on one machine: `/branch-review`
167
+ reviews locally, `/ship` runs the suite locally, and `/release` never pushes. CI is the
168
+ only differently-configured instrument, and under this flow it sees the branch for the
169
+ first time *after* both gates have passed. Found in the field: a release merged and
170
+ tagged on green local gates, then failed CI on a test asserting against a path that
171
+ exists only on the author's machine, leaving a tag cut but never published — the exact
172
+ "local ahead of published" state Phase 0 warns about. The sequence now has a
173
+ `gh pr checks --watch` step between create and merge, and merges only on green.
174
+ - **`/release` — the exit-code rule applies to the orchestrator's own shell too.** `/ship`
175
+ carried it for the worker, but the handoff steps are typed by hand and were not covered.
176
+ In the same field run, `gh run watch --exit-status | tail -2; echo $?` printed `0` for a
177
+ failed run, turning a red CI into a green reading.
178
+ - **`/release` — the docs sweep may fix a line a ledger bullet names, but must not delete
179
+ the bullet.** Calling the sweep the place to "close" a doc-only item made `/release` a
180
+ second deleter of state with exactly one owner, contradicting both the ledger's
181
+ one-append-one-delete split and the one-writer-per-state build rule this release adds.
182
+ The sweep still fixes the line; `/refactor`'s next revalidation drops the bullet.
183
+ - **`/branch-review` — the exit-cleanliness check could not see its own target.** The
184
+ guardrail said porcelain must be empty or list only the two allowed paths, but `.claude/`
185
+ is gitignored, so porcelain is empty whether the reviewer wrote those files, wrote
186
+ nothing, or overwrote `MEMORY.md`. `git status --ignored` does not close it either — it
187
+ collapses to the directory, not the files. Porcelain keeps its real job (no tracked file
188
+ changed); an `md5sum` comparison over `.claude/remember/` now covers the two writes.
189
+ - **`/ship` and `/branch-review` — exit codes must be read off the bare command, not a
190
+ pipeline.** `$?` after a pipe is the last element's status, so the natural multi-suite
191
+ shape `out=$(cmd 2>&1 | tail -1); echo "exit=$?"` records `tail`'s success for a suite
192
+ that exited non-zero. Found in the field: a check printing "exit 2: prerequisites
193
+ missing" entered the gate as a pass. `${PIPESTATUS[0]}` does not rescue it inside a
194
+ command substitution either.
195
+ - **`/branch-review` — ledger dedupe uses plain `grep`, not `git grep`.** The ledger is
196
+ deliberately gitignored and `git grep` searches tracked content only, so the dedupe
197
+ check reported "not found" for snippets sitting in the file and would have re-appended
198
+ every finding on every run.
199
+ - **`/branch-review` — ledger bullets are subject to stage 3.** A field run produced a true
200
+ finding whose stated consequence was false. Bullets must now be verified or carry an
201
+ `UNVERIFIED:` prefix so `/refactor` retests before acting.
202
+ - **`/remember` — a marker pair already present in `CLAUDE.md` had no rule.** The clause
203
+ covered a missing pair (append it) and the AGENT_RULES exception (leave it alone) but
204
+ said nothing for an already-present MEMORY pair, which is the common case on every run
205
+ after the first. Now: replace its content in place; the AGENT_RULES bootstrap-once
206
+ exception directly below still overrides.
207
+ - **`docs/product/branch-review-README.md`** — dropped a stale "five-line record" count.
208
+ The record stopped being five lines once level, coverage and blockers were added.
209
+ - **`/refactor` — ledger mode's step 1 caught a dirty tree only after a worker already
210
+ existed.** Step 1 now documents the split: the orchestrator runs the tree check before
211
+ spawning, and the worker re-runs it as its own first act, matching `/branch-review`'s
212
+ Target section.
11
213
 
12
214
  ## [2.22.1] - 2026-09-01
13
215
 
package/README.md CHANGED
@@ -147,11 +147,11 @@ Results land in `.claude/remember/friction/antigen_review.md` with projects, err
147
147
  - **skill-creator** - Guide for creating new skills
148
148
  - **debug-method** - Four-phase debugging framework
149
149
  - **optimize** - Performance analysis
150
- - **refactor** - Safe refactoring with behavior preservation
151
- - **branch-review** - Pre-merge gate: general review at a chosen effort level plus a full security audit, adversarial verify pass; reports findings, never fixes
150
+ - **refactor** - Safe refactoring with behavior preservation; with no arguments, works through the fix ledger `/branch-review` accumulated
151
+ - **branch-review** - Pre-merge gate that reports and never fixes: general review at a chosen effort level, a full security audit that runs at full depth regardless of level, then an adversarial verify pass. Needs a clean tree. Only reproduced Critical/High failures block; everything else lands in the fix ledger for `/refactor`
152
152
  - **security** - Standalone vulnerability scan; also runs as stage 2 of `/branch-review`
153
- - **ship** - Pre-deployment checklist
154
- - **release** - Prepare a release on the current branch: verify → docs sweep → version bump → local commit, then hand back the merge/tag/publish sequence (never pushes)
153
+ - **ship** - Mechanical pre-deploy gate: every item is answerable by running a command and reading its exit code, recorded as pass/fail/N/A. A check not run is a fail, and N/A needs a stated reason
154
+ - **release** - Prepare a release on the current branch: verify → docs sweep → version bump → local commit, then hand back the merge/tag/publish sequence (never pushes). Refuses without a review recorded at the current HEAD SHA
155
155
  - **test-generate** - Generate test suites
156
156
 
157
157
  > **Claude-only plugin:** `live-canvas-channel` is a bundled Claude Code MCP channel plugin that ships under `~/.claude/plugins/live-canvas-marketplace/`. One-time `/plugin install` + a session started with `--dangerously-load-development-channels` unlocks live mode. Skill probes for the channel on each invocation and handholds setup when missing. See [`packages/claude/skills/live-canvas/README.md`](packages/claude/skills/live-canvas/README.md) for the full walkthrough.
@@ -166,6 +166,7 @@ Results land in `.claude/remember/friction/antigen_review.md` with projects, err
166
166
  | **[subagentic-manual.md](packages/subagentic-manual.md)** | Detailed agent/command reference |
167
167
  | **[remember-README.md](docs/product/remember-README.md)** | How hot memory works — the `/stash` → `/remember` pipeline, the friction sensor, and the antigen ledger |
168
168
  | **[docs-builder-README.md](docs/product/docs-builder-README.md)** | How `/docs-builder` works — the reorg/cleanup modes, what it measurably costs, and the ledger that tracks doc drift |
169
+ | **[branch-review-README.md](docs/product/branch-review-README.md)** | How the pre-merge gate works — the three stages, what blocks a merge, and the `/branch-review` → fix ledger → `/refactor` loop |
169
170
 
170
171
  ---
171
172
 
@@ -184,7 +185,8 @@ Results land in `.claude/remember/friction/antigen_review.md` with projects, err
184
185
  **Code Quality:**
185
186
  ```
186
187
  @quality-assurance Review this PR before merge
187
- /branch-review main medium # review branch vs main + security audit; reports, never fixes
188
+ /branch-review main medium # review + security audit; blockers reported, the rest to the fix ledger
189
+ /refactor # no args: work through the fix ledger branch-review accumulated, between features
188
190
  /debug-method Investigate this race condition
189
191
  ```
190
192
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liteagents",
3
- "version": "2.22.1",
3
+ "version": "2.24.0",
4
4
  "description": "AI development toolkit with 11 specialized agents and 18 commands including live-canvas UI design with click-to-annotate feedback. Simple one-question installer for Claude, Opencode, Ampcode, and Droid.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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, even for a finding
37
- you are certain about. Report it. Re-run `git status --porcelain` before you
38
- report and confirm it is still empty — if it is not, say what changed. That
39
- turns "it never edits" from a claim into a checked fact.
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
- ### ⚠️ Warnings (should fix)
161
- ### 💡 Suggestions (nice to have)
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
- Each finding: **Location** (`file:line`) · **What's wrong** · **Failure
164
- scenario** (inputs/state result) · **Why it matters** · **Suggested fix**
165
- (described, not applied) · **Verdict** (confirmed / uncertain).
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
- Then a coverage line: stage 1 at level `<level>`, stage 2 full — each `ran ✓/✗`
168
- with its evidence. A stage you did not actually run is a **✗**, never an
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 and at exit.**
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.
@@ -1085,10 +1085,17 @@ const LINK_SKIP = /(^|\/)(CHANGELOG\.md|log\.md)$/;
1085
1085
  // moves is already covered by the resident check the instant its own git mv lands, before
1086
1086
  // rewriteLinks ever runs for it.
1087
1087
  let plannedArchiveSrc = new Set();
1088
+ // Set only for the duration of cleanup-apply's restore pass (see cleanupApply below), which
1089
+ // walks inbound references back from the archived copy to the relocated core page. That pass
1090
+ // must skip the split's own pages: they cite the original BY LINE NUMBER, so their `sources:`
1091
+ // and citations belong on the frozen archive copy, not on the shortened core page. A
1092
+ // predicate rather than a Set because the exemption is a directory prefix plus one file.
1093
+ let linkRestoreExempt = null;
1088
1094
  // One predicate, called from the one place rewriteLinks() loops over candidate files, so the
1089
1095
  // exemption can never desync across callers the way moveDoc's follow-ups almost did.
1090
1096
  function isRewriteExempt(f) {
1091
- return LINK_SKIP.test(f) || f.startsWith(REORG_DEST.archive + '/') || plannedArchiveSrc.has(f);
1097
+ return LINK_SKIP.test(f) || f.startsWith(REORG_DEST.archive + '/') || plannedArchiveSrc.has(f)
1098
+ || (linkRestoreExempt !== null && linkRestoreExempt(f));
1092
1099
  }
1093
1100
 
1094
1101
  // A real corpus (astral-sh/uv) cross-links its docs with RELATIVE paths — `../concepts/x.md`,
@@ -1301,8 +1308,12 @@ function flushCommitAdvisory() {
1301
1308
  const outsideDocs = Array.from(new Set(outsideFiles.map(f => f.split('/')[0]))).sort();
1302
1309
  console.log(`\n${moveSet.length} rename(s) this run (git mv stages these automatically; `
1303
1310
  + 'the copy+unlink fallback used outside a git repo does not)');
1311
+ // Unit, stated: linkSet holds FILE paths, so this is a file count. cleanup-apply's
1312
+ // restore pass just below reports OCCURRENCES, and a real run printed "33" here next to
1313
+ // "restored 38" — same-sounding nouns, two different units, and the reader has no way to
1314
+ // tell they are not a discrepancy. Name the unit in both places.
1304
1315
  console.log(linkSet.length
1305
- ? `${linkSet.length} link rewrite(s) UNSTAGED` + (outsideFiles.length
1316
+ ? `${linkSet.length} file(s) with link rewrites UNSTAGED` + (outsideFiles.length
1306
1317
  ? `, ${outsideFiles.length} outside docs/: ${outsideDocs.join(', ')}`
1307
1318
  : ' (all inside docs/)')
1308
1319
  : 'no inbound-link rewrites this run.');
@@ -2352,6 +2363,40 @@ function cleanupApply(file, outlineF, labelsF) {
2352
2363
  for (const m of r.artifactNotes) console.log(` ${m}`);
2353
2364
  if (r.failures.length) console.error(` WARN core page relocated, but ${r.failures.join('; ')}`);
2354
2365
  noteMoved(r.rel); noteLinks(r.linkFiles.map(x => x.file));
2366
+
2367
+ // FIELD BUG (real, reproduced): archiveOrThrow above rewrote EVERY inbound reference
2368
+ // to point at docs/archive/, because at that moment the archive genuinely was the
2369
+ // only copy. The relocation on the line above then put a live page back at the
2370
+ // original path — so the corpus now tells readers that "the PRD" lives in the
2371
+ // archive while the live page sits unreferenced. On a real split that was 33
2372
+ // references across 15 files outside docs/, and it is precisely the backwards
2373
+ // outcome this command exists to prevent. Walk them back.
2374
+ //
2375
+ // The ordering itself cannot be swapped: the original must vacate the path before
2376
+ // the core page can occupy it, and rewriting links to a path nothing occupies yet
2377
+ // would be worse. So the restore is a third step, not a reordering.
2378
+ //
2379
+ // EXEMPT, and this is the point of the pass rather than an edge case: the split's
2380
+ // own pages cite the original BY LINE NUMBER, so their `sources:` and citations
2381
+ // must stay on the frozen archive copy — as must the relocated core page's own.
2382
+ // Rewriting those back would be a new bug wearing the old one's clothes.
2383
+ const archivedRel = path.posix.join(REORG_DEST.archive, path.basename(file));
2384
+ const pagesPrefix = pages.replace(/\/*$/, '') + '/';
2385
+ linkRestoreExempt = f => f === r.rel || f.startsWith(pagesPrefix);
2386
+ let restored;
2387
+ try { restored = rewriteLinks(archivedRel, r.rel); }
2388
+ finally { linkRestoreExempt = null; }
2389
+ if (restored.skipped) {
2390
+ console.log(` ${restored.skipped}`);
2391
+ } else if (restored.total) {
2392
+ console.log(` restored ${restored.total} inbound reference(s) from ${archivedRel} `
2393
+ + `back to ${r.rel} (the archive step had aimed them at the frozen copy; `
2394
+ + `${pagesPrefix} pages keep theirs, their citations are line-numbered)`);
2395
+ for (const { file: lf, n } of restored.files) console.log(` ${lf}: ${n}`);
2396
+ noteLinks(restored.files.map(x => x.file));
2397
+ } else {
2398
+ console.log(` no inbound references to ${archivedRel} needed restoring.`);
2399
+ }
2355
2400
  } catch (e) {
2356
2401
  console.error(` WARN could not relocate the core page from ${from} to ${to}: `
2357
2402
  + `${e.message} — it remains at ${from}.`);