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 +126 -0
- package/package.json +1 -1
- package/packages/ampcode/commands/docs-builder/docs-builder.cjs +52 -23
- package/packages/ampcode/commands/docs-builder.md +50 -21
- package/packages/ampcode/commands/remember/friction.cjs +407 -7
- package/packages/ampcode/commands/remember.md +236 -159
- package/packages/claude/commands/docs-builder/docs-builder.cjs +52 -23
- package/packages/claude/commands/docs-builder.md +50 -21
- package/packages/claude/commands/remember/friction.cjs +407 -7
- package/packages/claude/commands/remember.md +236 -159
- package/packages/droid/commands/docs-builder/docs-builder.cjs +52 -23
- package/packages/droid/commands/docs-builder.md +50 -21
- package/packages/droid/commands/remember/friction.cjs +407 -7
- package/packages/droid/commands/remember.md +236 -159
- package/packages/opencode/command/docs-builder/docs-builder.cjs +52 -23
- package/packages/opencode/command/docs-builder.md +50 -21
- package/packages/opencode/command/remember/friction.cjs +407 -7
- package/packages/opencode/command/remember.md +236 -159
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.
|
|
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
|
-
|
|
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
|
|
734
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
+ '
|
|
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
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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/
|
|
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.
|
|
86
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
node
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
816
|
-
node
|
|
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`:
|