liteagents 2.18.0 → 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,27 @@ 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
+
18
39
  ## [2.18.0] - 2026-08-26
19
40
 
20
41
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liteagents",
3
- "version": "2.18.0",
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) {
@@ -767,9 +786,10 @@ function indexFlat() {
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
@@ -747,7 +747,13 @@ Writes **one** `docs/index.md` covering the whole corpus, in three sections: `##
747
747
  (one row per file under `docs/product/`, plus any pages under `PAGES` — default `docs/wiki/`
748
748
  — if they exist, plus any doc still sitting in place elsewhere), `## Logs` (one row per file
749
749
  under `docs/logs/`), and `## Archive` (one row per file under `docs/archive/`). Each row is
750
- 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.
751
757
  Default destination `docs/index.md` — **the only writer of that default path** in this whole
752
758
  pipeline (nothing else writes an index at all).
753
759
  `search` reads `outline.json`, never `index.md`. Prints the row counts and records a `log.md`
@@ -346,12 +346,13 @@ Reads all raw material (`.amp/stash/*.md` + `.amp/remember/friction/antigen_clus
346
346
  inline duplication is needed
347
347
  - If `.amp/remember/AGENT_RULES.md` exists (bootstrapped in step 1), compose a second,
348
348
  independent section between `<!-- AGENT_RULES:START -->` and `<!-- AGENT_RULES:END -->`
349
- 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:
350
352
  ```
351
353
  <!-- AGENT_RULES:START -->
352
- Consult when building something new or adding a feature — a standards guide, not hot
353
- context like MEMORY.md above:
354
- @.amp/remember/AGENT_RULES.md
354
+ Standards guide (read when designing/building something new, not hot context):
355
+ .amp/remember/AGENT_RULES.md
355
356
  <!-- AGENT_RULES:END -->
356
357
  ```
357
358
  - Each marker pair is independent: if AGENT.md already has a given pair, replace the
@@ -411,11 +412,12 @@ Reads all raw material (`.amp/stash/*.md` + `.amp/remember/friction/antigen_clus
411
412
  6. **Update processed manifest**
412
413
  - Append paths of newly processed stashes to `.amp/remember/.processed`
413
414
 
414
- 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)
415
416
 
416
- `/remember` never reconciles docs, never writes frontmatter, never edits a page. It
417
- prints at most one nudge line. Wrapped so any failure here can never block the memory
418
- 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.
419
421
 
420
422
  - **Locate `docs-builder.cjs`** — bundled next to this command at
421
423
  `docs-builder/docs-builder.cjs` (same convention as `remember/friction.cjs`). Call it by
@@ -444,7 +446,19 @@ Reads all raw material (`.amp/stash/*.md` + `.amp/remember/friction/antigen_clus
444
446
  - If `due` prints "no ledger yet" (no `docs/.docs-builder/ledger.json` to compare against),
445
447
  do NOT relay it — print the same `/docs-builder reorg` line as the no-`docs/.docs-builder/`
446
448
  case above, for the same reason: `ledger` would stamp an unsorted pile as correct.
447
- - If DUE, end with one line and nothing more:
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:
448
462
  ```
449
463
  docs: 7 changed since 991f72d3 — run /docs-builder reorg
450
464
  ```
@@ -489,12 +503,14 @@ Reads all raw material (`.amp/stash/*.md` + `.amp/remember/friction/antigen_clus
489
503
  ledger: ag-002 "literal scoped ask" ESCALATED → Fact; 2 phrasings failed. Hook or accept?
490
504
  ```
491
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
492
508
  - Confirm MEMORY.md and AGENT.md updated
493
509
 
494
510
  **File locations (all project-local — two dirs: `/stash` owns `.amp/stash/`, `/remember` owns `.amp/remember/`)**
495
511
  - Stash files: `.amp/stash/*.md`
496
512
  - Memory file: `.amp/remember/MEMORY.md` (single source of truth, referenced as `@.amp/remember/MEMORY.md`)
497
- - Rules template: `.amp/remember/AGENT_RULES.md` (bootstrapped once from the bundled package template on first `/remember` run, never overwritten again — user-owned after that; referenced as `@.amp/remember/AGENT_RULES.md`)
513
+ - Rules template: `.amp/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)
498
514
  - Antigen ledger: `.amp/remember/ledger.json` (per-rule evidence trail: class, status, attempts/rejected-buffer, recurrence-while-hot)
499
515
  - Consolidation report: `.amp/remember/report.md` (latest step-8 report, overwritten each run)
500
516
  - Processed manifest: `.amp/remember/.processed`
@@ -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) {
@@ -767,9 +786,10 @@ function indexFlat() {
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
@@ -747,7 +747,13 @@ Writes **one** `docs/index.md` covering the whole corpus, in three sections: `##
747
747
  (one row per file under `docs/product/`, plus any pages under `PAGES` — default `docs/wiki/`
748
748
  — if they exist, plus any doc still sitting in place elsewhere), `## Logs` (one row per file
749
749
  under `docs/logs/`), and `## Archive` (one row per file under `docs/archive/`). Each row is
750
- 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.
751
757
  Default destination `docs/index.md` — **the only writer of that default path** in this whole
752
758
  pipeline (nothing else writes an index at all).
753
759
  `search` reads `outline.json`, never `index.md`. Prints the row counts and records a `log.md`
@@ -346,12 +346,13 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
346
346
  inline duplication is needed
347
347
  - If `.claude/remember/AGENT_RULES.md` exists (bootstrapped in step 1), compose a second,
348
348
  independent section between `<!-- AGENT_RULES:START -->` and `<!-- AGENT_RULES:END -->`
349
- 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:
350
352
  ```
351
353
  <!-- AGENT_RULES:START -->
352
- Consult when building something new or adding a feature — a standards guide, not hot
353
- context like MEMORY.md above:
354
- @.claude/remember/AGENT_RULES.md
354
+ Standards guide (read when designing/building something new, not hot context):
355
+ .claude/remember/AGENT_RULES.md
355
356
  <!-- AGENT_RULES:END -->
356
357
  ```
357
358
  - Each marker pair is independent: if CLAUDE.md already has a given pair, replace the
@@ -411,11 +412,12 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
411
412
  6. **Update processed manifest**
412
413
  - Append paths of newly processed stashes to `.claude/remember/.processed`
413
414
 
414
- 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)
415
416
 
416
- `/remember` never reconciles docs, never writes frontmatter, never edits a page. It
417
- prints at most one nudge line. Wrapped so any failure here can never block the memory
418
- 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.
419
421
 
420
422
  - **Locate `docs-builder.cjs`** — bundled next to this command at
421
423
  `docs-builder/docs-builder.cjs` (same convention as `remember/friction.cjs`). Call it by
@@ -444,7 +446,19 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
444
446
  - If `due` prints "no ledger yet" (no `docs/.docs-builder/ledger.json` to compare against),
445
447
  do NOT relay it — print the same `/docs-builder reorg` line as the no-`docs/.docs-builder/`
446
448
  case above, for the same reason: `ledger` would stamp an unsorted pile as correct.
447
- - If DUE, end with one line and nothing more:
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:
448
462
  ```
449
463
  docs: 7 changed since 991f72d3 — run /docs-builder reorg
450
464
  ```
@@ -489,12 +503,14 @@ Reads all raw material (`.claude/stash/*.md` + `.claude/remember/friction/antige
489
503
  ledger: ag-002 "literal scoped ask" ESCALATED → Fact; 2 phrasings failed. Hook or accept?
490
504
  ```
491
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
492
508
  - Confirm MEMORY.md and CLAUDE.md updated
493
509
 
494
510
  **File locations (all project-local — two dirs: `/stash` owns `.claude/stash/`, `/remember` owns `.claude/remember/`)**
495
511
  - Stash files: `.claude/stash/*.md`
496
512
  - Memory file: `.claude/remember/MEMORY.md` (single source of truth, referenced as `@.claude/remember/MEMORY.md`)
497
- - 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)
498
514
  - Antigen ledger: `.claude/remember/ledger.json` (per-rule evidence trail: class, status, attempts/rejected-buffer, recurrence-while-hot)
499
515
  - Consolidation report: `.claude/remember/report.md` (latest step-8 report, overwritten each run)
500
516
  - Processed manifest: `.claude/remember/.processed`
@@ -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) {
@@ -767,9 +786,10 @@ function indexFlat() {
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
@@ -747,7 +747,13 @@ Writes **one** `docs/index.md` covering the whole corpus, in three sections: `##
747
747
  (one row per file under `docs/product/`, plus any pages under `PAGES` — default `docs/wiki/`
748
748
  — if they exist, plus any doc still sitting in place elsewhere), `## Logs` (one row per file
749
749
  under `docs/logs/`), and `## Archive` (one row per file under `docs/archive/`). Each row is
750
- 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.
751
757
  Default destination `docs/index.md` — **the only writer of that default path** in this whole
752
758
  pipeline (nothing else writes an index at all).
753
759
  `search` reads `outline.json`, never `index.md`. Prints the row counts and records a `log.md`
@@ -346,12 +346,13 @@ Reads all raw material (`.factory/stash/*.md` + `.factory/remember/friction/anti
346
346
  inline duplication is needed
347
347
  - If `.factory/remember/AGENT_RULES.md` exists (bootstrapped in step 1), compose a second,
348
348
  independent section between `<!-- AGENT_RULES:START -->` and `<!-- AGENT_RULES:END -->`
349
- 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:
350
352
  ```
351
353
  <!-- AGENT_RULES:START -->
352
- Consult when building something new or adding a feature — a standards guide, not hot
353
- context like MEMORY.md above:
354
- @.factory/remember/AGENT_RULES.md
354
+ Standards guide (read when designing/building something new, not hot context):
355
+ .factory/remember/AGENT_RULES.md
355
356
  <!-- AGENT_RULES:END -->
356
357
  ```
357
358
  - Each marker pair is independent: if AGENTS.md already has a given pair, replace the
@@ -411,11 +412,12 @@ Reads all raw material (`.factory/stash/*.md` + `.factory/remember/friction/anti
411
412
  6. **Update processed manifest**
412
413
  - Append paths of newly processed stashes to `.factory/remember/.processed`
413
414
 
414
- 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)
415
416
 
416
- `/remember` never reconciles docs, never writes frontmatter, never edits a page. It
417
- prints at most one nudge line. Wrapped so any failure here can never block the memory
418
- 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.
419
421
 
420
422
  - **Locate `docs-builder.cjs`** — bundled next to this command at
421
423
  `docs-builder/docs-builder.cjs` (same convention as `remember/friction.cjs`). Call it by
@@ -444,7 +446,19 @@ Reads all raw material (`.factory/stash/*.md` + `.factory/remember/friction/anti
444
446
  - If `due` prints "no ledger yet" (no `docs/.docs-builder/ledger.json` to compare against),
445
447
  do NOT relay it — print the same `/docs-builder reorg` line as the no-`docs/.docs-builder/`
446
448
  case above, for the same reason: `ledger` would stamp an unsorted pile as correct.
447
- - If DUE, end with one line and nothing more:
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:
448
462
  ```
449
463
  docs: 7 changed since 991f72d3 — run /docs-builder reorg
450
464
  ```
@@ -489,12 +503,14 @@ Reads all raw material (`.factory/stash/*.md` + `.factory/remember/friction/anti
489
503
  ledger: ag-002 "literal scoped ask" ESCALATED → Fact; 2 phrasings failed. Hook or accept?
490
504
  ```
491
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
492
508
  - Confirm MEMORY.md and AGENTS.md updated
493
509
 
494
510
  **File locations (all project-local — two dirs: `/stash` owns `.factory/stash/`, `/remember` owns `.factory/remember/`)**
495
511
  - Stash files: `.factory/stash/*.md`
496
512
  - Memory file: `.factory/remember/MEMORY.md` (single source of truth, referenced as `@.factory/remember/MEMORY.md`)
497
- - Rules template: `.factory/remember/AGENT_RULES.md` (bootstrapped once from the bundled package template on first `/remember` run, never overwritten again — user-owned after that; referenced as `@.factory/remember/AGENT_RULES.md`)
513
+ - Rules template: `.factory/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)
498
514
  - Antigen ledger: `.factory/remember/ledger.json` (per-rule evidence trail: class, status, attempts/rejected-buffer, recurrence-while-hot)
499
515
  - Consolidation report: `.factory/remember/report.md` (latest step-8 report, overwritten each run)
500
516
  - Processed manifest: `.factory/remember/.processed`
@@ -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) {
@@ -767,9 +786,10 @@ function indexFlat() {
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
@@ -747,7 +747,13 @@ Writes **one** `docs/index.md` covering the whole corpus, in three sections: `##
747
747
  (one row per file under `docs/product/`, plus any pages under `PAGES` — default `docs/wiki/`
748
748
  — if they exist, plus any doc still sitting in place elsewhere), `## Logs` (one row per file
749
749
  under `docs/logs/`), and `## Archive` (one row per file under `docs/archive/`). Each row is
750
- 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.
751
757
  Default destination `docs/index.md` — **the only writer of that default path** in this whole
752
758
  pipeline (nothing else writes an index at all).
753
759
  `search` reads `outline.json`, never `index.md`. Prints the row counts and records a `log.md`
@@ -346,12 +346,13 @@ Reads all raw material (`.opencode/stash/*.md` + `.opencode/remember/friction/an
346
346
  inline duplication is needed
347
347
  - If `.opencode/remember/AGENT_RULES.md` exists (bootstrapped in step 1), compose a second,
348
348
  independent section between `<!-- AGENT_RULES:START -->` and `<!-- AGENT_RULES:END -->`
349
- 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:
350
352
  ```
351
353
  <!-- AGENT_RULES:START -->
352
- Consult when building something new or adding a feature — a standards guide, not hot
353
- context like MEMORY.md above:
354
- @.opencode/remember/AGENT_RULES.md
354
+ Standards guide (read when designing/building something new, not hot context):
355
+ .opencode/remember/AGENT_RULES.md
355
356
  <!-- AGENT_RULES:END -->
356
357
  ```
357
358
  - Each marker pair is independent: if AGENTS.md already has a given pair, replace the
@@ -411,11 +412,12 @@ Reads all raw material (`.opencode/stash/*.md` + `.opencode/remember/friction/an
411
412
  6. **Update processed manifest**
412
413
  - Append paths of newly processed stashes to `.opencode/remember/.processed`
413
414
 
414
- 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)
415
416
 
416
- `/remember` never reconciles docs, never writes frontmatter, never edits a page. It
417
- prints at most one nudge line. Wrapped so any failure here can never block the memory
418
- 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.
419
421
 
420
422
  - **Locate `docs-builder.cjs`** — bundled next to this command at
421
423
  `docs-builder/docs-builder.cjs` (same convention as `remember/friction.cjs`). Call it by
@@ -444,7 +446,19 @@ Reads all raw material (`.opencode/stash/*.md` + `.opencode/remember/friction/an
444
446
  - If `due` prints "no ledger yet" (no `docs/.docs-builder/ledger.json` to compare against),
445
447
  do NOT relay it — print the same `/docs-builder reorg` line as the no-`docs/.docs-builder/`
446
448
  case above, for the same reason: `ledger` would stamp an unsorted pile as correct.
447
- - If DUE, end with one line and nothing more:
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:
448
462
  ```
449
463
  docs: 7 changed since 991f72d3 — run /docs-builder reorg
450
464
  ```
@@ -489,12 +503,14 @@ Reads all raw material (`.opencode/stash/*.md` + `.opencode/remember/friction/an
489
503
  ledger: ag-002 "literal scoped ask" ESCALATED → Fact; 2 phrasings failed. Hook or accept?
490
504
  ```
491
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
492
508
  - Confirm MEMORY.md and AGENTS.md updated
493
509
 
494
510
  **File locations (all project-local — two dirs: `/stash` owns `.opencode/stash/`, `/remember` owns `.opencode/remember/`)**
495
511
  - Stash files: `.opencode/stash/*.md`
496
512
  - Memory file: `.opencode/remember/MEMORY.md` (single source of truth, referenced as `@.opencode/remember/MEMORY.md`)
497
- - Rules template: `.opencode/remember/AGENT_RULES.md` (bootstrapped once from the bundled package template on first `/remember` run, never overwritten again — user-owned after that; referenced as `@.opencode/remember/AGENT_RULES.md`)
513
+ - Rules template: `.opencode/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)
498
514
  - Antigen ledger: `.opencode/remember/ledger.json` (per-rule evidence trail: class, status, attempts/rejected-buffer, recurrence-while-hot)
499
515
  - Consolidation report: `.opencode/remember/report.md` (latest step-8 report, overwritten each run)
500
516
  - Processed manifest: `.opencode/remember/.processed`