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.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,132 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  - Enhanced testing capabilities
16
16
  - Performance optimizations
17
17
 
18
+ ## [2.19.0] - 2026-08-26
19
+
20
+ ### Changed
21
+ - **`docs/index.md` rows now list each doc's H2 headings, one per line, with a line range.**
22
+ The index is meant to let an agent find and slice-read a section without opening the doc —
23
+ previously each row carried only an H1, a line count, and a link, so an agent still had to
24
+ open the file to find anything inside it. Each H2 line reuses the exact `headings()` +
25
+ `fenceMask()` boundaries `scan` already writes to `outline.json` (no second parser), so a
26
+ heading inside a fenced code block still never appears. Archive rows stay H1-only — an
27
+ archived doc is frozen history, not a live section to route into.
28
+ - **`/remember` step 7 self-heals `docs/index.md` every run, not just at reorg time.** Any
29
+ drift `due` reports (new/moved/changed/deleted, not only the >=5-doc DUE threshold) now
30
+ also re-runs `index-flat` — script-only, no model call — so the index stays current between
31
+ full `/docs-builder reorg` passes instead of silently drifting until the next one.
32
+ - **`AGENT_RULES.md` demoted from an `@`-include to a plain path pointer.** It was wired into
33
+ CLAUDE.md as `@.claude/remember/AGENT_RULES.md`, which hot-loads the whole file into every
34
+ session even though it's documented as "not hot context" — measured at ~6.5k tokens/session
35
+ of standards prose loaded despite the file's own claim otherwise. `MEMORY.md` stays
36
+ `@`-referenced (it is hot); `AGENT_RULES.md` is now a plain path line, read only when
37
+ designing or building something new.
38
+
39
+ ## [2.18.0] - 2026-08-26
40
+
41
+ ### Changed
42
+ - **`/remember`'s antigen step redesigned to classify-then-count.** A 15-repo audit found
43
+ MEMORY.md's Antigens section hand-drifted from the ledger in 14/15 repos and rule-text
44
+ disagreement in 22 entries, root-caused to the model writing the same rule text in three
45
+ places and doing hash/dedup arithmetic in prose. Narrowed the LLM to one classification
46
+ judgment per cluster (`drop` | existing `ag-NNN` | `new:<theme>`); moved everything else —
47
+ hash union, dedup, promotion, rendering, and checking — into deterministic code
48
+ (`friction.cjs count`/`render`/`check`/`migrate-attempts`). Added invariants I6-new
49
+ (render(ledger) byte-equal to MEMORY.md's Antigens section) and I7 (`rule` ==
50
+ `attempts[last].rule`), Guard B (`new:` clusters never merge with each other in the same
51
+ classify batch), and an adopted-date gate so pre-fix evidence can't count toward
52
+ `recurred_while_hot`. `remember.md` rewritten as literal commands to run, not prose to
53
+ interpret. Validated live on 3 real repos; 947 tests passing, mirrored to all
54
+ four packages with 0 non-path diffs.
55
+
56
+ ### Fixed
57
+ - **`friction.cjs check` exited 0 when I6-new was NOT EQUAL** — only I7 could fail it, so an
58
+ automated caller saw a pass while MEMORY.md was hand-drifted from the ledger. Now exits 1 on
59
+ either invariant failing. Validated on real backups (zkagent NOT EQUAL → 1, bareloop 8 I7
60
+ mismatches → 1, liteagents EQUAL → 0).
61
+ - **`observing`→`hot` promotion in `friction.cjs count` wrote no history line and left
62
+ `attempts[last].adopted` at the candidate date**, so on the next run a conversation from
63
+ before the rule went hot counted toward `recurred_while_hot` — the adopted-date gate was
64
+ comparing against the wrong date. Promotion now appends `promoted to hot (N sessions)` and
65
+ re-stamps `adopted` to the run date. Reproduced and fixed on liteagents' real ag-003
66
+ (unfixed: rwh=1; fixed: rwh=0, gated).
67
+ - **`/remember` could append near-duplicate episodes when re-processing already-filed
68
+ stashes.** The Episodes section only ever appended; nothing checked whether a new episode
69
+ covered the same work as one already in the section. Step 4b's episode rule now dedups
70
+ before appending — merge into the existing entry (judged by content, not title) instead of
71
+ adding a second copy. Validated on the exact data that surfaced it: with the old rule, 4/5
72
+ duplicate pairs survived across 5 isolated runs; with dedup, 0/5, and the 10-most-recent cap
73
+ still held. All four packages.
74
+ - **A newly-created antigen entry's `rule` text was a literal placeholder string,
75
+ not real content.** `friction.cjs count` wrote
76
+ a hardcoded placeholder string into `rule` and
77
+ `attempts[0].rule` for any brand-new `ag-NNN` entry, and `render` printed it verbatim into
78
+ MEMORY.md — so the placeholder could land in a user's actual memory file. The 4a classifier
79
+ now emits the one-line rule text alongside a `new:<theme>` label in the same judgment (no
80
+ new LLM pass): `{cluster_index: {label: "new:<theme>", rule: "..."}}`. `count` accepts both
81
+ this shape and the old bare-string shape (bare stays valid for `drop`/`ag-NNN`, and for
82
+ `new:` clusters with `sessions < 2`, which never create an entry). A `new:` cluster with
83
+ `sessions >= 2` and no rule is reported as malformed and creates nothing — never falls back
84
+ to placeholder text. All four packages; 947 tests passing.
85
+ - **friction's severity axis was degenerate — every cluster it ever emitted was severe.**
86
+ Clusters are seeded only on an observed reaction (`user_correction`, `user_curse`,
87
+ `interrupt_cascade`), and the severe test accepted all three of those same signals, so the
88
+ thing required for a cluster to exist also made it severe. Measured 69/69 severe on the real
89
+ corpus (3170 sessions, 77 projects) and 66/66 on the privcloud fixture. The documented
90
+ recurrence × severity 2×2 was therefore a 1×2 on recurrence alone: `fact` (recurring + mild)
91
+ and `drop` (one-off + mild) had never once fired, and every one-off "no, do X instead" was
92
+ labelled an `episode` — 68 of them in a single `/remember` run. Severe now means intensity:
93
+ a curse, an interrupt cascade, or a tool error corroborating the reaction; a plain
94
+ correction is mild. Re-run on the same corpus: 69 → 31 clusters, all 38 dropped were
95
+ correction-only with no curse/interrupt/error. Regression fixture (i) plus a new assertion
96
+ on (h) (13 plain corrections → `fact`, not `antigen`) observed failing on pre-fix code;
97
+ five dedup fixtures gained a curse word so they stay observable. All four packages.
98
+ - **Review of both specs against their scripts found 24 more defects; each was reproduced,
99
+ approved, and then fixed one at a time by a delegated agent with a failing-first test.**
100
+ docs-builder script: `apply-reorg` wrote a config pointer to a `docs/index.md` that
101
+ `index-flat` had just declined to write (now skipped with a message); its results JSON
102
+ reported `claudeMdUpdated: false` because it printed before the flag was set; `discover` on
103
+ an already-sorted corpus told the operator to run a classification interview on a 0-row
104
+ plan; the usage string omitted `cleanup-apply`; and JSON state (`docs/.docs-builder/*`)
105
+ resolved against the cwd while `index.md`/ledger/config resolved against `REPO`, splitting
106
+ the state when run from anywhere but the root — `ARTIFACTS` now resolves under `REPO` at
107
+ the one chokepoint, explicit `OUT=`/path args unchanged. friction: when analyze found no
108
+ sessions it fell through to extract on the PREVIOUS run's `friction_analysis.json`, exit 0,
109
+ clobbering `antigen_clusters.json` to empty — the no-input case now returns a distinct code
110
+ (2; 1 was already the verdict) and stops. docs-builder spec: commands now `cd` to the target
111
+ root and call the script by absolute path (`$DB`), every `REPO=<repo>` prefix dropped (they
112
+ also never matched `allowed-tools: Bash(node:*)`); neither picker flow ever stamped the
113
+ ledger, so `due` said NOT due forever — both end with `ledger` after the commit; Modes table
114
+ pointed at a nonexistent "step 4"; empty follow-up list had no instruction; two stale
115
+ `docs/README.md` refs and "fifteen subcommands" (fourteen). remember spec: step 7 called
116
+ `docs-builder.cjs` cwd-relative (MODULE_NOT_FOUND on every repo but this one) and relayed
117
+ `due`'s "run ledger" advice that step 7 itself forbids; the `antigen_review.md` fallback has
118
+ no `session_ids`, so 4c would have counted every re-scan as recurrence (no counting on that
119
+ path now); the migration clause re-fired every run on an entry that matched nothing; the
120
+ early-stop condition depended on 4c's own output; step 5 rendered legacy 1-session
121
+ `observing` entries; three wrong step cross-references; "recursively" was two levels.
122
+ Also: both test suites leaked every `mkdtemp` dir (~1,000 per docs-builder run) until
123
+ `/tmp` ran out of inodes mid-session — both now remove them on exit (`KEEP_TMP=1` keeps).
124
+ Not changed, flagged as a design call: a friction `fact` (3+ sessions, mild) writes straight
125
+ into hot Facts with no ledger stage while an antigen needs 5 — reachable for the first time
126
+ since the severity fix.
127
+ - **`/docs-builder` could not find its own script outside this repo.** `docs-builder.md` wrote
128
+ every command as `node docs-builder/docs-builder.cjs …` without saying that path is
129
+ relative to the command's own directory (`~/.claude/commands/`), so on an external repo the
130
+ model searched the target tree, found nothing, and refused to run. Added the same "locate
131
+ the script first" step `remember.md` already has for `friction.cjs`. All four packages.
132
+
133
+ - **`/remember` step 4b routed a one-off severe friction cluster to "an Episode" that has no
134
+ home.** The Episodes section is stash-fed and capped at 10, and 4c already refuses a ledger
135
+ entry for a 1-session cluster — so the instruction contradicted the rest of the command and,
136
+ post-severity-fix, would have asked for ~30 cross-project one-offs to flush the stash
137
+ episodes. 4b now says what 4c already implied: a one-off is written nowhere; friction
138
+ re-surfaces it every run until it recurs, and at 2 sessions it gets a ledger `observing`
139
+ entry. Step 8's "facts should not grow by the number of new facts" expectation was reworded
140
+ too — measured on bareloop at steady state (273 facts, mean 131 chars, 0 near-duplicates)
141
+ the compressor correctly shortens nothing, and the old wording invited forced merges.
142
+ All four packages.
143
+
18
144
  ## [2.17.1] - 2026-08-25
19
145
 
20
146
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liteagents",
3
- "version": "2.17.1",
3
+ "version": "2.19.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": {
@@ -728,14 +728,33 @@ function plan(outlineF, labelsF) {
728
728
  // tripwire, never a prune, never a collapse, never a delete.
729
729
  const ARCHIVE_WARN_ROWS = 100; // stated default, not measured — see docs-builder-v3-spec.md
730
730
 
731
- function indexRow(rel, dest) {
731
+ // `includeH2` is false for archive rows: an archived doc is frozen history, not a live
732
+ // section a reader is being routed into, so its row stays H1 + line count + link only.
733
+ function indexRow(rel, dest, includeH2) {
732
734
  const text = read(rel);
733
- const h1 = (text.split('\n').find(l => l.startsWith('# ')) || '').slice(2).trim();
734
- const lines = text.split('\n').length;
735
+ const lines = text.split('\n');
736
+ // Same headings()+fenceMask() path scan() uses -- no second parser -- so an H2 inside a
737
+ // ``` fence is masked out here exactly as it is there.
738
+ const mask = fenceMask(lines);
739
+ const { h1, heads } = headings(lines, mask);
735
740
  // Read from INSIDE index.md, so the link must resolve relative to index.md's own
736
741
  // directory, not the repo root.
737
742
  const relLink = path.relative(path.dirname(dest), repoPath(rel)).split(path.sep).join('/');
738
- return `- [${h1 || path.basename(rel)}](${relLink}) — ${lines} lines\n`;
743
+ let row = `- [${h1 || path.basename(rel)}](${relLink}) — ${lines.length} lines\n`;
744
+ if (!includeH2) return row;
745
+ // One line per H2, each with its own line range: from the `## ` heading's own 1-based
746
+ // line through the line before the next heading of level <= 2 (H1 or H2), or EOF for the
747
+ // last one -- the SAME boundary scan() uses to write s/e into outline.json, re-derived
748
+ // here from the same heads array rather than duplicated as a second computation. Lets an
749
+ // agent jump straight to (and slice-read) a section without opening the doc at all.
750
+ const boundaries = heads.filter(h => h.lvl <= 2);
751
+ boundaries.forEach((h, i) => {
752
+ if (h.lvl !== 2) return;
753
+ const start = h.line;
754
+ const end = i + 1 < boundaries.length ? boundaries[i + 1].line - 1 : lines.length;
755
+ row += ` - ${h.text} (L${start}–${end})\n`;
756
+ });
757
+ return row;
739
758
  }
740
759
 
741
760
  function renderSection(title, rows) {
@@ -761,15 +780,16 @@ function indexFlat() {
761
780
 
762
781
  if (!productFiles.length && !logsFiles.length && !archiveFiles.length && !pageFiles.length) {
763
782
  console.log('nothing to index — run `discover` + `apply-reorg` first.');
764
- return;
783
+ return false; // caller (applyReorg) uses this to skip the config pointer — no index.md was written
765
784
  }
766
785
 
767
786
  const outRel = process.env.OUT || 'docs/index.md';
768
787
  const dest = repoPath(outRel);
769
788
  const productRows = [...productFiles, ...pageFiles].sort()
770
- .map(f => ({ file: f, row: indexRow(f, dest) }));
771
- const logsRows = logsFiles.sort().map(f => ({ file: f, row: indexRow(f, dest) }));
772
- const archiveRows = archiveFiles.sort().map(f => ({ file: f, row: indexRow(f, dest) }));
789
+ .map(f => ({ file: f, row: indexRow(f, dest, true) }));
790
+ const logsRows = logsFiles.sort().map(f => ({ file: f, row: indexRow(f, dest, true) }));
791
+ // Archive rows are deliberately H1-only -- frozen history, not a live section to route into.
792
+ const archiveRows = archiveFiles.sort().map(f => ({ file: f, row: indexRow(f, dest, false) }));
773
793
 
774
794
  let s = '# Index\n\n';
775
795
  // Unconditional — not gated on row count, unlike ARCHIVE_WARN_ROWS below: a reader should
@@ -1748,7 +1768,11 @@ function discover(root) {
1748
1768
  // writes had persisted. Carry-forward (above) is exactly why a re-run can arrive here with
1749
1769
  // buckets already set, so this has to report what is actually in the plan.
1750
1770
  const filled = rows.filter(r => r.bucket).length;
1751
- if (!filled) {
1771
+ if (!rows.length) {
1772
+ console.log(`plan written to docs/.docs-builder/reorg-plan.json — 0 rows. Nothing outside `
1773
+ + 'product/, logs/ and archive/ is left to classify — the corpus is already sorted. '
1774
+ + '`apply-reorg` will only rescan and rebuild the index.');
1775
+ } else if (!filled) {
1752
1776
  console.log(`plan written to docs/.docs-builder/reorg-plan.json — every row's \`suggested\` `
1753
1777
  + 'is a PRIOR, not a verdict, and `bucket` is empty. Run the classification interview '
1754
1778
  + '(docs-builder.md): feed the model the plan, get bucket+reason per row, get the user\'s '
@@ -1880,7 +1904,7 @@ const DOCS_INDEX_END = '<!-- DOCS_INDEX:END -->';
1880
1904
  function docsIndexBlock() {
1881
1905
  return `${DOCS_INDEX_START}\n`
1882
1906
  + 'Docs map: `docs/index.md` — every doc in this project, with line counts.\n'
1883
- + 'Too many rows to read whole? Search instead: `/docs-builder search <query words>`\n'
1907
+ + 'Search this corpus instead of reading it whole: `/docs-builder search <query words>`\n'
1884
1908
  + `${DOCS_INDEX_END}`;
1885
1909
  }
1886
1910
  function injectClaudeMdPointer() {
@@ -1969,7 +1993,6 @@ function applyReorg(planFile) {
1969
1993
  // (re)builds outline.json for a corpus that already sat in docs/product/docs/archive/docs/logs
1970
1994
  // from a previous run, e.g. after a manual git mv or a re-run with nothing left to do.
1971
1995
  scanWholeCorpus();
1972
- console.log(JSON.stringify(results, null, 1));
1973
1996
  if (splitCandidates.length) {
1974
1997
  // Ranked, logs last (spec §5): a prereg is a legitimate split target but rarely the best
1975
1998
  // NEXT one. Array.prototype.sort is stable in Node, so this only reorders logs to the
@@ -1982,18 +2005,23 @@ function applyReorg(planFile) {
1982
2005
  // v3: apply-reorg writes docs/index.md itself — a reorg-only corpus ends up indexed
1983
2006
  // without a second command. Runs unconditionally — oversized docs are sorted like anything
1984
2007
  // else now, so this was never conditional on them.
1985
- indexFlat();
1986
- // Crash-isolated, same spirit as the moveDoc() follow-up failures collected above: a
1987
- // failure to write the config file is a WARN, never a thrown error — it must not make an
1988
- // already-moved file look unmoved or fail the run.
2008
+ const indexed = indexFlat() !== false;
1989
2009
  const configName = process.env.CONFIG || 'CLAUDE.md';
1990
- try {
1991
- injectClaudeMdPointer();
1992
- results.claudeMdUpdated = true;
1993
- console.log(` updated ${configName} with the docs/index.md pointer`);
1994
- } catch (e) {
1995
- console.error(` WARN could not update ${configName} with the docs/index.md pointer: ${e.message}`);
2010
+ if (!indexed) {
2011
+ console.log(` skipped the ${configName} pointer — no docs/index.md was written`);
2012
+ } else {
2013
+ // Crash-isolated, same spirit as the moveDoc() follow-up failures collected above: a
2014
+ // failure to write the config file is a WARN, never a thrown error — it must not make an
2015
+ // already-moved file look unmoved or fail the run.
2016
+ try {
2017
+ injectClaudeMdPointer();
2018
+ results.claudeMdUpdated = true;
2019
+ console.log(` updated ${configName} with the docs/index.md pointer`);
2020
+ } catch (e) {
2021
+ console.error(` WARN could not update ${configName} with the docs/index.md pointer: ${e.message}`);
2022
+ }
1996
2023
  }
2024
+ console.log(JSON.stringify(results, null, 1));
1997
2025
  logOp('apply-reorg', `moved ${results.moved}, skipped ${results.skipped}, `
1998
2026
  + `${splitCandidates.length} oversized split candidate(s), `
1999
2027
  + `${results.linksRewritten} link(s) rewritten, ${results.syncFailed} sync failure(s), `
@@ -2327,7 +2355,7 @@ function cleanupApply(file, outlineF, labelsF) {
2327
2355
 
2328
2356
  // Machine state has one home. Callers can override with OUT, but the default must never
2329
2357
  // scatter JSON into whatever directory the user happened to be standing in.
2330
- const ARTIFACTS = 'docs/.docs-builder';
2358
+ const ARTIFACTS = path.join(REPO, 'docs/.docs-builder');
2331
2359
  function write(obj, fallback) {
2332
2360
  const dest = process.env.OUT || path.join(ARTIFACTS, fallback);
2333
2361
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -2352,7 +2380,7 @@ switch (cmd) {
2352
2380
  case 'cleanup-apply': cleanupApply(rest[0], rest[1], rest[2]); break;
2353
2381
  default:
2354
2382
  die('usage: docs-builder.cjs <scan|validate|plan|index-flat|search|archive|ledger|due|lint|'
2355
- + 'discover|apply-reorg|reorg|cleanup> [args]\n'
2383
+ + 'discover|apply-reorg|reorg|cleanup|cleanup-apply> [args]\n'
2356
2384
  + ' scan <file.md...> -> outline.json\n'
2357
2385
  + ' validate <outline.json> <labels.json> -> PASS/FAIL (exit 1 on FAIL)\n'
2358
2386
  + ' plan <outline.json> <labels.json> -> task-<theme>.json per page\n'
@@ -2368,6 +2396,7 @@ switch (cmd) {
2368
2396
  + 'drift summary if a ledger stamp exists (the single front door)\n'
2369
2397
  + ' cleanup <file.md> -> ONE named file: cost estimate, then scan\n'
2370
2398
  + ' (the ONLY entry point to the split pipeline)\n'
2399
+ + ' cleanup-apply <file.md> [outline] [labels] -> plan + pages + archive, after the cleanup interview\n'
2371
2400
  + 'env: REPO (default cwd), OUT (output path), INDEX (default docs/index.md), '
2372
2401
  + 'PAGES (default docs/wiki), TASKS (default docs/.docs-builder/tasks), '
2373
2402
  + 'N (search result count, default 10), OVERSIZED_LINES (default 500)');
@@ -9,7 +9,7 @@ allowed-tools: Read, Write, Edit, Grep, Glob, Task, AskUserQuestion, Bash(node:*
9
9
  # docs-builder
10
10
 
11
11
  Keep project docs **current, complete and findable**, and split a file when it outgrows
12
- its row in `docs/README.md`.
12
+ its row in `docs/index.md`.
13
13
 
14
14
  > **The honest label: this does NOT make docs cheaper to read.**
15
15
  > Measured four ways; best case is a tie with doing nothing. Cost tracks *findings*, not
@@ -36,6 +36,21 @@ carry over, the absolute prices do not.
36
36
 
37
37
  ## Invocation
38
38
 
39
+ **Locate the script first.** `docs-builder.cjs` is bundled next to this command at
40
+ `docs-builder/docs-builder.cjs` — the same directory as this file, whether installed or run
41
+ from the package. Never search the target repo for it, never reconstruct it from this spec,
42
+ and if it truly exists nowhere say so and stop. Set `DB` to its ABSOLUTE path, then `cd` to
43
+ the target repo's root:
44
+
45
+ ```
46
+ DB=<absolute path to docs-builder.cjs>
47
+ cd <target repo root>
48
+ ```
49
+
50
+ Every command below is `node $DB …`; everything the script writes (`docs/.docs-builder/*`
51
+ JSON state, `docs/index.md`, the ledger, the log, the config pointer) lands under the target
52
+ repo. `REPO=` is optional and only needed when not running from the repo root.
53
+
39
54
  **With an argument** (`reorg`, `cleanup <file>`, or `search <query words...>`) — run that mode
40
55
  directly, no question asked.
41
56
 
@@ -82,10 +97,17 @@ read-only — no model cost, no interview, nothing moves.
82
97
  3. `apply-reorg` moves every row, **oversized included** — size only decides whether a doc is
83
98
  *splittable*, not whether it gets sorted. It refuses outright if any row's `bucket` is
84
99
  still empty. Afterward it prints the oversized docs it just moved as a follow-up list,
85
- `cleanup <NEW path> (N lines)`, `logs/` entries last. Show that list, then **ask which to
86
- split** (any, all, none). Only then run `cleanup <file>` (Mode 1) on each chosen file
100
+ `cleanup <NEW path> (N lines)`, `logs/` entries last. If the list is empty (nothing
101
+ oversized), say so and skip the split question; otherwise show the list, then **ask which
102
+ to split** (any, all, none). Only then run `cleanup <file>` (Mode 1) on each chosen file —
87
103
  `cleanup` itself prints the estimated split cost for that one file, then a mechanical
88
104
  shape report, then stops for its own interview (Mode 1, step 1b) before anything else runs.
105
+ Before that first commit, add `docs/.docs-builder/` to `.gitignore` if it is not already
106
+ ignored: it is machine state, regenerated every run, and the ledger stamp is per-clone by
107
+ design — it must never ride into history on a later `git add -A`.
108
+ Once the moves are committed, run `node $DB ledger` — nothing in steps 1-3 stamps the
109
+ ledger, and without the stamp `due` stays NOT due, the picker's verdict stays uninformed,
110
+ and `/remember`'s docs nudge never fires.
89
111
 
90
112
  The two stops are deliberate and different. Step 2 guards *correctness* — the interview and
91
113
  the user's approval, before a single file moves. Step 3's follow-up guards *cost* — splitting
@@ -96,7 +118,8 @@ when they pick "First run". Never split N files in one shot on an unseen list.
96
118
  first, if a ledger stamp exists, then it runs `discover`. If any row's `bucket` is still
97
119
  empty (true on a genuine first run, or when new files appeared since the last classification),
98
120
  `reorg` **stops right there** and prints what to do next — it never silently proceeds past an
99
- unclassified plan. Once the plan is fully classified (an already-sorted corpus's re-run
121
+ unclassified plan. Commit what it changed, then run `node $DB ledger` to move the stamp. Once
122
+ the plan is fully classified (an already-sorted corpus's re-run
100
123
  carries its prior classifications forward automatically — see "Discover is idempotent"
101
124
  below), `reorg` continues straight through `apply-reorg` → `lint`, no further stop, so
102
125
  `index.md` and `lint.json` stay current. This is the common, cheap case for a corpus that is
@@ -109,7 +132,7 @@ already sorted: nothing new to classify, so the interview gate never fires.
109
132
  | Mode | Menu option | Does | Destructive |
110
133
  |---|---|---|---|
111
134
  | `/docs-builder reorg` (discover, classification interview, confirm, then apply-reorg) | *First run*, steps 1-3 | classify a WHOLE corpus into product/logs/archive | no (moves are `git mv`, plan classified and reviewed first) |
112
- | `/docs-builder cleanup <file>` | *First run*, step 4 | measure ONE named oversized doc (cost, scan, heading shape) → **stops for the interview** | no (measure-only; original preserved) |
135
+ | `/docs-builder cleanup <file>` | *First run*, step 3's split question | measure ONE named oversized doc (cost, scan, heading shape) → **stops for the interview** | no (measure-only; original preserved) |
113
136
  | `/docs-builder reorg` (bare `docs-builder.cjs reorg`) | *Docs drift* | due's drift summary (if a ledger stamp exists) + discover → (stops here if anything is still unclassified) → apply-reorg → lint, whole corpus | no |
114
137
  | `/docs-builder search <query words...>` | *(none — explicit-argument mode only, never offered in the bare picker)* | BM25-rank sections of `docs/.docs-builder/outline.json` against the query, read-only | no |
115
138
 
@@ -206,7 +229,7 @@ purpose, not silently dropped.
206
229
  ### 1. Discover (script) — enriches and PROPOSES, never classifies, never moves
207
230
 
208
231
  ```bash
209
- REPO=<repo> node docs-builder/docs-builder.cjs discover # defaults to docs/
232
+ node $DB discover # defaults to docs/
210
233
  ```
211
234
 
212
235
  Recursively finds every `*.md` under the root (skipping `wiki/`, `logs/`, `archive/`,
@@ -317,7 +340,7 @@ files with no gate at all.
317
340
  ### 3. Apply (script) — an ALREADY-CLASSIFIED plan, verified moves, survives a bad file
318
341
 
319
342
  ```bash
320
- CONFIG=AGENT.md node docs-builder/docs-builder.cjs apply-reorg # defaults to the plan above
343
+ CONFIG=AGENT.md node $DB apply-reorg # defaults to the plan above
321
344
  ```
322
345
 
323
346
  **Refuses outright if any row's `bucket` is still empty** — the interview-has-not-happened
@@ -455,7 +478,7 @@ real page count isn't known until the model groups sections in step 2), runs ste
455
478
  below for you, then measures the document's heading shape and **stops**:
456
479
 
457
480
  ```bash
458
- REPO=<repo> node docs-builder/docs-builder.cjs cleanup docs/BIG.md
481
+ node $DB cleanup docs/BIG.md
459
482
  ```
460
483
 
461
484
  **Nothing past this command runs until a human has answered the interview (step 1b) below.**
@@ -468,8 +491,8 @@ choosing to continue, and confirming, the themes.
468
491
  ### 1. Scan (script) — run automatically by `cleanup`, shown here for what it produces
469
492
 
470
493
  ```bash
471
- REPO=<repo> OUT=docs/.docs-builder/outline.json \
472
- node docs-builder/docs-builder.cjs scan docs/BIG.md
494
+ OUT=docs/.docs-builder/outline.json \
495
+ node $DB scan docs/BIG.md
473
496
  ```
474
497
 
475
498
  One record per H2, each carrying the doc's H1 identity, a 2-line snippet, every H3 **with
@@ -560,7 +583,7 @@ it `false`) on every other theme.
560
583
  ### 3. Validate (script) — **hard gate, exits 1 on failure**
561
584
 
562
585
  ```bash
563
- REPO=<repo> node docs-builder/docs-builder.cjs validate \
586
+ node $DB validate \
564
587
  docs/.docs-builder/{outline,labels}.json
565
588
  ```
566
589
 
@@ -586,7 +609,7 @@ index, which is gone — on the one whole-corpus index it would silently skip mo
586
609
  ### 4. Plan + apply (script) — `cleanup-apply`, the door back in after the interview
587
610
 
588
611
  ```bash
589
- REPO=<repo> node docs-builder/docs-builder.cjs cleanup-apply docs/BIG.md \
612
+ node $DB cleanup-apply docs/BIG.md \
590
613
  docs/.docs-builder/outline.json docs/.docs-builder/labels.json
591
614
  ```
592
615
 
@@ -616,7 +639,7 @@ the checkpoint; there is no separate state file to go stale. It is also still ru
616
639
  own:
617
640
 
618
641
  ```bash
619
- REPO=<repo> OUT=docs/.docs-builder/tasks node docs-builder/docs-builder.cjs plan \
642
+ OUT=docs/.docs-builder/tasks node $DB plan \
620
643
  docs/.docs-builder/{outline,labels}.json
621
644
  ```
622
645
 
@@ -653,7 +676,7 @@ Each agent reads **only its own line ranges**. The value is context isolation.
653
676
  ### 6. Archive the original (script) — run for you by `cleanup-apply` once all pages exist
654
677
 
655
678
  ```bash
656
- REPO=<repo> node docs-builder/docs-builder.cjs archive docs/BIG.md
679
+ node $DB archive docs/BIG.md
657
680
  ```
658
681
 
659
682
  A **verified move**, not a copy: hash → `git mv` (so history follows) → hash again →
@@ -697,7 +720,7 @@ which defaults the outline path and takes only the query. The underlying script
697
720
  works directly, and is what the slash command runs:
698
721
 
699
722
  ```bash
700
- REPO=<repo> node docs-builder/docs-builder.cjs search docs/.docs-builder/outline.json <query words...>
723
+ node $DB search docs/.docs-builder/outline.json <query words...>
701
724
  ```
702
725
 
703
726
  BM25 over each section's real text (no deps, no separate index to build — it reads
@@ -717,14 +740,20 @@ reorg, not only whichever ones a split happened to touch.
717
740
  some archived docs).
718
741
 
719
742
  ```bash
720
- REPO=<repo> node docs-builder/docs-builder.cjs index-flat
743
+ node $DB index-flat
721
744
  ```
722
745
 
723
746
  Writes **one** `docs/index.md` covering the whole corpus, in three sections: `## Product`
724
747
  (one row per file under `docs/product/`, plus any pages under `PAGES` — default `docs/wiki/`
725
748
  — if they exist, plus any doc still sitting in place elsewhere), `## Logs` (one row per file
726
749
  under `docs/logs/`), and `## Archive` (one row per file under `docs/archive/`). Each row is
727
- an H1 title, a line count, and a link. No theme grouping, no `labels.json`, no model call.
750
+ an H1 title, a line count, and a link, plus one indented line per H2 heading (in document
751
+ order) so an agent can find and slice-read a section without opening the doc — each H2
752
+ line carries its own `(Lstart–end)` line range, reusing the SAME `headings()`+`fenceMask()`
753
+ boundaries `scan` already writes to `outline.json` (no second parser). Omitted when the doc
754
+ has no H2s. `## Archive` rows are H1-only, never H2 lines — an archived doc is frozen
755
+ history, not a live section to route into. No theme grouping, no `labels.json`, no model
756
+ call.
728
757
  Default destination `docs/index.md` — **the only writer of that default path** in this whole
729
758
  pipeline (nothing else writes an index at all).
730
759
  `search` reads `outline.json`, never `index.md`. Prints the row counts and records a `log.md`
@@ -745,7 +774,7 @@ v3 folds the old `reconcile` and `due` commands into one: "first run" (nothing s
745
774
  state, and two separate commands only made users guess which one to run.
746
775
 
747
776
  ```bash
748
- REPO=<repo> node docs-builder/docs-builder.cjs reorg
777
+ node $DB reorg
749
778
  ```
750
779
 
751
780
  If a ledger stamp exists (see "Knowing when reorg is due" below), its `due`-style drift
@@ -773,7 +802,7 @@ still runnable by hand once a `labels.json` exists.
773
802
  `lint` is also runnable standalone, on any file list, not only as part of `reorg`:
774
803
 
775
804
  ```bash
776
- REPO=<repo> node docs-builder/docs-builder.cjs lint <file.md...>
805
+ node $DB lint <file.md...>
777
806
  ```
778
807
 
779
808
  -> `lint.json`. Every check below is declared-only (see the governing rule further down) —
@@ -812,8 +841,8 @@ git is the diff engine. The ledger stores only the one thing git cannot know —
812
841
  last consolidated** — so the two can never drift apart.
813
842
 
814
843
  ```bash
815
- node docs-builder/docs-builder.cjs ledger # stamp the current state (run after a reorg)
816
- node docs-builder/docs-builder.cjs due # what changed since, and by how much
844
+ node $DB ledger # stamp the current state (run after COMMITTING a reorg)
845
+ node $DB due # what changed since, and by how much
817
846
  ```
818
847
 
819
848
  `due` classifies every doc against the stamped SHA using `git diff --numstat -M`: