@treeseed/dsh-chapters 0.1.24 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +2 -1
  2. package/cordis.patch.yml +49 -1
  3. package/docs/architecture.md +1 -496
  4. package/docs/backlog.md +234 -4
  5. package/docs/contract.md +127 -1
  6. package/docs/development.md +248 -12
  7. package/docs/host-compaction-seam.md +108 -0
  8. package/docs/knowledge-repo.md +532 -16
  9. package/docs/project-state.md +323 -0
  10. package/docs/verify.md +266 -0
  11. package/lib/archive.d.ts +49 -1
  12. package/lib/archive.js +56 -2
  13. package/lib/archive.js.map +1 -1
  14. package/lib/arrival.js +24 -10
  15. package/lib/arrival.js.map +1 -1
  16. package/lib/attach.d.ts +46 -0
  17. package/lib/attach.js +120 -0
  18. package/lib/attach.js.map +1 -0
  19. package/lib/auto-save.d.ts +74 -0
  20. package/lib/auto-save.js +175 -26
  21. package/lib/auto-save.js.map +1 -1
  22. package/lib/client.js +413 -11
  23. package/lib/client.js.map +1 -1
  24. package/lib/commands.d.ts +8 -1
  25. package/lib/commands.js +135 -7
  26. package/lib/commands.js.map +1 -1
  27. package/lib/compose.js +25 -6
  28. package/lib/compose.js.map +1 -1
  29. package/lib/continue-core.d.ts +24 -1
  30. package/lib/continue-core.js +41 -9
  31. package/lib/continue-core.js.map +1 -1
  32. package/lib/diag.d.ts +55 -0
  33. package/lib/diag.js +186 -0
  34. package/lib/diag.js.map +1 -0
  35. package/lib/engine-core.d.ts +53 -1
  36. package/lib/engine-core.js +64 -7
  37. package/lib/engine-core.js.map +1 -1
  38. package/lib/engine.d.ts +99 -0
  39. package/lib/engine.js +488 -36
  40. package/lib/engine.js.map +1 -1
  41. package/lib/enrich-queue.js +28 -2
  42. package/lib/enrich-queue.js.map +1 -1
  43. package/lib/enrich-wire.d.ts +92 -2
  44. package/lib/enrich-wire.js +135 -17
  45. package/lib/enrich-wire.js.map +1 -1
  46. package/lib/enrich.d.ts +27 -2
  47. package/lib/enrich.js +114 -18
  48. package/lib/enrich.js.map +1 -1
  49. package/lib/index.d.ts +52 -0
  50. package/lib/index.js +182 -2
  51. package/lib/index.js.map +1 -1
  52. package/lib/indexing.d.ts +7 -0
  53. package/lib/indexing.js.map +1 -1
  54. package/lib/injections.d.ts +1 -0
  55. package/lib/injections.js +38 -0
  56. package/lib/injections.js.map +1 -1
  57. package/lib/last-resort.d.ts +24 -0
  58. package/lib/last-resort.js +27 -0
  59. package/lib/last-resort.js.map +1 -0
  60. package/lib/prompt.d.ts +130 -0
  61. package/lib/prompt.js +255 -0
  62. package/lib/prompt.js.map +1 -0
  63. package/lib/registry.d.ts +39 -1
  64. package/lib/registry.js +76 -9
  65. package/lib/registry.js.map +1 -1
  66. package/lib/render.d.ts +31 -3
  67. package/lib/render.js +75 -3
  68. package/lib/render.js.map +1 -1
  69. package/lib/repair-run.d.ts +49 -0
  70. package/lib/repair-run.js +423 -0
  71. package/lib/repair-run.js.map +1 -0
  72. package/lib/repair.d.ts +200 -0
  73. package/lib/repair.js +354 -0
  74. package/lib/repair.js.map +1 -0
  75. package/lib/rules-commands.d.ts +22 -0
  76. package/lib/rules-commands.js +177 -20
  77. package/lib/rules-commands.js.map +1 -1
  78. package/lib/rules.d.ts +184 -7
  79. package/lib/rules.js +312 -27
  80. package/lib/rules.js.map +1 -1
  81. package/lib/save-core.d.ts +31 -1
  82. package/lib/save-core.js +153 -17
  83. package/lib/save-core.js.map +1 -1
  84. package/lib/session-rules.d.ts +222 -0
  85. package/lib/session-rules.js +454 -0
  86. package/lib/session-rules.js.map +1 -0
  87. package/lib/store.d.ts +22 -1
  88. package/lib/store.js +94 -17
  89. package/lib/store.js.map +1 -1
  90. package/lib/sync.js +13 -1
  91. package/lib/sync.js.map +1 -1
  92. package/lib/tools.d.ts +14 -0
  93. package/lib/tools.js +321 -32
  94. package/lib/tools.js.map +1 -1
  95. package/lib/types.d.ts +12 -1
  96. package/lib/types.js.map +1 -1
  97. package/lib/version.d.ts +9 -0
  98. package/lib/version.js +10 -0
  99. package/lib/version.js.map +1 -0
  100. package/lib/warnings.d.ts +10 -0
  101. package/lib/warnings.js +18 -0
  102. package/lib/warnings.js.map +1 -0
  103. package/package.json +9 -3
@@ -0,0 +1,323 @@
1
+ # Project state — the history of what was built and measured
2
+
3
+ **Moved out of `AGENTS.md` verbatim on 2026-10-22.** `AGENTS.md` is loaded into every agent session
4
+ against a workspace instruction budget, and it had grown to 70,401 bytes — over the limit, so the file was
5
+ being silently TRUNCATED when delivered: the rules near the top survived and the ones at the bottom did not,
6
+ which is worse than a smaller honest file. This is the project's record of what happened, what was measured,
7
+ and what remains unverified. It is reference material, not a rule surface: **read it when you need the
8
+ history of a decision, not before every task.** The rules that lived inside it are in
9
+ `AGENTS.md` § Rig and metering rules, and the load-bearing spec stays in `docs/`.
10
+
11
+ ---
12
+
13
+ ## Start Here
14
+ ## 2026-10-22/23 — the host 0.2.0-rc.2 acceptance sweep (#56)
15
+
16
+ **What actually happened, in order.** The pinned host moved to 0.2.x; the tape corpus stayed at
17
+ 0.1.5-rc.1, so `CORPUS_HOST` closed the acceptance chain and no e2e claim was current (#56/#57). While
18
+ re-anchoring it, seven distinct failures turned out to be **one root cause plus six real defects**: the
19
+ preset pin had been written to a row id the host retired (`agent-presets` → `agent-preset-registry`), so
20
+ every browser boot since the move ran the STANDARD realm — no `compaction/*`, no arrival artifacting, no
21
+ `chapters_rules`. The legs were measuring an absence. Fixing the pin is what made the other six visible.
22
+
23
+ **Defects found by measurement, each with red-first rows, on `feature/56-reanchor-tape-corpus`:**
24
+ - **#77** — the continuation door called `sessions.open`, a verb that does not exist; the real seam is
25
+ `uiWorkspace.openSession` (`docs/contract.md` § Client Sessions Service). Three evenings of "the client
26
+ refuses to catalogue plugin-created children" were a door that never switched anything, while the tool
27
+ reported success. The door now verifies the switch from live state and degrades to
28
+ `switched:false, note:…` rather than claiming it.
29
+ - **The flattened 0.2.x `tool/result` shape** silently disabled arrival artifacting chain-wide (the
30
+ probe decoded `data.message` at the top level, where 0.2.x puts `data.message.message`). Arrival
31
+ artifacted nothing because the predicate was never true — `src/arrival.ts` now takes both dialects,
32
+ with a row that rebuilds the assertion against the host's real event shape.
33
+ - **#79** — the composer ghost ran `command/run` on **every render** (measured 4–26 per session, into
34
+ the transcript this plugin archives) because a `useEffect` listed `[props]` under a comment claiming
35
+ "once per mount"; and it kept pulling suggestions from a disabled feature.
36
+ - **#78** — relief that declines while the surface is over the trigger said nothing. It now prints
37
+ `surface=/trigger=/window=/ratio=` plus the offending node count; observed live:
38
+ `relief declined … (surface=33442 trigger=16000 window=32000 ratio=0.5)`. The engine deliberately does
39
+ **not** override the operator's arrival floor (AGENTS calls that switch a behavior switch, and a rig
40
+ that reinterprets config is the same dishonesty as a pin that is collected and never applied).
41
+ - **#81/#84** — `typeComposer` decided "did my send work?" from the editor's own emptiness, which the
42
+ host clears asynchronously: one session received the same instruction twice (seq 40 and 47). Then the
43
+ guard, matching an 80-character prefix, **swallowed** a genuinely different second instruction. Delivery
44
+ is now decided from the durable log, by the full instruction, after a bounded grace — and a drift row
45
+ fails if any composer call decides from the editor again.
46
+ - **#85** — replay booted a **different realm than record** because the rig's regime lived in three
47
+ places (distill tables, the chain script's inline env, boot defaults) and Playwright project `env`
48
+ reaches only workers, so `globalSetup` used defaults. One table now (`tests/e2e/rig-pins.ts`), read by
49
+ both the config and the boot.
50
+ - **#87** — the deepest one, and the last to become visible: **relief shadows the newest human
51
+ instruction.** Measured from one session's durable log: 13 compaction summaries, 108 distinct shadowed
52
+ seqs, `STILL-HERE` sent eleven times, **ten of them shadowed away**, final assistant text empty.
53
+ `retainRatio` reserves a token fraction as a verbatim tail, so an oversized node in the tail consumes
54
+ it and the question disappears with no error anywhere. The fix is inside our seam and honors
55
+ invariant 1: `carryOpenRequest` appends the newest human turn **verbatim** into the summary we author
56
+ (`openRequestCarryMaxChars`, default 4000; truncation is stated, never silent; injected scaffolding is
57
+ excluded by the one canonical predicate, `isInjectedProse`).
58
+
59
+ **The instruments earned their keep, and four bugs lived in the instruments.** `pending` never
60
+ matched a JSON-escaped newline; `ids.size > 0` disabled every #77 candidate; a report helper resolved
61
+ logs against the import-time home so its own gate could never pass; an unbounded
62
+ `scrollIntoViewIfNeeded` hung a leg for 17 minutes; my own MISS dump was swallowed by its own `catch`
63
+ for a whole run; and a second injected-prose list was caught by the #59 drift row within minutes.
64
+ `docs/verify.md` now carries a release gate shaped by these failures ("an unexercised assertion is a
65
+ rumor of an assertion"), and `npm run typecheck:e2e` exposes **77 errors in a browser plane that has
66
+ never been type-checked** (#83) — after I subtracted the ones my own new code caused.
67
+
68
+ **Still open as this lands:** #86 (a session with the arrival floor off can livelock on one oversized
69
+ result — 89 requests in 20 minutes, three outcomes for the same leg minutes apart), #80 (plugin command
70
+ results are durable but render nothing operator-visible), #82 (a distill and the deterministic suite share
71
+ the corpus directory), #81 criteria 3-4, and the #78 A/B product decision, which is the maintainer's.
72
+
73
+
74
+ **Phase 0 passed.** Across nine probe boots on an isolated instance, a plugin created a session seeded with
75
+ exactly one synthetic `user/message`, and that session was **listed** by `sessionQuery.listSessions`,
76
+ **resumable** via `agents.resume({ resumeSessionId })` after disposing the create handle, **durable across a
77
+ process kill**, and its TOC text round-tripped **byte-identical**. Structurally the head is bounded: ~102
78
+ tokens of notice versus ~2,476 for the equivalent transcript. Full measurements and the seed contract:
79
+ [spikes/probe/FINDINGS.md](spikes/probe/FINDINGS.md).
80
+
81
+ **Phase 0b passed (rounds 12-17): the compaction seam is real.** A plugin-subclassed `BasicCompactionEngine`
82
+ with a deterministic `summarize()` ran both the manual (`/compact`) and automatic (`agent/pre-step`)
83
+ paths to durable commit — surface 504→206 tokens, **zero summarization tokens**, log append-only
84
+ throughout. The live engine slot is the **agent-preset realm**, not the host plane; double-provisioning
85
+ one plane fails boot loudly; and the Phase 1 mount is a shipped preset naming `dsh-chapters` +
86
+ `setup`-mounted creation (all of it measured, cited, in
87
+ [docs/host-compaction-seam.md](docs/host-compaction-seam.md) and FINDINGS).
88
+
89
+ **Phase 1 stages 0-4 landed (rounds 18-22): the whole loop works with the real parts.** Registry/store/engine/
90
+ preset-install/tools pass — a realm row names our subpath export (`dsh-chapters/engine`), the cascade
91
+ finalizes post-commit, `presets/chapters/` installs itself into `.agent-presets` at boot (profile
92
+ `!!js` has no `require`, copying was the delivery mechanism) — and since 0.1.16 that copy validates the
93
+ WHOLE shipped file set and repairs missing files additively: the guard once tested only for `preset.yml`
94
+ while `agent.cordis.yml` is the file that mounts the engine, so a partial wipe was a permanent silent
95
+ failure (never clobber a user's edits; DO restore what is absent), and r22 closed the MVP loop: parent →
96
+ `chapters_segment` → `chapters_continue` (ranges only; plugin renders) → TOC-only child with the composed
97
+ preset (15K head) → the CHILD `read` its chapter and echoed its title back. Two real bugs caught and
98
+ fixed en route: finalization must run on the rejection path too (the retry loop can commit, then throw),
99
+ and `validateRanges` refuses before any file is written.
100
+
101
+ **Host 0.1.7-alpha.1 retired the preset mechanism that paragraph describes — do not carry it forward as
102
+ current.** Nothing reads `$DSH_HOME/.agent-presets` any more; a preset is an `@deepseek-ai/dsh-agent-preset`
103
+ *declaration row*, and the chapters preset IS one now: `preset-chapters`, inserted by our own bundle patch,
104
+ with the preset definition in its `config` — so it is a visible Loader entry you can inspect, override, or
105
+ `disabled: true` from a profile patch, exactly like the host's `preset-standard`. **Its row module is ours**
106
+ (`@treeseed/dsh-chapters/preset`, `src/preset.ts`), not the host package: it mirrors the host's plugin line
107
+ for line (`inject: ['agentPresets']`, `static [EntryGroup.key] = true` so child `!!js` expressions survive
108
+ until the child activates, `[Service.init]` yielding the registry disposer so **Cordis** owns the
109
+ registration's lifetime) and adds drop-what-will-not-load. Why not name the host package: one unresolvable
110
+ row aborts the plugin tree of a host predating declaration rows, and one unresolvable *child* makes the
111
+ whole preset `broken` — measured both ways; our module resolves wherever our package runs, and pruning a
112
+ missing child keeps the preset usable instead of declaring it unusable. **The lifecycle is the trap that
113
+ took longest**: registering in `apply()` and disposing through `ctx.effect` un-registered the preset the
114
+ moment activation finished (`register()` reported success while the roster came back WITHOUT `chapters`) —
115
+ same apply-fiber trap r35 caught for the storage domain; a row's own init is the only place with the right
116
+ lifetime. **Support floor is 0.2.0** (user decision 2026-10-16): the legacy `.agent-presets` copy, the
117
+ `presets/chapters/` tree and `preset-install.test.ts` are DELETED, and a pre-0.2.0 host logs
118
+ `this host cannot register preset rows` while its tools keep working. The tape acceptance chain needs
119
+ `dev/dsh-host-lock` moved to 0.2.x plus a re-distill. **#54 did the move: the lock now
120
+ resolves 0.2.0-rc.2 — chosen by WITNESS (the host this box runs the plugin on), not by npm's
121
+ latest — the refusal has lifted itself, and `hostSupportsPresetRows()` is what lifts it (a
122
+ version test in `tests/e2e/boot.ts`, because it runs before any host exists; silently skipping
123
+ the pins would make every segment pass a run that never exercised the preset).** Three 0.2.x
124
+ drifts were found by booting rather than guessing, all rig-side (and a fourth was NOT the host:
125
+ the acceptance chain's remaining failure is a **stale tape corpus**, now gated by
126
+ `tests/fixtures/model-tape/CORPUS_HOST` — `scripts/corpus-anchor.mjs` refuses replay by name and CI
127
+ skips the acceptance job with a tracked reason, issues #56/#57; a gate that blocks `record` too
128
+ would strand itself, so record stays allowed), all the kind that yields a
129
+ WRONG conclusion rather than an error: the durable log is `session.v4.jsonl.zstd` (five hardcoded
130
+ `v3` paths made every log assertion decode nothing and report "the archive never landed" —
131
+ `sessionLogFileFor()` resolves the highest now, pinned by a row); the host opens a **Preview
132
+ Notice**, a focus trap like the 2026-09-18 `Internal Testing Notice`, and dismissal must target
133
+ `[role=dialog]`-shaped elements (matching "any div mentioning a notice" matched the app wrapper
134
+ and pressed an unrelated 'Close'); and **provider config rides `cordis.patch.yml`** — a
135
+ profile-dir `settings.yaml` is read by nothing on 0.2.x, so without an `llm-pi-ai` block the
136
+ host has no `local` provider and the browser opens on "Add an API key to get started" with the
137
+ composer never reachable. Distillation no longer needs the GPU box: `E2E_UPSTREAM[_KEY|_MODEL]`
138
+ makes the tape proxy translate on the way out while the fixture keeps its pinned model id and a
139
+ key-free profile (the tape RECORDS the profile's name — a fixture of this rig, not of the
140
+ service that distilled it). Remaining: re-distill all eight legacy projects (the ninth, `prompt`, was distilled ON 0.2.0-rc.2 in #71 and needs no re-record), then a strict replay and a
141
+ fresh `coverage:e2e` ledger — until then no browser-plane claim is verified.** Health is still the host ROSTER's answer, asked AFTER activation, plus
142
+ `compositionInventory()` rows — **file presence is not preset existence, and "declared" is not "usable"**.
143
+ A session's preset id is durable state (`agent-preset/selected`), so an unregistered id makes every session
144
+ that chose it fail to *open* (`agent-preset/not-found`) while its log sits there intact — and meanwhile the
145
+ plugin boots, registers its tools, and Settings says "enabled and running". Facts with file:line:
146
+ `docs/contract.md` § Presets are declaration rows; the gate that turns the next host rename into a red row
147
+ instead of a broken user session is `tests/unit/preset-declaration.test.ts` (15 rows, each verified to fail
148
+ on the mutation it guards: ralph re-disabled, an unknown pruner key, `toolFilter` restored, `{{cwd}}`
149
+ restored, the preset id renamed).
150
+ Rounds 23-28 then: E3 measured (per-machine — see the Cache section, never cross the numbers),
151
+ `chapters_fork` (r24), the durable title (r25), the 20-check mechanical pass (r26/26b: 18/20 proven
152
+ on-scratch, the rest human-only), the target-hardware verdict (r28: zero-token compaction confirmed at
153
+ llama.cpp counters, ~2 s steady-state turns), and a full-install product bug found-and-fixed by the
154
+ same run — one domain, one opener, `acquireChapterStore` (e0e67d0).
155
+ **MVP state: engine + tools + preset verified ON THE REAL MODEL, and the message-level fork AND
156
+ continue BUTTONS are shipped and e2e-green** (both rows come from one table + one runner in
157
+ `src/client/doors.ts` — adding a third door must extend that table, never copy the body; a test refuses a
158
+ second runner) (`conversation.chat.assistant-actions` → `remote.commands` → watermark
159
+ fork → `sessions.open` after `refresh`; `tests/{unit,integration,e2e}`, Playwright with
160
+ workspace-cached chromium — client-plane facts are now measured, never guessed).
161
+ **Long-context mechanics (architecture amendments, 2026-09-19) are BUILT**: arrival-time
162
+ artifacting — tool results ≥ `toolResultArtifactTokens` never enter the surface (stub +
163
+ content-addressed artifact, queried via `chapters_artifact` toc/search/read; measured: 373KB
164
+ book, zero blob prefills, cacheRead monotone across the stub) — and plot carriage (persona
165
+ `PLOT:` discipline carried through in-place checkpoints; bounded elicited fallback behind
166
+ `elicitedPlot`). e2e is now TWELVE specs in NINE replay projects, each with its own boot (#71 adds the prompt-ghost journey, `prompt` @ 0.99 + NO_AUX_MODEL + E2E_PROMPT=1, scripted-distilled like plans — suite = knowledge-loop +
167
+ discovery @ 0.75 of the pinned 32K/15K stress regime; heavy @ 0.5 with the arrival floor OFF —
168
+ measured: a floor below the chunk size arrival-stubs the surface and silently prevents
169
+ compaction from ever being needed; arrival @ production 0.9 + floor 500; fanout @ 0.75 — the
170
+ chains its child sessions write are the deepest on this hardware and must be distilled in
171
+ isolation, and its child prompt caps readers at SIX 300-line reads (an uncapped qwen looped
172
+ past 30 exchanges; the cap still crosses the threshold inside every child); enrich @ 0.9 with
173
+ enrichmentEnabled; rules @ 0.9; fork @ suite pins — carved out of the shared suite boot 2026-09-25
174
+ when knowledge's fork child became the SPA's restore target for later specs (the fanout lesson
175
+ generalized: one journey, one boot); explore.spec is an on-demand selector-archaeology DEV
176
+ project, never in the CI chain) against port-keyed throwaway homes `var/e2e-home-<port>` — the dev
177
+ home is SHARED with live agents and must never be the test ledger (FINDINGS 2026-09-19: run 13
178
+ asserted against an agent transcript; draft identity, YAML colon-in-plain-scalar, worker-bound
179
+ setupFiles, and same-home sibling boots all bit; identity is now verified via localStorage + the
180
+ log's own header event). ACCEPTANCE RUNS OFF THE MODEL TAPE by default:
181
+ `npm run test:e2e:replay` serves every model turn from `tests/fixtures/model-tape/<project>` (proxy
182
+ `tests/e2e/model-proxy.ts`; explore 14.7 s replayed vs 1.7 min live; recorded usage keeps the
183
+ token meter — and thus threshold behavior — bit-identical); `npm run test:e2e:record` distills
184
+ tapes from the real model (once per scenario; re-record when PRODUCT prompts change —
185
+ persona, tool descriptions, thresholds — but NOT the notice: measured 2026-10-11 that no committed tape prefix contains a seeded notice, so notice prose is currently free; that is coverage luck (no journey makes a continuation child speak), not a guarantee. Workspace docs (AGENTS.md itself,
186
+ skill catalogs) are masked out of matching by the AGENTS-BLIND rule (user decision
187
+ 2026-09-20: development artifacts, every real project ships its own — doc edits are
188
+ FREE, no re-record; reaffirmed 2026-10-01 with no exceptions: the tapes stand in for
189
+ ANOTHER project, not development of this one, so no doc edit — near-threshold
190
+ compaction summaries included — ever couples a tape to this repo's docs).
191
+ `rm -rf tests/fixtures/model-tape` for a clean corpus. The normalization
192
+ pipeline is CLOSED — escape/whitespace classes, volatile rules, tool-result content and
193
+ injected instruction blocks matched out; the AGENTS-blind addition was the last change
194
+ and required one final re-record; 2026-09-23 added two DERIVED-COUNT collapses to the
195
+ same family — `(N est tokens)` in chapter index lines and the char count inside
196
+ `⟦omitted:host-injected …⟧` markers (both move with doc/render drift, both identified
197
+ already by their line — pinning them made heavy/enrich tapes brittle across builds). Replay chain verified green — ALL SEVEN segments strict 2026-09-25 and — for the first time — GREEN ON HOSTED CI (GitHub Actions, no model, no GPU): the tape is machine-neutral by contract now — injections mask to fixed tokens everywhere (skill catalogs incl. the 'catalog changed' notice), marker RUNS collapse (a dev box injects more spans than a runner), digest message-tallies collapse to <MSGCOUNT> (they count injections), the enricher's input defuses markers before its cap (hosted divergence at char 758), WS-PATH/DSH-CHECKOUT blind the system prompt's identity lines, the sessions-dir slug is COMPUTED from ROOT (a hardcoded /home/adrian one made CI poll an invisible log for 7 minutes per spec), zstd is a runner dep, the host comes from dev/dsh-host-lock's hermetic lockfile (npm global mode rewrites the plugin tree; rc-ranges float), workspaces boot PRISTINE (sessionIds cleared — restore races ended), and an unborn-mirror push is an honest no-op, not local-only (pristine-store discovery: the old knowledge assertion passed only on accumulated dev state). All 464 deterministic + 85/85/85 gate green on the hosted runner too. The `rules` browser scenario was UN-PARKED — the r39 diagnosis: the spec read zstd-compressed session directories as flat files and hardcoded a rule id; the product was always right. The enrich e2e earned its keep: it exposed two real product bugs invisible to every deterministic layer (verify.md T9/T10 — the body-hash anchor convention mismatch that silently blocked enrichment since P2, and the missing host-plane route) (r38 lesson: the DRAFT screen turns composer text into a first MESSAGE, not a command — establish a session before exercising command surfaces; and RR2=137: a chatty live loop that records mid-test can be OOM-killed — keep e2e prompts tool-free in WORDS the model obeys; and 2026-09-23: a tape records the CONTEXT it was distilled in — re-record whole projects, never one spec of a shared boot, and never run two GPU suites at once). 608 unit/integration, zero skips when the TreeDX container is up (live-gate fails loudly when configured-but-unreachable; skips only with no configuration at all), under a GATED coverage ledger: `npm run coverage:ci` (c8 --check-coverage) enforces ≥85% statements/branches/functions/lines in CI — current 97.94 / 86.47 / 93.82; `npm run coverage:e2e` remains the browser-plane ledger (eight replays since the plans journey joined, boot-level V8 profiles remapped through REAL source maps; current 79.92/64.64/76.28 — reported, not tuned; first ledger 80/62 before the audit). The 2026-09-23 coverage audit closed its own gap list the same way: providerKindFor policy rows, credential migration, the git-command surface incl. every refusal text (T13), the TreeDX client's failure vocabulary (timeout/bad_json/envelope), git-plane auth (T11), the push retry/divergence paths, scheduler pull rows, listing pagination over a paginating stub (T12), overlay-delete, rule-number walk-past by prefix (id-uniqueness), the composer busy-retry budget, and the coverage instrumentation itself; then the deterministic plane was swept to the gate — engine-lifecycle (real cordis Context + real base engine: engine.ts 40→91 stmts), the tool wrappers' full success/refusal surface, the TreeDX error taxonomy over stub faults + parser-shape drift via injected fetchImpl, the local-path git transport, host-command error arms, and the pure planners (vocabulary/enrich/indexing/search/arrival/archive) — catching one REAL bug (canonicalizeRemote carried embedded credentials into the project key: same repo with/without creds forked the pool; fixed + pinned). CI/CD shipped 2026-09-24 and went fully green 2026-09-25: .github/workflows/ci.yml (deterministic gate + seven-segment tape-replay acceptance with NO LLM/GPU/key — tape misses fail loud, hosted runs ship diff-dumps sorted by shared prefix) and release.yml (tag = package version → suite-gated npm publish --provenance; NPM_TOKEN strategy chosen, wired for the treeseed-ai org move); the pristine-runner dress rehearsal caught the scaffold-clobbering patch guard, the credential-less-provider death, and the unregistered-workspace home — plus the profile is now built with plain npm (dsh plugin add was a pnpm forwarder — nothing in this repo touches pnpm, and it is not and never will be a monorepo; the host lock is a standalone manifest). docs/development.md § Hosted CI.
198
+ Git history was rewritten 2026-09-20 (test-results/.dsh purged, .git 7M→840K) and force-pushed —
199
+ clone fresh or `git reset --hard origin/main` after pulling.
200
+ **Knowledge layer P1 (record §13) is complete and live-proven**: signatures collect at real
201
+ `turn/end`s (r35j), the fork composes topics live (r36 — same-file turns merge, topic changes
202
+ split), the index + `chapters_search` + redaction + `/chapters-link`/`/chapters-status` ride a
203
+ sync loop that degrades to local-only honestly and rebuilds the mirror on divergence, with the
204
+ two-machine exit criterion automated over a real git smart-HTTP remote (integration + the
205
+ `knowledge.spec.ts` browser journey). **P2 is BUILT (2026-09-20)**: the idle/after-push
206
+ enrichment ladder with validation-before-commit, provenance chains (`generated:` newest-first),
207
+ the body-hash guard as the only sanctioned chapter-file mutation, `/chapters-enrich
208
+ run|model|report`; shadow-by-default emergent vocabulary writing git-visible `topic-alias`
209
+ curation + derived `topics/vocabulary.json`; and index-level fragment stitching (adjacent
210
+ legacy fragments cohere into ONE search entry citing all members — nothing merged on disk). Two-machine exit criterion proven — catching r37 on the way: stageAllAndCommit compared the workdir against itself, making modifications to tracked files permanently invisible to commits (additive P1 hid it; enrichment would never have traveled). **P3 is BUILT (2026-09-20, §15 amendment: approval = per-machine curation fact, never a file rewrite)**: write-once proposed rule files, /chapters-rule add|list|approve|revoke, chapters_rule_propose tool, verbatim core block in the continuation notice (byte-identical ABSENT, golden-tested), refuse-with-numbers overflow budget, category-aware search with a per-machine core bonus, and the rules lifecycle proven transport-grade over a real pool with three machines. **P3c — two-tier rules is BUILT, both capture speeds (#50; record §7.1 + §7.3–§7.9, amendments 2026-10-20 and 2026-10-21). Two things remain unverified, for different reasons: the **browser plane** (the host pin moved to 0.2.0-rc.2 in #54 and the boot's named refusal lifted, but the corpus still has to be re-distilled on the new host before ANY browser-plane claim is real — this feature added a tool AND changed the preset persona, both prompt bytes), and **mining quality beyond n=3** — `npm run mining:probe` did run against the real session model (`qwen3.8-flash`, regional token-plan door; the GPU box was unnecessary) on three of this repo's own chapters: 9 rules, 6 stated / 3 inferred, all inferences confined to the chapter with no human text. See `docs/verify.md § Two-Tier Rules` R5 and `docs/backlog.md` 1b/1c.** The hole was concrete, not vague: `planSummarize` composes `leadSections = [planSection, plotSection]`, so a plan survives an in-place compaction and a plot survives it, and a **rule does not**. Session rules (`scope: session`, local, never staged — one filter in `planStoreToRepo`) captured by a deterministic `ADD RULE:` parse at the existing `turn/end` listener (zero model tokens, invariant 1) plus enrichment capture (`rulesEnrichment`, ON) that MINES standing orders the human never said and lets them govern on arrival — `rulesAutoActivate: true` is the default, and the earlier design that required a verbatim human quote before a mined rule could apply was REJECTED by the maintainer and removed (record Amendment 2026-10-21): demanding proof that a human already said it makes the tier redundant with the deterministic marker, and the premise of the tier is that the human may not know. Visibility replaced permission: each mined rule renders with its warrant (`stated by you, mined from ch.N` / `inferred from ch.N — nobody said it`), the agent must NAME the session rules that shaped its reply, at most 3 per chapter plus per-tier display caps, and `reviewOnly` + `approve` is the opt-in review-before-trust posture; `rulesModel` gives mining a model of its own (one extra bounded call, and its answer REPLACES the annotation pass's rules). One auxiliary completion budget, `enrichmentMaxTokens` (default 4000), is the single source for that ceiling on both planes — the route measured here does not enforce `max_tokens` and counts reasoning tokens toward completion usage, so a small number is not safety, it is truncation on a stricter provider. What did not loosen: a session rule never overrides a project rule, and never becomes one without a human. Activation itself is unchanged: **DERIVED** from the ancestor path, never a `curation.jsonl` fact — the 2026-09-20 per-machine approval boundary is NOT relaxed, it still governs the entire shared tier. Rules are carried at the two moments a head is rebuilt — the continuation notice AND the in-place compaction checkpoint (`rulesCarry` is the third lead section in `planSummarize`, and `session-rules.renderRulesBlock` is the ONE builder both paths plus `chapters_rules` call: a source-shape row fails if either door starts picking rules itself) — **never injected per request**: invariant 3, and measured llama.cpp `cacheReuse 0` on a head-position replacement means a churning ruleset is a cache-wipe machine. `chapters_continue` carries both tiers, `chapters_fork` carries project only — the SAME `intent` axis plans already use (`docs/plans-carriage.md` §§ 9–13), not a new distinction. **#52 (same round, second half): rule ancestry ALSO crosses the delegation edge** —
211
+ `session-rules.ruleAncestorSet` walks registry `parentSession` ∪ the kernel header's
212
+ `parentSession`, transitively, READ-ONLY, upward only (a subagent inherits its delegator's orders; a
213
+ child's inferences never govern the parent), bounded at 64 expansions and LOUD when the cap bites;
214
+ every door, the checkpoint and `/chapters-rule active` call that ONE walk, and an anti-drift row
215
+ fails if a plane computes ancestry by hand again — hand-built ancestry is how delegated subagents
216
+ ended up with zero rules. And the `chapters` preset's **persona suffix** is the per-composition
217
+ channel that tells a session — fresh, continued, or delegated — to run `chapters_rules` before a
218
+ task and `chapters_search` for history: that is allowed because it is preset-scoped, not a
219
+ system-prompt contribution (invariant 3), and two couplings are pinned by behaviour: the suffix must
220
+ stay matched by the plot template-echo guard, and it may not introduce a `{{variable}}` the host has
221
+ not registered. Changing that suffix is a PRODUCT PROMPT change — the tape re-distill debt GROWS,
222
+ it does not shrink. Selection is pinned → topicality → index-only, via `rules.selectRules` — the head's selector, deliberately NOT `search.rankEntries`, which ranks the corpus (one implementation of each job, and bending a rebuilt head to read index shards was rejected); per-tier count caps config-driven, and the **withheld count is mandatory** — silent selection would rebuild "lost in the noise" inside the fix for it; the token ceiling still refuses with numbers, never clips. **No global tier was built, because none was needed**: `projectKey` is UUIDv5 over the canonicalized *remote* and `harnessId` defaults to hostname, so checkouts of one repository already share one rule namespace; what that round actually owes is loudness for the four ways the identity silently diverges (record §7.8) — a `path-derived` key, a fork listed first, TreeDX keying on the pool location, and an unlinked/un-synced clone, each of which must SAY it rendered zero rules: `/chapters-status` now prints an `Identity:` line and warns when the derived key differs from the bound one (the repo's own non-git test fixture trips it, so the warning is not theoretical).
223
+ One design lesson from that reversal, because it is the recurring mistake in this repo: a safety mechanism must block the harm, not the value. The quote gate blocked invented rules by also blocking every inferred one; the mechanisms that replaced it (a rendered warrant, the agent naming its rules, caps, one-command revoke) hold the same risk without amputating the feature. Suspicion of model authorship is a reason to make output visible and reversible, not a reason to demand human provenance.
224
+ Two local build facts, both learned the hard way: the deterministic suites need **Node ≥ 24** (`engines`), and on this box's Node 22 every test file fails in a way that looks like your diff; and until 2026-10-21 `npm run coverage:ci` intermittently reddened `git-auth-classification`/`sync-remote` with an EPIPE blamed on their `before` hook — **diagnosed as pre-existing, which was true and not the point**: the test git server wrote request bodies into a CGI child's `stdin` with no error listener, so a child that had already exited raised an async EPIPE nobody handled. Streams are guarded now (three consecutive combined runs: 800 pass / 0 fail). Recording the sequence because the mistake is the reusable part: proving a failure predates your diff is a step, not an answer.
225
+ **Knowledge transport is now a provider seam (2026-09-21, record §15: "transport is pluggable; git is one provider" + "host-injected context is not conversation")**: `src/provider.ts`'s six verbs carry every sync path; `kind:'git'` (default, every pre-existing record — zero behavior change) or `kind:'treedx'` (`/chapters-link treedx+<url>/<repo> <token>`, `src/treedx/`: workspace-create → overlay-write → commit transport, `.treedx-state.json` mirror memory). Search/rules/index/notice layers read the materialized mirror unchanged — §3.1 survives both backends; divergence routes through rebuild-from-the-store in both; TreeDX lease contention maps to the existing rejected→pull-retry, moved head to diverged. Contract + truth table in `docs/provider.md`; TreeDX API facts (doc-reads vs live-measured, with corrections) in `spikes/treedx/FINDINGS.md`; dev loop `scripts/treedx-local.sh` + `dev/treedx.compose.yaml` (the cloned repo's own compose is platform-bound and will not boot standalone — measured). Transport-grade proof: 24 tests over an in-process stub (`treedx-{provider,sync,link}.test.ts`); the real-container exit criterion (`treedx-live.test.ts`) ran and PASSED 2026-09-21 (5/5 against a booted dev-auth service; full suite 286/286 zero skips — the live run caught three parser shapes, the born-with-`.treedxkeep` phantom-deletion trap, the lease-survives-TTL close requirement, and a swallowed auth detail, all fixed and pinned). **Re-verification 2026-09-23 caught one more, in the wild:** `paths/list` PAGINATES (limit 100, base64 offset cursor in `page.nextCursor`) — reading one page silently truncated the corpus once the shared dev repo grew past 100 paths (machine B reported 'up to date' while missing real files). `corpusPaths` now follows the cursor to the end; the paginating stub + a 150-file row pin it (T12), and the live container proves it at the real service. **The same round landed the injection screen** (`src/injections.ts`): host-injected context (instruction files, runtime snapshots, skill catalogs, our own TOC notice — event-level and `<system-reminder>` spans inside human messages) is marked-and-counted, never archived; the conversation-bytes guarantee and TOC `(N msgs)` now say what is actually true, and the knowledge corpus carries no project's instruction bytes (H1 rows in `docs/verify.md`).
226
+ **Session-plan carriage is BUILT (2026-10-09, `docs/plans-carriage.md`)**: `src/plans.ts` pairs an
227
+ `exit_plan_mode` call with the `tool/result` that carries the human verdict (approved / rejected /
228
+ dismissed / pending / superseded / failed — the fifth class is the review never opening, which is not a
229
+ verdict); chapters carry a `## Plans in this chapter` block AFTER the verbatim transcript with the plan's
230
+ outline, bounded head and content-addressed full text; `plan/mode` folds last-value-wins into `planMode:`
231
+ instead of falling into `unrenderedSeqs`; the child's notice carries the operative plan (intent-aware
232
+ since the second round — see below; a pending one is marked unapproved); plans are forced chapter boundaries in
233
+ both segmenters and title the chapter they conclude. Two truths worth keeping: the token meter is NEVER
234
+ exempted (the block is counted by `preflight`, and a plan-less path renders zero bytes — which is why all
235
+ seven tapes still replay WITHOUT re-recording, verified 2026-10-09), and a plan whose review is still open
236
+ sits in an unfinished turn above the archive ceiling, so it is carried as live and un-stored. Fixed on the
237
+ way: chapter `artifacts/…` citations pointed at paths `chapters_artifact` could not resolve at all
238
+ (`writeArchive` writes inside the session tree; the resolver only looked at the store root) — true for
239
+ every deferred blob since arrival-stubbing shipped, now pinned (verify.md P10). The browser plane is IN the chain (project `plans`, verified 2026-10-10); its tape is distilled from
240
+ the scripted upstream `tests/e2e/stub-model.ts` because this box's model server drops the stream
241
+ mid-turn — read `tests/fixtures/model-tape/plans/PROVENANCE.md` before quoting what it proves (the
242
+ carriage yes, a model's choice to plan no). **Second round, same day, four decisions landed**
243
+ (`docs/plans-carriage.md` §§ 9–13, verify.md P12–P17):
244
+ - **Intent decides what a child is told.** A CONTINUATION (`chapters_continue`) extends the same stream
245
+ of work, so it gets `## Active plan` + `## Previous plans on this path` and, while the parent was still
246
+ planning, a seeded `plan/mode`. A BRANCH (`chapters_fork`, the fork button) splits a new direction, so it
247
+ gets **zero** plan bytes — its plans are history, in the cited chapters and as documents.
248
+ - **A plan is a document of a third kind**: `kind: plan`, written in `chapters/` as `plan-NNN-<slug>.md`
249
+ with its own registry counter (`nextPlanNumber`, allocated through the registry — never a directory
250
+ scan), so it never consumes a chapter number, is carried by the pool transport and the mirror rehydrate
251
+ for free, and is findable with `chapters_search` **as a plan**. Superseding writes a new document; the
252
+ old one keeps its bytes.
253
+ - **The enrichment ladder never touches a plan document.** Directives are not narrative; paraphrasing an
254
+ order is the drift this store exists to prevent. The guard reads the file's own frontmatter.
255
+ - **The operative plan is carried into every in-place checkpoint** (the plot mechanism, but chosen
256
+ session-wide and newest-first), bounded, with truncation announced.
257
+ - **Findability round (0.1.12, `docs/plans-carriage.md` § 14, knowledge-repo Amendment 2026-10-11): two
258
+ doors, ONE scorer (`search.rankEntries`) — the pool mirror when pulled, the workspace store otherwise
259
+ (and as a fallback when the pool has no match). A bare workspace used to answer `chapters_search` with
260
+ "link a remote" while its own chapters, `kind: plan` documents and rules sat two directories away; that
261
+ was the flaw the audit found. The answer must NAME ITS DOOR ("searched this machine's store, N docs …")
262
+ so "this machine only" is never read as "the whole project", and "nothing matched" ≠ "nothing archived
263
+ here" are reported apart (`documents`/`empty`). Artifacts are never indexed (they answer to
264
+ `chapters_artifact`; a second index is forbidden). The notice gains `## Finding things` naming
265
+ read/chapters_artifact/chapters_search (`retrievalHints`, default on) because an unmentioned tool does
266
+ not exist to a small model. Two pinned rows asserted the old contract and failed loudly — that is why
267
+ this is a fact rather than a guess; amended with the reason in the diff.
268
+ - **Kernel-drift gate (`tests/unit/host-seed-contract.test.ts`)**: the seeded `plan/mode` depends on the
269
+ host knowing the type and treating it as NOT surface-eligible. Read the kernel's OWN exported predicates
270
+ (`KNOWN_SESSION_EVENT_TYPES`, `isSurfaceEligibleType`, `isAppendSurfaceEvent`), never reimplement them —
271
+ a kernel change that silently ungates a child must show up as a red row, not as a passing suite.
272
+ - **Two deterministic doors, one builder (`/chapters-fork`, `/chapters-continue`)** — the load-bearing
273
+ lesson of attempt 5: **a tape cannot carry a model-authored range.** The recorded `chapters_continue`
274
+ said `endSeq: 30`, the runner's ceiling was 29, and `validateRanges` refused it — correctly. And a small
275
+ local model will not reliably chain `chapters_segment` → `chapters_continue` at all. So anything that must
276
+ be accepted repeatably goes through the plugin's OWN derivation (`deriveRanges`/`composeChapters`, zero
277
+ model involvement); the tool remains for the model's use but is never the only proof path. Both doors come
278
+ from `deterministicCommand(door)` and a row pins that they **archive identically** (same ranges, titles,
279
+ plan facts) — two implementations would drift. `intent` is the ONLY semantic difference: continue ⇒
280
+ `## Active plan` (+ seeded `plan/mode` while still planning); branch ⇒ zero directive bytes.
281
+ Also measured there: **the turn containing a plan submission does not end until the review is answered**,
282
+ so a session whose only turn is pending has no archive ceiling and the door refuses with its first guard.
283
+ - **The one exception to the injection screen**: the harness's `plan-mode` + `form:'notice'`
284
+ mode-flip sentence archives verbatim (named by producer AND form, so it cannot widen — v3 wrapper
285
+ `{kind:'plugin', plugin:'plan-mode'}` and v4 `{kind:'plan-mode'}` both match, issue #43).
286
+ `isHumanAuthored` stays strict on purpose — it drives topic signatures, and the r28 lesson was that
287
+ injected text moves chapter boundaries.
288
+ **Outstanding: the human verification rows (live `/compact` in a real browser, readability
289
+ judgment), the seeded-gate case OBSERVED in a browser (pinned deterministically; needs a second distilled session — `docs/plans-carriage.md` § 15), per-message anchoring (the button forks at the conversation's end today; `deriveRanges`
290
+ already supports any anchor), and the default-preset shipping policy (opt-in menu vs profile
291
+ default), which is the user's call, not ours.**
292
+
293
+ The fiber-eviction trap `dsh-session-fork/src/branch.ts:10-14` warns about did **not** bite — an unattached
294
+ session was the whole explanation for round 6's "not listed", fixed by
295
+ `workspaceRegistry.createCanonical(cwd)` → `workspace.attachSession(id)`.
296
+
297
+ **Cache behaviour measured (rounds 10-11, 13, 16, real provider).** A continuation builds its own cache —
298
+ 0% hit cold, 74% warm — and the parent keeps hitting across the boundary, so **G3 and G2 are green**. G1's
299
+ head is now *measured* for the composed shape: r16's `setup`-mounted child's first request was **13,315
300
+ tokens (27-tool header, this roster)** — quote the older "≈ 8K" only as a projection. Steady state it
301
+ protects: 396,800 cached + 822 uncached (**99.79%**) live session. E3 measured — and it is
302
+ per-machine (r23/r28): on the cloud provider a replacement keeps ~7.4K of prefix cached (refill
303
+ halves, 13,658 → ~7,000); on llama.cpp (the target) the slot reuses NOTHING across a head-position
304
+ replacement (cacheReuse 0) — the local win is zero-token compaction (r28: 0 prompt tokens vs ~11 min
305
+ for an LLM summary on that box) plus ~2 s steady-state turns. **Never quote one machine's cache
306
+ number for the other.**
307
+
308
+ Metric formula, since getting it wrong produces 1721% hit rates: `totalPrompt = inputTokens +
309
+ cacheReadTokens`; `hit = cacheReadTokens / totalPrompt`. Judge "cache undisturbed" by
310
+ `cacheReadTokens` not collapsing, **not** by hit percentage, which falls innocently as the prompt grows
311
+ past a cache-block boundary.
312
+
313
+ Phase 1 follow-ups CLOSED: the title call is the *service* (`sessionController.rename`, live-handle
314
+ window — r25 durable), and the preset/composition note in invariant 2 ships in every child.
315
+
316
+ Coexistence with `dsh-session-fork` is currently **untestable, not disputed**: the shipped example fails
317
+ to boot the installed host ALONE (its rc.2-era code vs rc.1's `webServer` inject requirement — verified
318
+ by pair-boot then solo-boot, r26). Name-spaces are disjoint by inspection; re-run the pair-boot on a
319
+ version-matched host before claiming either way. The dev/test loop that avoids restarting the harness
320
+ being edited from is solved — see [docs/development.md](docs/development.md).
321
+
322
+ Then [docs/contract.md](docs/contract.md) for how to declare things, and
323
+ [docs/verify.md](docs/verify.md) before claiming any step passes.