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
@@ -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}.`);
@@ -1,12 +1,80 @@
1
1
  ---
2
2
  name: refactor
3
3
  description: Refactor [code]
4
- usage: /refactor <code-section>
5
- argument-hint: [file-or-function]
6
- allowed-tools: Read, Edit, Grep, Glob, Bash(npm test:*), Bash(npx jest:*), Bash(npx vitest:*), Bash(pnpm test:*), Bash(yarn test:*), Bash(pytest:*), Bash(python:*), Bash(go test:*), Bash(cargo test:*), Bash(make test:*), Bash(git diff:*)
4
+ usage: /refactor <code-section> | /refactor (no args = fix-ledger mode)
5
+ argument-hint: [file-or-function, or empty for the fix ledger]
6
+ allowed-tools: Read, Edit, Grep, Glob, Bash(npm test:*), Bash(npx jest:*), Bash(npx vitest:*), Bash(pnpm test:*), Bash(yarn test:*), Bash(pytest:*), Bash(python:*), Bash(go test:*), Bash(cargo test:*), Bash(make test:*), Bash(git diff:*), Bash(git grep:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git switch:*)
7
7
  ---
8
8
  Refactor $ARGUMENTS.
9
9
 
10
+ ## Guardrails
11
+ - **Spawn a worker and explicitly select your tool's mid tier.** State the
12
+ tier on the spawn — do not omit it and rely on a default. An omitted tier
13
+ inherits the *parent's* tier, which is not the same thing as the balanced
14
+ one. Pick the judgment-capable tier that is cheaper and faster than your top
15
+ reasoning tier. **Not the cheapest/fastest tier**: on judgment work it
16
+ measurably degrades (misclassification rates several times higher). Choose by
17
+ tier, not by a vendor model name copied from this file — names drift, and
18
+ this command ships to several tools. Fall back to running inline if your tool
19
+ has no subagent mechanism.
20
+ - **Escalate, never assume.** Anything you cannot decide, cannot verify, or
21
+ that this spec does not cover → **stop and report it to the orchestrator**
22
+ (the main session). Never improvise, never widen scope, never fix a side
23
+ issue you noticed along the way.
24
+ - **The worker does the work itself — no delegation.** The fixer must **not**
25
+ spawn subagents of its own. Every edit it reports, and every test run it
26
+ cites, has to be one it made or ran with its own tool calls: a relayed "I
27
+ fixed it and the suite is green" from a sub-worker is hearsay, and this
28
+ command's whole output is the claim that a change landed and the tests still
29
+ pass. A fix that delegates its work is a report about a report.
30
+ - **The HITL gates below belong to the orchestrator, not the worker.** A
31
+ subagent cannot hold a conversation with the user, so it cannot run a gate
32
+ that ends in *stop and ask*. When one trips — a failing test, a crossed
33
+ public API boundary, a change bigger than the bullet asked for — the worker
34
+ **stops there and hands the situation back**, with the options and its
35
+ reasoning but no choice made. The orchestrator asks. A worker that picks
36
+ revert / patch / update-test on the user's behalf has answered a question it
37
+ was never allowed to ask.
38
+ - **Edit only what a surviving bullet names.** Ledger mode's scope is the
39
+ bullets that survive revalidation, one change per bullet — not the
40
+ neighbouring code, not the formatting, not a second finding noticed on the
41
+ way past. Anything else goes back to the orchestrator to become a new
42
+ bullet.
43
+ - **Prove the blast radius with two checks, because neither sees what the
44
+ other does.** `git status --porcelain` at exit must list only files a
45
+ surviving bullet named — that is this command's scope guarantee, and unlike
46
+ `/branch-review` it is not expected to be empty. It cannot police the
47
+ memory directory: `.claude/` is normally gitignored, so porcelain stays
48
+ empty whether you deleted a fixed bullet, wrote nothing, or overwrote
49
+ `MEMORY.md`. So also take `md5sum .claude/remember/*` before you start and
50
+ again before you report, and show the comparison: only `fix-ledger.md` may
51
+ differ. `last-review.md` in particular is `/branch-review`'s to write —
52
+ a fixer that touches it forges the gate that judges its own work.
53
+
54
+ ## Ledger mode — `$ARGUMENTS` empty
55
+ Work through `.claude/remember/fix-ledger.md`, the non-blocking findings
56
+ `/branch-review` has accumulated. Everything below (goals, constraints,
57
+ verification, HITL gates) still applies; this section only says what to
58
+ refactor and how to close each item.
59
+
60
+ 1. **Tree must be clean and not on `main`.** The orchestrator runs this check
61
+ before spawning the worker, so a dirty tree costs no worker; the worker
62
+ then re-runs it as its own first act. `git status --porcelain` non-empty
63
+ → stop, say what is uncommitted. On `main` → `git switch -c chore/fix-ledger`.
64
+ 2. **Ledger missing or has zero bullets** → say so and stop. Nothing to do.
65
+ 3. **Revalidate every bullet first, fix nothing yet.** For each: `git grep -F
66
+ "<snippet>" -- <path>`. **No hit → delete the bullet** and list it as
67
+ "cleaned by other work". Hit → re-read the surrounding code; if the finding
68
+ no longer holds, delete the bullet with a one-line reason. What survives is
69
+ the work list.
70
+ 4. **Fix the survivors, one bullet per change**, under the constraints below.
71
+ Delete each bullet as its fix lands. A fix that turns out to need a
72
+ behaviour change is not a refactor — leave the bullet, note it in the report.
73
+ 5. Run the tests as described below. Then report: **fixed / dropped / left**
74
+ with the reason per left item, and the remaining bullet count.
75
+ 6. Say plainly: **commit, then run `/branch-review`** on this branch — ledger
76
+ mode is a fixer, not a review, and its diff gets the ordinary gate.
77
+
10
78
  ## Goals
11
79
  - Reduce complexity
12
80
  - Improve readability
@@ -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 SHA the review recorded — `/branch-review`
60
- ends with `Reviewed at HEAD <sha>`. Asking the orchestrator "did a review run?"
61
- puts the question to the one party with an incentive to say yes, so its word is
62
- not evidence: obtain the review's own recorded SHA and match the two strings.
63
- **No recorded SHA to compare = no review**, never a pass.
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 merge --admin --squash --delete-branch` (main is PR-protected;
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
- > 4. `git tag vX.Y.Z` on `main` and push the tag
149
- > 5. Publish **if this project has a publish path** (e.g.
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
- > 6. Verify it is actually live (`npm view <pkg> version`, and the published
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 must have a purpose.** No speculative code, no "might need this later", no abstractions for one use case
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 pre-existing dead code alone unless asked. Every changed line should trace directly to the request
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 CLAUDE.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 must have a purpose no speculative code, no premature abstractions.
299
+ **Open-source only.** No vendor lock-in. Every line of code earns its placeif 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
 
@@ -1765,8 +1765,26 @@ function extractToolsFromTurn(event) {
1765
1765
  const toolName = block.name || 'unknown';
1766
1766
  tools.push({ tool: toolName, action: 'call' });
1767
1767
  } else if (block.type === 'tool_result') {
1768
- const result = String(block.content || '');
1769
- if (result.includes('Exit code 0')) {
1768
+ // The transcript marks a failed tool call with `is_error`; a success
1769
+ // carries no sentinel in the result text. Reading the text for
1770
+ // 'Exit code 0' matched 1 block in 2623 sampled from the real
1771
+ // corpus, so `result` was 'unknown' on 31 of 34 clusters — and the
1772
+ // case this field exists for is a CLAIMED success the user is
1773
+ // contradicting, which is the one that never fired. Content is also
1774
+ // sometimes an array of blocks (403/2623), which String() turned
1775
+ // into "[object Object]" so no pattern could match it.
1776
+ //
1777
+ // A block with no `is_error` stays unknown on purpose: those are
1778
+ // harness meta-results (skill/agent launches, question answers —
1779
+ // 560/2624 sampled) and calling them success would inflate the very
1780
+ // signal this field is here to detect.
1781
+ const raw = block.content;
1782
+ const result = Array.isArray(raw)
1783
+ ? raw.map(b => (b && typeof b === 'object' ? (b.text || '') : String(b || ''))).join('\n')
1784
+ : String(raw || '');
1785
+ if (block.is_error === true) {
1786
+ tools.push({ tool: 'result', action: 'error' });
1787
+ } else if (typeof block.is_error === 'boolean') {
1770
1788
  tools.push({ tool: 'result', action: 'success' });
1771
1789
  } else if (/Exit code [1-9]|Traceback|Error/.test(result)) {
1772
1790
  tools.push({ tool: 'result', action: 'error' });
@@ -2142,6 +2160,7 @@ function clusterCandidates(allCandidates, canonicalGroups) {
2142
2160
  errors: b.errors,
2143
2161
  peak: b.peak,
2144
2162
  texts: b.texts,
2163
+ files: [...b.files],
2145
2164
  preceding: b.preceding,
2146
2165
  anySelf: b.selfVotes > 0, // at least one self-correction → warn, LLM confirms target
2147
2166
  };
@@ -2166,7 +2185,7 @@ function clusterCandidates(allCandidates, canonicalGroups) {
2166
2185
  if (best) {
2167
2186
  cl = best;
2168
2187
  } else {
2169
- cl = { sig: new Set(), seedSig: new Set(ss.sig), shCount: new Map(), sessions: {}, signals: {}, contexts: [], errors: [], peaks: [], anySelf: false, preceding: null };
2188
+ cl = { sig: new Set(), seedSig: new Set(ss.sig), shCount: new Map(), sessions: {}, signals: {}, contexts: [], errors: [], peaks: [], anySelf: false, preceding: null, files: new Set() };
2170
2189
  clusters.push(cl);
2171
2190
  }
2172
2191
  for (const s of ss.sig) { cl.sig.add(s); cl.shCount.set(s, (cl.shCount.get(s) || 0) + 1); }
@@ -2178,6 +2197,15 @@ function clusterCandidates(allCandidates, canonicalGroups) {
2178
2197
  cl.peaks.push(ss.peak);
2179
2198
  if (ss.anySelf) cl.anySelf = true;
2180
2199
  if (ss.preceding && (!cl.preceding || (ss.preceding.action !== 'none' || ss.preceding.error))) cl.preceding = ss.preceding;
2200
+ // The file referents a cluster's sessions touched. Measured on a 34-cluster real
2201
+ // corpus as the one channel that actually separates clusters: file basenames gave 28
2202
+ // distinct signatures at a 3.7% collision rate, against 13 / 19.3% for the preceding
2203
+ // action+result pair and 10 / 25.8% for tool_sequence. Matching an incoming cluster
2204
+ // to a ledger entry otherwise runs on class_hints alone, and those ARE fragments of
2205
+ // the entry's own evidence quotes — one channel, where identity and proof are the
2206
+ // same strings. These paths are mechanical, so carrying them adds a second channel
2207
+ // without reintroducing an LLM distillation step.
2208
+ for (const f of ss.files) cl.files.add(f);
2181
2209
  }
2182
2210
 
2183
2211
  const out = clusters.map(cl => {
@@ -2251,6 +2279,7 @@ function clusterCandidates(allCandidates, canonicalGroups) {
2251
2279
  max_peak: peaks[peaks.length - 1],
2252
2280
  contexts: cl.contexts,
2253
2281
  errors: cl.errors,
2282
+ files: [...cl.files].sort().slice(0, 8), // referents: the discriminative match channel (see merge above)
2254
2283
  preceding: cl.preceding, // #4: agent action + result just before the reaction
2255
2284
  self_suspect: allSelf || cl.anySelf, // #3: a self-correction is present — LLM confirms target (advisory)
2256
2285
  top_keywords: topSh.slice(0, 10),
@@ -133,6 +133,14 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
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 `.claude/remember/MEMORY.md` in the format under step 5.
138
146
 
@@ -276,7 +284,11 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
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 (`.claude/stash/*.md` + `.claude/remember/friction/antige
346
358
  inline duplication is needed
347
359
  - If `.claude/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, this is a **plain path pointer, never `@`-referenced**
350
- — an `@`-reference hot-loads the whole file into every session, and this is a standards
351
- guide to consult when designing/building something new, not hot context:
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
  .claude/remember/AGENT_RULES.md
356
380
  <!-- AGENT_RULES:END -->
357
381
  ```
358
- - Each marker pair is independent: if CLAUDE.md already has a given pair, replace the
359
- section between them; if not, append it at the end; if no CLAUDE.md exists, create one
360
- containing whichever section(s) apply
382
+ - Each marker pair is independent: if CLAUDE.md lacks a given pair, append it at the
383
+ end; if a given pair already exists, replace its content in place; if no CLAUDE.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 CLAUDE.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`).
@@ -333,9 +333,9 @@
333
333
  }
334
334
  },
335
335
  "node_modules/es-object-atoms": {
336
- "version": "1.1.1",
337
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
338
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
336
+ "version": "1.1.2",
337
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
338
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
339
339
  "license": "MIT",
340
340
  "dependencies": {
341
341
  "es-errors": "^1.3.0"
@@ -448,9 +448,9 @@
448
448
  "license": "MIT"
449
449
  },
450
450
  "node_modules/fast-uri": {
451
- "version": "3.1.5",
452
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
453
- "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
451
+ "version": "3.1.7",
452
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
453
+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
454
454
  "funding": [
455
455
  {
456
456
  "type": "github",
@@ -573,9 +573,9 @@
573
573
  }
574
574
  },
575
575
  "node_modules/hasown": {
576
- "version": "2.0.3",
577
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
578
- "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
576
+ "version": "2.0.4",
577
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
578
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
579
579
  "license": "MIT",
580
580
  "dependencies": {
581
581
  "function-bind": "^1.1.2"
@@ -849,12 +849,13 @@
849
849
  }
850
850
  },
851
851
  "node_modules/qs": {
852
- "version": "6.15.2",
853
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
854
- "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
852
+ "version": "6.16.0",
853
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
854
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
855
855
  "license": "BSD-3-Clause",
856
856
  "dependencies": {
857
- "side-channel": "^1.1.0"
857
+ "es-define-property": "^1.0.1",
858
+ "side-channel": "^1.1.1"
858
859
  },
859
860
  "engines": {
860
861
  "node": ">=0.6"
@@ -991,14 +992,14 @@
991
992
  }
992
993
  },
993
994
  "node_modules/side-channel": {
994
- "version": "1.1.0",
995
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
996
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
995
+ "version": "1.1.1",
996
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
997
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
997
998
  "license": "MIT",
998
999
  "dependencies": {
999
1000
  "es-errors": "^1.3.0",
1000
- "object-inspect": "^1.13.3",
1001
- "side-channel-list": "^1.0.0",
1001
+ "object-inspect": "^1.13.4",
1002
+ "side-channel-list": "^1.0.1",
1002
1003
  "side-channel-map": "^1.0.1",
1003
1004
  "side-channel-weakmap": "^1.0.2"
1004
1005
  },