liteagents 2.17.1 → 2.19.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.
@@ -9,7 +9,7 @@ Run friction analysis, then consolidate session stashes + friction antigens into
9
9
  **Guardrails**
10
10
  - Favor straightforward, minimal implementations first and add complexity only when requested or clearly required.
11
11
  - Keep changes tightly scoped to the requested outcome.
12
- - **Precision over recall for hot memory.** A false antigen loaded into `@MEMORY.md` steers every future session. When unsure, record as a low-confidence episode do not promote.
12
+ - **Precision over recall for hot memory.** A false antigen loaded into `@MEMORY.md` steers every future session. When unsure, do not promote — leave it to recurrence (a ledger `observing` entry at 2 sessions, nothing at 1).
13
13
  - **Mid-tier model, not hardcoded.** Steps 2/3/4a delegate to a mid-tier model — capable of
14
14
  semantic judgment, cheaper/faster than your top reasoning tier (e.g. Claude's Sonnet vs
15
15
  Opus). Use whatever your tool designates as that balanced default; never hardcode a
@@ -39,7 +39,9 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
39
39
  (the same directory as `remember.md`, whether installed or run from the package). If it
40
40
  exists nowhere, skip to step 1 (stash-only) and tell the user friction.cjs is missing.
41
41
  - **Resolve the global sessions root** — probe this list top-to-bottom, use the first that
42
- exists and contains `.jsonl` files (recursively). **Never prompt the user.**
42
+ exists and contains `.jsonl` files directly, or one level down in per-project
43
+ subdirectories (friction.cjs scans exactly those two levels, not a deep recursive walk).
44
+ **Never prompt the user.**
43
45
  ```
44
46
  # ── Add your own global sessions root at the TOP so it is checked first ──
45
47
  ~/.claude/projects/ # Claude Code
@@ -76,15 +78,17 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
76
78
  already exists, leave it untouched — never overwrite, even if the bundled template
77
79
  changes in a later version; it becomes user-owned the moment it lands in the project.
78
80
  - Read all `.claude/stash/*.md` files in the current project
79
- - Read friction output written in step 0: `.claude/remember/friction/antigen_clusters.json` (preferred) or `.claude/remember/friction/antigen_review.md` (fallback)
81
+ - Read friction output written in step 0: `.claude/remember/friction/antigen_clusters.json` (preferred) or `.claude/remember/friction/antigen_review.md` (fallback). On the fallback path, step 4c does NO counting — merge quotes into
82
+ matching entries only; never change `sessions`, `last_seen`, or `recurred_while_hot` (the
83
+ fallback carries no `session_ids`, so identity matching cannot run on it).
80
84
  - Read existing `.claude/remember/MEMORY.md` if it exists — create dir if missing
81
85
  - Read processed manifest at `.claude/remember/.processed` — skip already-processed stashes
82
- - If no unprocessed stashes AND friction produced no new antigens, run the step-8 mechanical
83
- length check (the same awk: over 180 chars with no >100-char backtick literal) against the
84
- existing `.claude/remember/MEMORY.md`. If it returns 0 lines, report "nothing to
85
- consolidate" and stop. If it returns any lines, "no new input" is not a reason to leave
86
- gate debt in place do NOT stop: proceed to step 3 and run the Facts rewrite + pre-write
87
- gate on the existing content with no new input, then continue through step 8 as normal.
86
+ - **No unprocessed stashes skip steps 2-3 (extraction and the Facts rewrite)
87
+ entirely facts are never rewritten with zero new input**, not even to clear existing
88
+ length-gate debt on `.claude/remember/MEMORY.md`. Steps 4-5 (friction ledger count
89
+ Antigens render) are stash-independent and still run whenever friction produced output
90
+ (see step 4's own guard). If there is also no friction
91
+ output, report "nothing to consolidate" and stop after step 1.
88
92
 
89
93
  2. **Extract from unprocessed stashes** (up to 5 stashes per agent, as few agents as possible — see Guardrails)
90
94
  - Each agent reads its batch of stashes together and calls the mid-tier model (see Guardrails) to extract:
@@ -123,10 +127,14 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
123
127
  history — never exempted. Only a draft that passes the gate (or has its overruns
124
128
  exempted under the 100-char backtick rule) is written to `.claude/remember/MEMORY.md`.
125
129
  - **Episodes section**: append new episode entries, keep only the **10 most recent**.
126
- Every older episode is **folded, then deleted**: its lesson becomes a fact (handed to
127
- the rewrite above); the narrative is removed. No archivegit has the history.
130
+ **Dedup before appending**: if a new episode covers the same work as one already in the
131
+ section (same goal or same session's work under different wording judge by content, not
132
+ title), merge the new detail into the existing entry instead of appending a second copy.
133
+ Re-processing a stash whose episode is already filed must not create a near-duplicate
134
+ pair. Every older episode is **folded, then deleted**: its lesson becomes a fact (handed
135
+ to the rewrite above); the narrative is removed. No archive — git has the history.
128
136
  - **Antigens section**: only update from friction output (step 4)
129
- - Write merged result to `.claude/remember/MEMORY.md` in the format under step 6.
137
+ - Write merged result to `.claude/remember/MEMORY.md` in the format under step 5.
130
138
 
131
139
  4. **Distill friction into antigens** (only if friction output exists)
132
140
 
@@ -141,48 +149,72 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
141
149
  > logs.** Friction's lexical grouping and flags are hints, not the verdict.
142
150
 
143
151
  - Read `.claude/remember/friction/antigen_clusters.json`.
144
- - **4a. Classify target, then semantically consolidate** (the parts lexical matching can't do).
145
- Call the mid-tier model with the cluster quotes + their `preceding`/`projects`/`sessions`/`self_suspect`
146
- (NOT the logs), and have it:
147
- 1. **Decide the target of each reaction — agent or self.** Drop *self/context*
148
- corrections where the user redirected themselves ("wrong project", "wrong window",
149
- "nevermind") the agent did nothing wrong. `self_suspect` and an empty `preceding`
150
- (no agent action) are strong cues. Keep only **agent-directed** reactions.
151
- 2. **Merge same-complaint paraphrases** that friction left split because they share no
152
- words (e.g. "nothing landed, fuck you" + "it says pushed but none got it" → one antigen).
153
- Output one object per surviving antigen:
154
- ```json
155
- {
156
- "rule": "Verify the artifact is actually live after publish; a clean exit code ≠ done",
157
- "target": "agent",
158
- "evidence": ["both say pushed... none got it", "notng landed in live-claude, fuck you"],
159
- "errors": ["Exit code 0 (claimed success)"],
160
- "sessions": 2,
161
- "confidence": "medium"
162
- }
163
- ```
164
- - **4b. Route + tier by recurrence.** For each cluster and each LLM-merged group:
165
- - `suggested_artifact: antigen` (recurring + severe) or an LLM-merged group an
152
+ - **4a. Classify** (the LLM classifies only no merging, no arithmetic; counting
153
+ and rendering are mechanical, see 4c and step 5). Call the mid-tier model once per
154
+ cluster batch with each cluster's `contexts`, `preceding`, `errors`,
155
+ `self_suspect`, `projects`, `sessions`, `top_keywords`, and the ledger's existing
156
+ entries (`id`, `class_hints`, `rule`, `evidence.quotes`). For EACH cluster, output
157
+ exactly one label nothing else:
158
+ - `drop` — self-directed correction, agent's own prose captured as context, or a
159
+ real reaction too short/ambiguous to name a specific mistake (`self_suspect` and
160
+ an empty `preceding` no agent action are strong self-directed cues). Don't
161
+ force a match on one overlapping word.
162
+ - an existing ledger id (`ag-NNN`) — only if the cluster is narrowly the SAME
163
+ mistake class as that entry's `class_hints`+`rule`+`evidence.quotes`, not just
164
+ similar sentiment. State the entry's specific claim precisely in the prompt (a
165
+ generic one-liner rule is not enough to bound the match — see 4c Open item 2) and
166
+ give the classifier a negative example, not just the positive claim, e.g. for
167
+ ag-001 (validate, don't assert): "did you test it?" matches; "we're burning money,
168
+ why is it failing?" does NOT — cost/outcome complaints are not validation claims.
169
+ - `new:<theme>` — a real, agent-directed mistake matching no existing entry.
170
+ `<theme>` is NOT freeform LLM prose: derive it mechanically from the cluster's
171
+ own `top_keywords[0]` and `top_keywords[1]` (lowercase, hyphen-joined). This
172
+ alone raised measured 5-run exact-label agreement from 0.884 to ~0.97-0.99 by
173
+ removing wording variance as a source of disagreement the remaining variance
174
+ is genuine classification disagreement (drop vs. new:, or which existing id),
175
+ not paraphrase noise. Also output a `rule`: one line stating the behavioral
176
+ rule this cluster's evidence supports, same do/don't imperative style as an
177
+ existing ledger entry's `rule` (e.g. "Never say work is validated... without an
178
+ actual run behind it"). This is the only LLM-authored field here — `<theme>`
179
+ naming stays mechanical. `friction.cjs count` requires it whenever the cluster's
180
+ own `sessions >= 2` (it will create a ledger entry); below that it's unused.
181
+
182
+ Output is `{cluster_index: label}` for `drop`/`ag-NNN`; for `new:<theme>`, output
183
+ `{cluster_index: {label: "new:<theme>", rule: "<one-line rule>"}}`.
184
+ - **4b. Route + tier by recurrence.** For each cluster and each same-label group
185
+ (the clusters 4a gave the same label) — its tier comes from the distinct-
186
+ conversation count `friction.cjs count` (4c) computes for it, the union of the
187
+ group's hashes deduped against the ledger:
188
+ - `suggested_artifact: antigen` (recurring + severe) or a same-label group → an
166
189
  **antigen** (a "do/don't" behavioral rule), with its verbatim evidence quotes.
167
190
  - `suggested_artifact: fact` (recurring + mild) → a **Fact**.
168
- - `suggested_artifact: episode` that did **not** merge into a recurring group → an
169
- **Episode** (one-off; recorded, not a rule).
191
+ - `suggested_artifact: episode` that did **not** land in a recurring group → **not
192
+ an Episode, and at 1 session not written anywhere.** The Episodes section is stash-fed
193
+ and capped at 10; friction's one-offs are cross-project and arrive by the dozen, so
194
+ filing them there would flush the stash episodes. Nothing is lost: friction re-scans
195
+ every session log on every run, so the cluster re-surfaces until it recurs — and at 2
196
+ sessions it gets its home, a ledger `observing` entry (4c), which step 5 renders under
197
+ Low Confidence. `suggested_artifact` is friction's structural proposal, not a filing
198
+ decision; the filing rule is this list.
170
199
  - Confidence by distinct-session recurrence:
171
200
  - **High** (5+ sessions) → loaded hot via `@MEMORY.md`
172
201
  - **Medium** (3-4 sessions) → recorded under Antigens, *not* loaded hot
173
- - **Low** (<3 sessions) → keep as Episode only
174
- - **Recurrence tiers bind everything, including LLM-merged groups:** merging consolidates
175
- evidence, it never elevates it — a merged group's tier comes from its combined
176
- distinct-session count (e.g. a 2-session merged group is still Low → Episode + ledger
177
- `observing`, not an antigen entry in MEMORY.md).
202
+ - **Low** (<3 sessions) → ledger `observing` only at 2 sessions; nothing at 1
203
+ - **Recurrence tiers bind everything, including same-label groups:** grouping consolidates
204
+ evidence, it never elevates it — a same-label group's tier comes from its combined
205
+ distinct-session count (e.g. a 2-session same-label group is still Low → ledger
206
+ `observing` only, not an antigen entry in MEMORY.md).
178
207
  - **Never auto-promote.** Only High-confidence (5+ sessions) antigens load hot. A
179
- single dramatic correction is an Episode, not an antigen.
180
- - Update the Antigens section in MEMORY.md (promote/demote based on new recurrence).
181
- - **4c. Update the antigen ledger** (`.claude/remember/ledger.json`) — the evidence trail
182
- linking each rule to the mistake it targets and whether it is working. Create it as
183
- `{"version": 1, "entries": []}` if missing. It is JSON for exact matchingbookkeeping
184
- only, never injected into context as guidance (MEMORY.md is what gets read; the ledger
185
- is what gets checked).
208
+ single dramatic correction is recorded nowhere yet, not an antigen.
209
+ - **4c. Count** (`.claude/remember/ledger.json`, the evidence trail linking each rule
210
+ to the mistake it targets and whether it is working replaces the old evidence-merge
211
+ arithmetic; a script now does every count, not the LLM). **This is a literal command
212
+ you run, not a description you reason from.** The LLM's job ended at 4a do not
213
+ hand-compute session counts, do not decide by inspection which entries changed, even
214
+ if you are confident you can do it correctly. Create `ledger.json` as `{"version": 1,
215
+ "entries": []}` if missing. It is JSON for exact matching — bookkeeping only, never
216
+ injected into context as guidance (MEMORY.md is what gets read; the ledger is what
217
+ gets checked).
186
218
 
187
219
  Entry shape:
188
220
  ```json
@@ -196,96 +228,90 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
196
228
  "history": [{ "date": "YYYY-MM-DD", "event": "<transition>" }] }
197
229
  ```
198
230
 
199
- **Evidence merges by session identity, not by re-counting a re-scan.** Friction re-scans
200
- the entire corpus every run, so the same old session matches its cluster again on every
201
- run; without an identity check, that re-detection would masquerade as new recurrence and
202
- could promote a one-off to hot — the exact false-preference failure the observed-signal
203
- redesign exists to prevent. Friction's clusters already carry `session_ids`. Identity is
204
- the **trailing 8-char hash** the part after the last `-` in the id — because project
205
- labels can be renamed while the hash, derived from the session filename, is stable. Two
206
- ids with the same hash are the same session, full stop.
207
-
208
- Two ids with DIFFERENT hashes can also be one session. A fork or resume writes the same
209
- conversation to a second session file with its own filename, so the hash alone would
210
- count one reaction twice. `friction.cjs` collapses these before it emits clusters —
211
- sessions sharing at least one message `uuid` are one conversation, and the group is
212
- reported under a single canonical id (the lexicographically smallest). So the ids
213
- reaching this step are already canonical; do not attempt to re-derive fork identity
214
- here. Measured on a real 3,158-session corpus: 6 such groups exist, and of ~5M possible
215
- session pairs only 9 share any uuid at all — every one a genuine duplicate.
216
-
217
- **Migration (one-time, grandfathered): SEED, DO NOT COUNT.** Existing entries predate
218
- `session_ids` and carry only a bare `sessions` count with an empty `session_ids` set. This
219
- is mechanicaldo not resolve it by judgment. On the run that first populates such an
220
- entry's `session_ids` (i.e. `session_ids` is empty going in), do exactly these two things
221
- and nothing else:
222
- 1. Write `session_ids` to this run's matched ids (NOT an empty set — the empty set is the
223
- pre-migration state you are migrating FROM, not what you write).
224
- 2. Append the history line "identity migration legacy count grandfathered, growth
225
- requires new hashes".
226
-
227
- Change NOTHING else on this run not `sessions`, not `last_seen`, not
228
- `recurred_while_hot`, not `status`. In particular, do NOT apply the "no hashes present →
229
- new conversation" rule here: `session_ids` was empty, so every hash looks absent, and
230
- counting would treat the entry's own already-counted history as fresh recurrence on a
231
- `hot` entry that also fires `recurred_while_hot`, which at 2 marks the phrasing failed and
232
- rewrites a rule that never actually failed. Counting resumes on the NEXT run, once
233
- `session_ids` is non-empty and an absent hash set is genuinely new evidence.
234
-
235
- Observed for real: bareloop's first migration run carried two `hot` entries, each already
236
- at `recurred_while_hot: 1`. Counting on migration would have taken both to 2 and force-
237
- rephrased two working rules from re-detected pre-existing sessions. Two separate runs
238
- avoided it only because whoever ran them noticed and overrode the text — which is the
239
- definition of a rule that needs to be mechanical rather than prose.
240
-
241
- For each surviving antigen from 4a/4b, match against existing entries by `class_hints`
242
- (the mistake class, not the rule wording — rules change, the class doesn't). A matched
243
- cluster represents exactly ONE conversation, no matter how many hashes its `session_ids`
244
- holds a fork/resume group deliberately carries every member file's hash so the cluster
245
- can be matched under any of the conversation's filenames.
246
-
247
- **`sessions` is the authoritative conversation count. `session_ids` is evidence detail —
248
- a list of the FILES one conversation was written to. NEVER derive a count from
249
- `len(session_ids)`; a fork or resume makes that number larger than the conversation
250
- count.** This is mechanical do not resolve it by judgment. Compare the cluster's hashes
251
- against the entry's stored hash set as a set, not one at a time, and count per
252
- conversation, never per hash:
253
- - **None of the cluster's hashes present** a genuinely new conversation: add ALL of the
254
- cluster's hashes to `session_ids`, increment `sessions` by exactly 1 (never by the
255
- number of hashes in the cluster), refresh `last_seen` to today's run date (session ids
256
- carry no year), and count it once toward the 4b promotion threshold.
257
- - **Any of the cluster's hashes already present** this conversation is already counted:
258
- add whichever of its hashes are still missing from `session_ids` (they are aliases of
259
- the same conversation, and storing them keeps future matching robust under any of its
260
- filenames), but change NOTHING else not `sessions`, not `last_seen`, not history, not
261
- `recurred_while_hot`. It is a re-scan re-detecting a conversation already counted, not
262
- new evidence.
263
- - **No match, cluster `sessions` >= 2** → new entry, `status: "observing"`, attempt 1,
264
- history "candidate (N sessions)".
265
- - **No match, cluster `sessions` == 1** do NOT create a ledger entry. The ledger tracks
266
- recurrence, and a single occurrence has no recurrence to track yet — seeding singletons
267
- grows the ledger by dozens of never-recurring entries per run. Friction re-scans every
268
- session log on every run, so if this mistake recurs, a later run will match it back to
269
- 2+ sessions and seed it then. This does not change the Match bullets below — a
270
- 1-session cluster can still merge into an EXISTING entry; that is recurrence.
271
- - **Match, `observing`** apply the new-conversation / already-counted rule above
272
- (sessions, session_ids, quotes, projects, last_seen). Crosses the 4b hot threshold on a
273
- genuinely new conversation `status: "hot"`, history "promoted to hot (N sessions)".
274
- - **Match, `hot`** the mistake happened *while its rule was loaded*, and only when the
275
- match is a genuinely new conversation (per the rule above, not a re-scan of an
276
- already-present hash): `recurred_while_hot += 1` once per conversation, merge evidence,
277
- history "recurred while hot (count)".
278
- - At `recurred_while_hot >= 2`: the phrasing failed. Mark the current attempt
279
- `outcome: "failed"`, draft attempt n+1 it must differ from **every** prior
280
- attempt's text in this entry (failed attempts are the rejected-edit buffer: never
281
- re-propose one verbatim) replace `rule`, update MEMORY.md's Antigens section,
282
- reset `recurred_while_hot` to 0.
283
- - If 2 attempts have already failed and the antigen persists → `status: "escalated"`:
284
- remove the rule from MEMORY.md's hot section, record a Fact instead ("persistent
285
- failure mode: <class> — no phrasing reduces it"), and flag it in the step-7 report.
286
- **Flag, don't act** — the user decides: enforcement (a hook, where the tool has
287
- them) or accepted limit.
288
- - **Match, `escalated`/`rejected`** (rejected = user veto) → merge evidence only; never re-propose.
231
+ Immediately after 4a produces `labels.json`, run these exact commands as real shell
232
+ invocations, in order. First:
233
+ ```bash
234
+ node <path-to-friction.cjs> migrate-attempts .claude/remember/ledger.json .claude/remember/ledger.json
235
+ ```
236
+ This records any hand-drifted `rule` text as a new attempt so I7 (`rule` == the last
237
+ attempt's `rule`) holds before counting runs; it is a no-op on an already-consistent
238
+ ledger, so always run it regardless of whether drift is suspected. Then:
239
+ ```bash
240
+ node <path-to-friction.cjs> count <labels.json> <ledger.json> <clusters.json> <today's-date> <ledger.json>.new .claude/remember/friction/count_report.json
241
+ ```
242
+ Then **overwrite `ledger.json` with `<ledger.json>.new`'s contents** (e.g. `mv
243
+ ledger.json.new ledger.json`). Do not stop after `labels.json` producing labels is
244
+ 4a, not the deliverable of this step. `friction.cjs count <labels.json> <ledger.json>
245
+ [clusters.json] [runDate] [outLedgerPath] [reportPath]` is deterministic, no LLM
246
+ involved, so it can't drift between repos or runs; the count report is also written to
247
+ `.claude/remember/friction/count_report.json` for step 8 to read back. It implements,
248
+ mechanically, everything the old
249
+ prose reasoning here used to require by hand:
250
+
251
+ - **Session identity** the trailing 8-char hash of a session id (stable across
252
+ project-label renames) with the same fork/resume canonicalization friction.cjs
253
+ already applies before clusters are emitted (sessions sharing >=1 message `uuid`
254
+ collapse to one canonical id; do not re-derive fork identity yourself).
255
+ - **Migration (one-time, grandfathered): SEED, DO NOT COUNT** — an entry whose
256
+ `session_ids` is empty going in AND carries no prior "identity migration" history
257
+ line has that line's first match seed `session_ids` from this run's matches WITHOUT
258
+ incrementing `sessions`/`last_seen`/`recurred_while_hot`. The **migration-fill
259
+ sub-case** `session_ids` still empty but an "identity migration" line already
260
+ exists fills `session_ids` on the first post-migration match, still without
261
+ incrementing; counting resumes only once `session_ids` is non-empty.
262
+ - **Counting is per CLUSTER INDEX, never per hash and never once per label-group.**
263
+ For each cluster index in a matched group: if none of that cluster's own hashes are
264
+ already in the entry's stored set, it is one genuinely new conversation `sessions`
265
+ += 1 (never by hash count), `last_seen` refreshed, and if the entry is `hot` and
266
+ the session's own date is on/after the current attempt's `adopted` date (the
267
+ **adopted-date gate**: a mistake that predates the rule's current phrasing isn't a
268
+ phrasing failure of it) `recurred_while_hot` += 1. A gated-out (predates-adopted)
269
+ new conversation still counts as evidence (sessions, hash) but not toward
270
+ `recurred_while_hot`. A hash already present is a re-scan of an already-counted
271
+ conversation: only missing alias hashes are added, nothing else changes.
272
+ - Promotes `observing`→`hot` at `sessions >= 5` (a fresh ledger on a project with
273
+ mature global evidence can hit the hot case on its very first run: new entry born
274
+ `hot` directly if `sessions >= 5` on arrival).
275
+ - **No match, cluster `sessions` == 1** writes nothing. The ledger tracks
276
+ recurrence; a single occurrence has none to track yet. Friction re-scans every
277
+ session log every run, so a later run matches it back to 2+ sessions and seeds it
278
+ then — this does not change matching against an EXISTING entry, which is recurrence
279
+ regardless of the matching cluster's own session count.
280
+ - For `new:<theme>` groups with no ledger match: distinct conversations = distinct
281
+ cluster indices in the group (within one classify batch, no two cluster indices
282
+ share a session hash). `sessions < 2` writes nothing. `sessions >= 2` → new entry,
283
+ `status` follows the same >=5-hot / else-observing rule.
284
+
285
+ **Open item 1 `new:` label collisions: resolved (Guard B).** `new:` clusters never
286
+ merge in-batch, regardless of whether two cluster indices share the same `new:` string
287
+ each `new:` cluster with `sessions >= 2` creates its own ledger entry, and a genuine
288
+ recurrence of the same new mistake is matched on a later run by `class_hints`, like any
289
+ other entry. Measured against the alternative (merge same-labeled `new:` clusters when
290
+ their `top_keywords` overlap by >=1): a synthetic new entry was correctly re-matched by
291
+ a fresh classifier on 5/5 runs under Guard B, while the keyword-overlap guard wrongly
292
+ merged two real, distinct mistakes on real data (clusters 21/23 unrelated mistakes
293
+ sharing the generic keyword "fucking validate"). `friction.cjs count` implements Guard
294
+ B: every `new:`-labeled cluster stands alone.
295
+
296
+ **Open item 2 — a generic one-line `rule` under-specifies the class for matching:**
297
+ the ledger's `class_hints`+`rule` alone can be too broad for the LLM classifier (4a)
298
+ to reliably tell two different mistakes apart, as the "fucking validate" false-merge
299
+ case above shows. Consider requiring a short negative example ("NOT X, even though it
300
+ sounds similar") on ledger entries whose `class_hints` are single generic words/phrases.
301
+
302
+ **After `friction.cjs count` returns**, resume the parts it does not do:
303
+ - **Escalation.** For any `hot` entry the count run left with `recurred_while_hot >=
304
+ 2`: the phrasing failed. Mark the current attempt `outcome: "failed"`, draft
305
+ attempt n+1 it must differ from **every** prior attempt's text in this entry
306
+ (failed attempts are the rejected-edit buffer: never re-propose one verbatim)
307
+ replace `rule`, update MEMORY.md's Antigens section (step 5), reset
308
+ `recurred_while_hot` to 0. If 2 attempts have already failed and the antigen
309
+ persists → `status: "escalated"`: remove the rule from MEMORY.md's hot section,
310
+ record a Fact instead ("persistent failure mode: <class> no phrasing reduces
311
+ it"), and flag it in the step-8 report. **Flag, don't act** — the user decides:
312
+ enforcement (a hook, where the tool has them) or accepted limit.
313
+ - **`escalated`/`rejected` entries** (rejected = user veto) never get a new attempt
314
+ proposed, count run or not.
289
315
 
290
316
  **Decay (observing only).** Antigens are the fastest-decaying artifact and, until now,
291
317
  had no exit. This is meaningful *because* of the identity fix above — without it,
@@ -320,12 +346,13 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
320
346
  inline duplication is needed
321
347
  - If `.claude/remember/AGENT_RULES.md` exists (bootstrapped in step 1), compose a second,
322
348
  independent section between `<!-- AGENT_RULES:START -->` and `<!-- AGENT_RULES:END -->`
323
- markers:
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:
324
352
  ```
325
353
  <!-- AGENT_RULES:START -->
326
- Consult when building something new or adding a feature — a standards guide, not hot
327
- context like MEMORY.md above:
328
- @.claude/remember/AGENT_RULES.md
354
+ Standards guide (read when designing/building something new, not hot context):
355
+ .claude/remember/AGENT_RULES.md
329
356
  <!-- AGENT_RULES:END -->
330
357
  ```
331
358
  - Each marker pair is independent: if CLAUDE.md already has a given pair, replace the
@@ -343,31 +370,59 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
343
370
  ### YYYY-MM-DD - [title]
344
371
  - [bullet narrative]
345
372
 
373
+ ## Antigens
374
+ [rendered — see below, do not hand-write]
375
+ ```
376
+ **The `## Antigens` section is rendered, not hand-written — run this too, as a literal
377
+ command, do not hand-write it even to match the format shown above.** After 4c has
378
+ overwritten `ledger.json`, run:
379
+ ```bash
380
+ node <path-to-friction.cjs> render <ledger.json>
381
+ ```
382
+ Take that command's stdout **verbatim** and replace MEMORY.md's entire `## Antigens`
383
+ section with it (from the `## Antigens` line up to, but not including, the next `## `
384
+ heading, or to end of file if Antigens is the last section — it usually is). This step's
385
+ output IS the correctness check for itself: after replacing, `node
386
+ <path-to-friction.cjs> check <ledger.json> <MEMORY.md>` must report `I6-new: EQUAL` — if
387
+ it doesn't, something was edited by hand instead of pasted from the script's stdout; redo
388
+ it from the script's stdout exactly, never patch MEMORY.md manually to make it match.
389
+
390
+ `friction.cjs render` prints the section byte-for-byte, no LLM paraphrase, no manual
391
+ template filling:
392
+ ```
346
393
  ## Antigens
347
394
  ### High Confidence (loaded — applies every session)
348
- - [behavioral rule] (evidence: [N] sessions — "[verbatim quote]")
395
+ - [behavioral rule] (evidence: [N] sessions, [P] projects — "[quote1]", "[quote2]") — ag-NNN
349
396
 
350
397
  ### Medium Confidence (observing — not loaded)
351
- - [behavioral rule] (evidence: [N] sessions)
398
+ - [behavioral rule] (evidence: [N] sessions) — ag-NNN
352
399
 
353
400
  ### Low Confidence (needs more data)
354
- - [pattern] (evidence: [N] sessions)
401
+ - [pattern] (evidence: [N] sessions) — ag-NNN
355
402
  ```
356
- The Medium/Low lists render only entries whose ledger `status` is `observing` (or `hot`
357
- for High) an entry marked `expired` by the decay rule (step 4c) is skipped here even
358
- though it stays in `ledger.json`.
403
+ Tiers: High = `status == "hot" && sessions >= 5` (the only tier that prints quotes — the
404
+ first 2 of `evidence.quotes`, verbatim, capped at 2 and `evidence.projects.length`).
405
+ Medium = `status == "observing" && 3 <= sessions <= 4`. Low = `status == "observing" &&
406
+ sessions == 2`. `expired`/`escalated`/`rejected`/`sessions < 2` never render, same as
407
+ before — an entry marked `expired` by the decay rule (4c) is skipped here even though it
408
+ stays in `ledger.json`. A legacy 1-session `observing` entry (see 4b) stays in the ledger
409
+ and grows or expires like any other, but is not rendered. An empty tier prints `- (none —
410
+ <why>)`, one line, never an empty section.
359
411
 
360
412
  6. **Update processed manifest**
361
413
  - Append paths of newly processed stashes to `.claude/remember/.processed`
362
414
 
363
- 7. **Docs reconcile check DETECT ONLY** (best-effort, crash-isolated like step 0)
415
+ 7. **Docs reconcile check + auto re-index** (best-effort, crash-isolated like step 0)
364
416
 
365
- `/remember` never reconciles docs, never writes frontmatter, never edits a page. It
366
- prints at most one nudge line. Wrapped so any failure here can never block the memory
367
- write that already happened in steps 3-6.
417
+ `/remember` never reconciles doc CONTENT, never writes frontmatter, never edits a page
418
+ the only write here is the generated `docs/index.md` itself, via the same deterministic
419
+ `index-flat` script `/docs-builder` already uses, never a model call. Wrapped so any
420
+ failure here can never block the memory write that already happened in steps 3-6.
368
421
 
369
422
  - **Locate `docs-builder.cjs`** — bundled next to this command at
370
- `docs-builder/docs-builder.cjs` (same convention as `remember/friction.cjs`).
423
+ `docs-builder/docs-builder.cjs` (same convention as `remember/friction.cjs`). Call it by
424
+ its **absolute path** in the command below — the cwd here is the target repo, not this
425
+ package, so a cwd-relative path fails everywhere except the liteagents repo itself.
371
426
  - **Not applicable, stay silent:** if the project has no `docs/` directory, skip without
372
427
  saying anything. Most projects have no doc corpus and a nudge every run is noise.
373
428
  - **Applicable but could not run — say so, loudly:** if `docs/` exists but the script is
@@ -382,13 +437,28 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
382
437
  below.
383
438
  - Otherwise run it and pass through its verdict:
384
439
  ```bash
385
- node docs-builder/docs-builder.cjs due
440
+ node <docs-builder.cjs> due
386
441
  ```
387
442
  `due` compares `docs/` against the SHA stamped in `docs/.docs-builder/ledger.json`
388
443
  using `git diff --numstat -M`, classifying each doc as new / moved / moved+changed /
389
444
  changed (with the line delta and rough percentage) / deleted. It is **due at >=5
390
445
  changed docs** — the same derived-not-counted shape as `/stash`'s nudge.
391
- - If DUE, end with one line and nothing more:
446
+ - If `due` prints "no ledger yet" (no `docs/.docs-builder/ledger.json` to compare against),
447
+ do NOT relay it — print the same `/docs-builder reorg` line as the no-`docs/.docs-builder/`
448
+ case above, for the same reason: `ledger` would stamp an unsorted pile as correct.
449
+ - **Auto re-index — script only, no model, in addition to the DUE advisory below, not a
450
+ replacement for it.** If `due`'s output was NOT `docs unchanged since <sha>. NOT due.`
451
+ (i.e. it printed a row table -- any new/moved/moved+changed/changed/deleted doc, whether
452
+ or not the >=5 threshold below was crossed), the index has drifted and self-heals right
453
+ here, unconditionally:
454
+ ```bash
455
+ node <docs-builder.cjs> index-flat
456
+ ```
457
+ Same script `/docs-builder reorg` already calls, run standalone — no model call, no
458
+ interview, nothing moves. Note in the step-8 report that `docs/index.md` (and
459
+ `docs/log.md`, if `index-flat` touched it) were regenerated, so they are included
460
+ alongside whatever step 3-6 already changed when this run is committed.
461
+ - If DUE (the row count crossed the >=5 threshold), ALSO end with one line:
392
462
  ```
393
463
  docs: 7 changed since 991f72d3 — run /docs-builder reorg
394
464
  ```
@@ -396,7 +466,10 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
396
466
  8. **Report to user** — print it AND write the same content to `.claude/remember/report.md`
397
467
  (overwritten each run; the ledger keeps history — the report is just the latest snapshot)
398
468
  - Number of stashes processed
399
- - Facts count (before → after the rewrite; the number should not grow by the number of new facts)
469
+ - Facts count (before → after the rewrite) plus how many existing lines were merged or
470
+ shortened. At steady state — lines already ≤160, no near-duplicates — a run that grows by
471
+ exactly its new facts and shortens nothing is correct; say so rather than forcing merges
472
+ to hit a number. The bound on facts is the write-bar at entrance, not a count.
400
473
  - **Mechanical length check** — run, don't estimate. This confirms the step-3 gate rather
401
474
  than being the first check to catch an overrun. It implements the SAME mechanical
402
475
  exemption as the gate (a line whose longest backtick literal exceeds 100 chars is not
@@ -418,7 +491,9 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
418
491
  — every remaining overrun already had its chance to be exempted (100-char backtick
419
492
  literal) inside the step-3 gate, so anything printed here should not exist.
420
493
  - Episodes count (new, kept hot, folded + deleted)
421
- - Antigens count by confidence tier, with how many newly promoted to hot
494
+ - Antigens count by confidence tier, with how many newly promoted to hot — sourced
495
+ from `.claude/remember/friction/count_report.json` (4c's count report), not
496
+ recomputed by hand
422
497
  - Ledger lines — one per non-observing entry: id, short rule, status, recurrences since
423
498
  adoption. Highlight rephrased (RECURRED) and ESCALATED entries; escalations need a
424
499
  user decision, e.g.:
@@ -428,14 +503,16 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
428
503
  ledger: ag-002 "literal scoped ask" ESCALATED → Fact; 2 phrasings failed. Hook or accept?
429
504
  ```
430
505
  - If AGENT_RULES.md was bootstrapped this run, say so (one line)
506
+ - If step 7 ran the auto re-index, say so and name the regenerated files
507
+ (`docs/index.md`, plus `docs/log.md` if touched) so they are staged with this run
431
508
  - Confirm MEMORY.md and CLAUDE.md updated
432
509
 
433
510
  **File locations (all project-local — two dirs: `/stash` owns `.claude/stash/`, `/remember` owns `.claude/remember/`)**
434
511
  - Stash files: `.claude/stash/*.md`
435
512
  - Memory file: `.claude/remember/MEMORY.md` (single source of truth, referenced as `@.claude/remember/MEMORY.md`)
436
- - Rules template: `.claude/remember/AGENT_RULES.md` (bootstrapped once from the bundled package template on first `/remember` run, never overwritten again — user-owned after that; referenced as `@.claude/remember/AGENT_RULES.md`)
513
+ - Rules template: `.claude/remember/AGENT_RULES.md` (bootstrapped once from the bundled package template on first `/remember` run, never overwritten again — user-owned after that; referenced by a plain path pointer, not `@`-referenced — see step 5)
437
514
  - Antigen ledger: `.claude/remember/ledger.json` (per-rule evidence trail: class, status, attempts/rejected-buffer, recurrence-while-hot)
438
- - Consolidation report: `.claude/remember/report.md` (latest step-7 report, overwritten each run)
515
+ - Consolidation report: `.claude/remember/report.md` (latest step-8 report, overwritten each run)
439
516
  - Processed manifest: `.claude/remember/.processed`
440
517
  - Docs ledger (READ ONLY from here — owned by `/docs-builder`): `docs/.docs-builder/ledger.json`
441
518
  - Friction output (transient, regenerated each run): `.claude/remember/friction/` — `antigen_clusters.json` (preferred input), `antigen_review.md` (fallback), plus raw analysis files