@treeseed/dsh-chapters 0.1.24 → 0.2.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.
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 +217 -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 +162 -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 +47 -0
  33. package/lib/diag.js +141 -0
  34. package/lib/diag.js.map +1 -0
  35. package/lib/engine-core.d.ts +51 -1
  36. package/lib/engine-core.js +63 -7
  37. package/lib/engine-core.js.map +1 -1
  38. package/lib/engine.d.ts +93 -0
  39. package/lib/engine.js +477 -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 +51 -0
  50. package/lib/index.js +180 -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 +311 -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.
package/docs/verify.md CHANGED
@@ -165,6 +165,88 @@ renderer, the segmentation tool, and the child's notice.
165
165
  | P15 | Plan mode survives a continuation only when it should | seeded `plan/mode` ONLY for `intent: 'continue'` while the fold says active (measured: kernel accepts the two-event seed, host projection folds it to `active:true`, one-event control does not); never for a branch, never after an approval — because approving a plan EXITS plan mode | unit `continue-plans.test.ts` (four rows: seed shape, approved-no-seed, branch-no-seed, `planSeedMode:false`) against the installed-kernel probe recorded in docs/plans-carriage.md § 10 |
166
166
  | P16 | The notice reads the intent, not just the log | continuation → `## Active plan` + `## Previous plans on this path` (marked history); branch → zero bytes, plans live in the cited chapters and as documents; the mode statement is either "the directive above is live" or "this child starts OUTSIDE plan mode", never both | unit `plans.test.ts` (seeded/unseeded/branch rows) |
167
167
  | P17 | The one injection-screen exception is named, not open-ended | `kind:'plugin' + plugin:'plan-mode' + form:'notice'` archives verbatim; any other plugin notice, snapshot or instruction shape still screens; `isHumanAuthored` (which drives topic signatures and `(N msgs)`) is unchanged | unit `injections.test.ts` (two rows, incl. wrong-form and wrong-plugin negatives) |
168
+ ## Repair (#60) — recovering an archive from the durable log
169
+
170
+ | # | Claim | Procedure | Status 2026-10-22 |
171
+ |---|---|---|---|
172
+ | R1 | The auditor finds the measured damage | `node scripts/chapters-repair.mjs --home <lab> --project <cwd> --cwd-override <lab-tree>` on a COPY of SAIP's live registry + store + 26 session logs | **PASS** — 436 findings: hollow 5, empty-artifact 335, never-archived-span 19, unregistered-file 73, ghost manifest 2, stuck reservation 2. Matches the manual audit exactly. |
173
+ | R2 | A repair re-derives lost knowledge | apply, then grep for three phrases that previously matched 0 files | **PASS** — span 5265-5467 (the "foundational to the project" hosting brief) is now in `chapters/030-hosting-…-plan-1.md`; 719 moves across 3 passes |
174
+ | R3 | Repair converges (idempotent) | run apply again; the third pass must move 0 | **PASS** — pass 3 = 0 moves; audit falls 436 → 3 findings |
175
+ | R4 | What it will NOT do | 3 files remain: two `015-*.md` claiming one number. Re-registering needs a new number = renumbering a chapter → refused, with the reason printed | **PASS** — residue is a human decision, reported as such, not silently "fixed" |
176
+ | R5 | Nothing is deleted or rewritten | after a full apply, the husk files still exist (copied to `.repair-quarantine/`), the registry keeps every record (append-only), and the log's md5 is unchanged | **PASS** |
177
+ | R6 | The run is reversible | `restoreRepair` walks the journal backwards; new files return from quarantine | **PASS** — `tests/integration/repair-apply.test.ts` |
178
+ | R7 | Dry-run is genuinely read-only | registry bytes and directory listing identical before/after | **PASS** — pinned by a row |
179
+ | R8 | LIVE application | `~/.dsh` is read-only to the agent sandbox (EROFS), so SAIP and this repo have NOT been repaired for real | **BLOCKED on the user**: run `node scripts/chapters-repair.mjs --project /home/adrian/Projects/saip --apply all --yes` yourself, or approve the sandbox |
180
+
181
+ Known limitation, stated rather than hidden: an audit through `ctx.fs` cannot list a directory, so it
182
+ reports `blindSpots: ['orphan-file','test-litter']`; the offline script has `readdir` and sees them.
183
+
184
+ ## Two-Tier Rules — SPEC (record §7.1, §7.3–§7.9; issue #50)
185
+
186
+ Rows **R1–R10 verified 2026-10-20** except **R-e2e**, which this host cannot run (see the last row
187
+ and §13 P3c). Each row below was written docs-first and then proven to fail on the mutation it
188
+ guards — the mutation runs are named per row so a future reader can repeat them. The P3 rows above
189
+ stay the shipped-behavior record; these extend it with the session tier.
190
+
191
+ | # | Claim | Status | How it is witnessed (and the mutation that proves it) |
192
+ |---|---|---|---|
193
+ | R0 | The empty tier still renders **zero bytes**, and the pre-#50 option shape renders **byte-identically** to the previous builder | ✅ verified | The golden literal was captured by running **HEAD's** `buildRulesSection` over the same fixture and diffing it against the new one (text-equal, kind-equal, only the non-rendered `withheld` field added). On the checkpoint path an empty set asserts neither header nor announcement appears |
194
+ | R1 | A rule survives an in-place compaction | ✅ verified | `rules-checkpoint.test.ts` drives the **REAL realm engine** through `summarize()` and reads the text that would be prefilled next: project rule + session rule verbatim, project before session (positional precedence); a parent's rule rides the child's checkpoint (R1b — the tier binds the TREE); over budget the omission is announced. Mutation: dropping `rulesSection` from `leadSections` breaks 3 of 4 rows |
195
+ | R2 | `chapters_continue` carries both tiers; `chapters_fork` carries project and **zero session bytes** | ✅ verified | `rules-doors.test.ts` asserts the asymmetry through the shared renderer, and a **source-shape row** fails if `tools.ts` or `engine.ts` calls `buildRulesSection` directly — two rule-pickers would drift apart invisibly (the same argument that pins `rankEntries` and `doors.ts`) |
196
+ | R3 | `ADD RULE:` is deterministic and free | ✅ verified | `session-rules.test.ts` (8 rows): line-start-only, minimum length, dedupe; the two injection screens (`isHumanAuthored`, `stripReminderSpans`) each with a positive and a negative; assistant-restatement never captures (the amplification path); idempotent across repeated turn/ends; numbers honor registry **and** disk; §9 credential screen; listener containment (disabled / missing cwd / throwing store). The module holds no `ctx.llm` reference at all |
197
+ | R4 | Session rules never reach the shared pool, on **either** provider | ✅ verified | `rules-transport.test.ts`: a real git pool (A stages project only, B pulls and never sees the session file, A still renders it locally — exclusion ≠ invisibility) and the TreeDX stub's commit tree. Mutation: deleting the single `isSessionRuleFile` skip reddens both |
198
+ | R5 | Mined rules are governed by **policy**, not by proof that a human said them, and each one carries its **warrant** (§7.5, Amendment 2026-10-21) | ✅ verified | `rules-mining.test.ts` (20 rows): `rules` is OPTIONAL so the old three-field answer still validates; bounds (≤3 **written** per chapter, sub-minimum dropped, a garbage `rules` value cannot cost the annotation); the quote check is a LABEL — `stated` when the chapter's human bytes contain it, `inferred` when they don't, and BOTH govern under the default `rulesAutoActivate: true`; the row named "THE REVERSAL, PINNED" asserts exactly that, and the rendered head shows `inferred from ch.N — nobody said it` beside the text with the agent required to name the rules it applied; `autoActivate: false` writes `reviewOnly: true` and renders NOTHING until `approve` (a curation fact — the write-once file is byte-compared unchanged); assistant prose is inadmissible even as a label; ladder rows prove human-bytes-only evidence, mining before the hash-guarded write, `rulesEnrichment: false` logging the drop, an unwired plane WARNING, a throwing sink not losing the annotation, and `rulesModel` making a second call on its own route whose answer replaces the annotation pass's; `/chapters-rule active` prints the posture and the inferred count. Mutations: predicate ignoring `reviewOnly` → 2 rows; `autoActivate` ignored → 2; `rulesModel` ignored → 2; basis classification collapsed → 1 |
199
+ | R6 | Selection is deterministic and announced | ✅ verified | Shuffled input order yields byte-identical selection; two identical runs render identical text; the withheld line appears exactly when `shown < active`, and its ABSENCE is asserted when no cap is set. `active`/`chapters_rules` return the counts even when nothing was withheld, so "you are seeing all of it" is a fact |
200
+ | R7 | Nothing was silently clipped anywhere the budget bites | ✅ verified | Token ceiling still returns `refusal` with per-rule numbers and now counts **both** tiers; the checkpoint announces the omission instead of squeezing; the door path still refuses the continuation with numbers (§1.2) |
201
+ | R8 | Ungoverned never looks like governed | ✅ verified | `/chapters-status` prints an `Identity:` line with the divergence warning — and this repo's own non-git test workspace hit that branch during verification, which is the honest evidence the warning is not theoretical; `chapters_rules` answers "no linked project" separately from "nothing approved on this machine" separately from "nothing captured this session"; the head states the zero-project tier only when session rules are actually governing it |
202
+ | R10 | Every chapters session — fresh, continued, or DELEGATED — is told to use `chapters_rules` and `chapters_search` | persona row parsed from the shipped patch: the suffix names both tools, names delegation, keeps the `PLOT:` discipline, adds no unregistered prompt variable, and remains matched by the plot template-echo guard. Verified by mutation — dropping `chapters_rules` from the suffix reddens the row |
203
+ | R11 | Rule ancestry crosses the delegation edge, and every door uses the ONE walk | `tests/unit/rules-lineage.test.ts` (8 rows: header-only parent, transitive mixed edges, one-way direction, cycle termination, the 64-expansion cap with its loud flag, unknown session, throwing reader, anti-drift). Verified by mutation: disabling the header edge reddens 4 rows |
204
+ | R9 | A conflict is reported, never resolved | ✅ verified | `conflicts` wrote no file (directory listing asserted unchanged), the prompt explicitly forbids rewriting, and with no conversation route the verb refuses rather than half-answering |
205
+ | R10 | Losing a checkout is re-derivation, not corruption | 🟡 partial | `parseRuleMarkers` is deterministic over message text and capture dedupes against disk, so a replay fills gaps without duplicating — but the **end-to-end wipe-and-replay row is not written**. The mining half is now re-derivable by re-running the ladder, and capture dedupes against disk, so a replay fills gaps rather than duplicating. |
206
+ | R-e2e | The browser plane: a live model, the `rules` journey, the new tool in the header | ❌ cannot run on this host | `npm run test:e2e:replay` **refuses by name** (`tests/e2e/boot.ts:130`: the pinned host predates preset declaration rows). The re-record this feature owes is queued with the `dev/dsh-host-lock` move in `docs/backlog.md` — stated, not skipped silently |
207
+
208
+ **First live witness (2026-10-21, `npm run mining:probe`).** Three real archived chapters of this repo,
209
+ mined by `qwen3.8-flash` — the model that powers live sessions here — through the shipped code paths
210
+ (`buildMineInput` → `parseMinedRules` → `quoteIsHuman` → `captureMinedRules`), local :8080 down so the
211
+ token-plan hosted route was used. Nine rules captured (the 3-per-chapter cap bound every chapter),
212
+ **6 stated / 3 inferred**, and all three inferences came from the one chapter with ZERO human bytes —
213
+ a subagent chapter — which is exactly what the classification should do. The model's own `basis` claim
214
+ was the weaker signal: chapters 2 and 3 each declared only 1 stated rule, while the plugin's check
215
+ found 3/3 quotes genuinely present in the human text. So the label is earned from evidence, not from
216
+ the model's say-so, and the head rendered all nine in 479 tokens with nothing withheld at a cap of 12.
217
+ On quality: none of the nine was wrong or unsafe; the three inferences were arguably the most
218
+ transferable ("verify decisive architectural claims in shipped code before treating them as settled").
219
+ n = 3 chapters on one repo's own history is not proof, but it is no longer nothing.
220
+
221
+ **A cost finding, stated twice because the first version was wrong.** The probe reported completion
222
+ usage of 969–3237 tokens against a 700-token request. The inference I drew — that the annotation
223
+ ceiling was truncating mining — was **wrong**: a direct test of the same route with a 64-token cap
224
+ still produced 113 tokens with `finish_reason: stop`, and usage itemizes
225
+ `completion_tokens_details.reasoning_tokens`. `max_tokens` is advisory on this provider, not
226
+ enforced, and reasoning counts toward completion usage. So nothing was truncating; what was true is
227
+ that the ceiling had no business being a literal at three call sites (900/900/600) sized for
228
+ pre-mining answers. It is now `enrichmentMaxTokens`, default **4000** — above the observed 3.2K —
229
+ because a *strict* provider would enforce it, and one that itemizes reasoning deserves a budget that
230
+ covers the thinking. The lesson stays worth recording: a usage number is not evidence about a
231
+ mechanism until the mechanism is tested directly.
232
+
233
+ **Inference QUALITY had no witness when the posture was set; the first data is below and it is thin.** The mechanisms are proven against fixtures; nothing here can say how often a small model infers a *correct* standing order, now that inferences govern by default. That is the open risk of the 2026-10-21 decision, and the measured first step is cheap: point the enrichment route at a reachable model, run `/chapters-enrich run` over real archives, then read the `stated` vs `inferred` ratio in `/chapters-rule list --scope session`. If inferences are wrong more often than a human catches them in replies, the answer is `rulesAutoActivate: false` — not a new evidence gate (the amendment records why the gate was the wrong instrument). The box's GPU driver was unreachable when this landed (`nvidia-smi` cannot talk to it), so no vLLM boot and no real mining pass ran.
234
+
235
+ **Two environmental facts, learned the hard way on 2026-10-20 and worth repeating.** (1) The
236
+ deterministic suites need **Node ≥ 24** (`package.json` engines); on Node 22 every test file fails
237
+ with `ERR_NO_TYPESCRIPT`/unknown-extension, which looks exactly like a broken change and is not one.
238
+ (2) Under `npm run coverage:ci`'s parallel load the real-HTTP-git files
239
+ (`git-auth-classification`, `sync-remote`) intermittently failed with an EPIPE blamed on their
240
+ `before` hook. It was **recorded as a box-level flake and left alone — which was the wrong call**: the
241
+ "prove it predates my diff" step was right, and stopping there was not. The cause was real and is now
242
+ fixed in the rig: the test git server wrote each request body into a CGI child's `stdin` with **no
243
+ error listener**, so a child that had already exited raised an async EPIPE with no handler, which the
244
+ runner reported as uncaughtException after the test ended. Guards now sit on the child's three
245
+ streams and on both request/response objects ([tests/integration/http-git-server.ts](http-git-server.ts));
246
+ three consecutive combined runs since then are 800 pass / 0 fail. Lesson for the next agent, in the
247
+ repo's own style: a pre-existing failure is still a failure.
248
+ Ledger on this branch: **93.16% statements / 85.53% branches / 90.01% functions** — the gate holds.
249
+
168
250
  ## The Cache Metric, Before You Trust a Number
169
251
 
170
252
  `usage.inputTokens` is the **uncached delta** and `usage.cacheReadTokens` is the cached prefix, so:
@@ -204,6 +286,27 @@ that still need a running chapter engine:
204
286
  | E7 | Arrival-time artifacting (architecture amendment) | **green 2026-09-19 (9.3m)**: 373KB book / one read → exactly 1 arrival prune at the tail; model answers buried token + chapter count via 5 `chapters_artifact` calls; ZERO compaction events at production 0.9; blob fingerprint in no `request/*` event; cache table in FINDINGS shows prefix intact (cacheRead monotonically 15.7K→24.5K) |
205
287
  | E8 | Plot carriage across in-place checkpoints | forward path live (persona discipline — suiteB oversized run carries `PLOT:` blocks through 7 compactions); render→extract round trip pinned in unit (`carried plot survives the checkpoint round-trip`); elicited fallback unit-pinned (capped input/output, degrades to null) — fires only when the agent writes no note | compaction lands at `agent/pre-step`, bracketed `compaction/start.turn` = open turn number (r13/r17 `turn: 2`); never mid-request, never mid-step |
206
288
 
289
+ ## Door 5 — Subagent Settlement (#47; read contract corrected by #58)
290
+
291
+ The settlement door is the only archive that runs with nobody watching, so its checks are about **payload**,
292
+ not about plumbing. Every row here was verified to fail on the mutation it guards.
293
+
294
+ | # | Check | How to verify |
295
+ |---|---|---|
296
+ | S1 | The durable read is payload-bearing | `tests/unit/host-durable-read.test.ts` reads the **host's own published types** and asserts `SessionEventRecord` has no `data` key while `readSession`/`snapshotSessionEvent` yields one. This is a kernel-drift gate: a host rename must turn it red, not re-create husks. Cross-check by hand: `zstd -dc ~/.dsh/sessions/<slug>/<id>/session.v4.jsonl.zstd \| head -20` for a settled child — the real log has `data.name`, `data.arguments`, `data.message.content`; a hollow chapter has none of them. |
297
+ | S2 | Both arms, both modes | A **one-shot** child settles with a live handle (`snapshotEvents()`); a **continuable** child is disposed *before* `subagent/end` (`dsh-subagent/lib/index.js:1240-1250`) so only the durable arm runs. Integration rows cover each arm × each mode, and one covers **depth 2** (a continuation child that delegates — `d57b4710`'s parent is `ch-dc3be3ab` in the SAIP field data). |
298
+ | S3 | No archive that rendered nothing | Feed the real `data`-less record shape to `saveSession` and assert: skipped `payloadless`, **no file written**, **no chapter number reserved**, `nextChapterNumber` unchanged. Then the converse pair: the same span read through `readSession` archives with `stats.messages > 0`. |
299
+ | S4 | The watermark is never poisoned | After a S3 refusal, a later settle of the same child still archives (i.e. `max(endSeq)` did not advance). This is the property that makes the defect recoverable instead of permanent. |
300
+ | S5 | Warnings survive the door | `writeArchive` emits `N event(s) rendered no output` for a low-content chapter; assert door 5 logs it (via the injected `warn`/sink), not returns-and-drops. |
301
+ | S6 | Lineage recorded, tree NOT re-homed | After a settle the child's own row/frontmatter carry `subagent` + `parentSession` from the durable header, and the child keeps its OWN tree: `rootSession` is unchanged and its chapter paths still resolve. **AMENDED 2026-10-22 (#58):** this row originally demanded `linkChild` onto the parent's path. Measured against the code, that buys no reach (`buildIndex` walks ancestors only) and re-homes files already written under the child's root, so the door does not link; the row now pins the absence, and `subagent-auto-save.test.ts` ("lineage by design") holds both halves. |
302
+ | S7 | Fanout containment | A 10-child fanout schedules sync 10× and runs enrichment **0** times (`runNow` never per settlement) and no child blocks another (per-child promise chains). |
303
+ | S8 | Field census (human, one-time) | `SELECT` the store: no chapter may have `messages === 0` with `events > 0`. The recovery half of this defect is #60; measure **before/after counts** there, and quote them as a repair result, never as a passing test. |
304
+
305
+ **Anti-pattern that created this section:** a stub that returns *your* type instead of the host's.
306
+ `tests/integration/subagent-auto-save.test.ts` once faked `listEvents: async () => durable` with full
307
+ events and passed against a host verb that returns metadata. If a test proves a door works while the
308
+ production arm returns a different shape, the test is the bug.
309
+
207
310
  ## Bug Reports
208
311
 
209
312
  Cache and context behaviour are the plugin's core promise, so a report must include:
@@ -234,3 +337,117 @@ transport registry). The first push against a real remote is therefore a
234
337
  If isomorphic-git ever chokes on a specific forge's smart-HTTP, the documented
235
338
  fallback is a CLI driver (the `GitDriver` seam in `src/gitops.ts` exists for
236
339
  exactly this swap) — the loop's logic is driver-agnostic by design.
340
+
341
+ ## Prompt recommendations (#71) — SPEC + verification rows
342
+
343
+ Interface: site §28 (`concepts/prompt-recommendations`); host facts in `docs/contract.md`
344
+ § Composer Ghost Layer. Off by default — **the effective model string non-empty IS the
345
+ enable**; there is deliberately no second boolean.
346
+
347
+ Deterministic rows (each must be proven to fail on the mutation it guards):
348
+
349
+ | Row | Pins |
350
+ |---|---|
351
+ | enable derivation | `effectiveModel = setting('prompt.model') ?? config.promptModel`; empty or unset ⇒ OFF, and a stored empty override beats a non-empty config (`off` verb) |
352
+ | input assembly | chapter summaries first, then the live tail; total ≤ `promptContextChars`; host-injected spans defused BEFORE the cap; deterministic order (same input ⇒ same bytes) |
353
+ | answer validation | trim → reject empty → reject > `promptMaxChars` → ONE retry with the cap restated → refuse with the measured length. Never truncate |
354
+ | cache & watermark | one entry per session keyed by the last completed `turn/end` seq; fresh hit ⇒ zero llm calls; new turn ⇒ recompute on next ask; in-flight dedup shares one promise |
355
+ | command arms | `model` show/set/clear texts, `off`, `suggest` refusals (off / no route / not computable / model failure) each name their remedy |
356
+ | status line | `/chapters-status` prints ON with the model + source, or OFF — both states, linked store or not |
357
+ | client pure logic | `shouldShowGhost` matrix: enabled ∧ !running ∧ draft==='' ∧ hasText ∧ !suppressed/boundary; accept ⇒ setDraft; typing ⇒ hidden; Esc ⇒ suppressed until the boundary moves |
358
+ | client anti-dup | `src/client/index.ts` contains no `commands.execute` for the ghost either — the wire body lives in `src/client/prompt-ghost.ts` ONE time, mirroring the door-runner rule (tests/unit/client-doors.test.ts pattern) |
359
+ | honesty | a suggestion never reaches a chapter file, the notice, the pool, or any model request: an archived session that used the feature contains zero recommendation bytes |
360
+
361
+ Manual browser procedure (browser-plane claim stays **gated by #56** — the corpus must be
362
+ re-anchored before replay can vouch for it):
363
+
364
+ 1. `/chapters-prompt model local/qwen3.8-flash` (or a cloud route) ⇒ expect "…enables the feature".
365
+ 2. Finish a turn, idle with an empty draft ⇒ the ghost line appears dimmed inside the
366
+ composer card, labeled with the model; the host placeholder is HIDDEN beneath it (draft
367
+ empty ⇒ host placeholder visible — our layer renders over it; if both are readable at
368
+ once, that is a styling bug).
369
+ 3. Type one character ⇒ ghost gone instantly; delete it ⇒ ghost back (while not accepted).
370
+ 4. Click into the editor, press Right-arrow ⇒ suggestion lands as real draft text, cursor
371
+ at its end; nothing sends. Enter sends as a normal human message — then verify in the
372
+ log that the message is plain human-authored.
373
+ 5. Esc on a fresh ghost ⇒ hidden for this turn; send any turn ⇒ boundary moves, a new offer
374
+ may come.
375
+ 6. `/chapters-prompt off` ⇒ ghost never reappears in this or later sessions, `suggest`
376
+ refuses; `/chapters-status` says OFF.
377
+ 7. Kill the model route (wrong provider) ⇒ ghost hidden, refusal only in command output, no banner.
378
+
379
+ **Distilled, observed, AND replay-verified 2026-10-22** (`prompt-suggestion.spec.ts`, project `prompt`,
380
+ port 41739): the scripted-upstream record run PASSED in a real browser on host 0.2.0-rc.2 — chip visible
381
+ with the suggestion, hidden by one typed character, re-shown on an emptied draft, Escape dismissed
382
+ boundary 1, the next boundary re-armed, ArrowRight accepted into the draft via the host's own
383
+ `InputActions.setDraft`, Enter sent it, and the log held the human message with zero persona bytes.
384
+ A true `E2E_MODEL=replay` run then passed with ALL SIX requests served from tape and zero misses
385
+ (corpus gate lifted for that check ALONE and restored byte-identical — the committed gate still
386
+ refuses the chain until #56 re-anchors the other eight; `tests/fixtures/model-tape/prompt/PROVENANCE.md`). The distill ALSO caught a rig bug worth remembering: `localModelUp` answered "no upstream" while a
387
+ healthy stub ran on the `E2E_UPSTREAM` port — the guard probed :8080 for any LOCAL base, the same
388
+ silent-skip family #54 fixed for remotes. A ROW had pinned that behavior ("loopback is answered on
389
+ :8080, not by E2E_UPSTREAM") — and the row was measured WRONG in use, twice, by this very journey
390
+ refusing to run against a live stub. Per the house rule (change obsolete behavior with its tests):
391
+ the row now pins probe-the-actual-base for local upstreams, plus a dead-local-port NO arm, with the
392
+ reason in its comment. The guard lying by REFUSING is the same disease as lying by nodding.
393
+
394
+ ## Release gate — field checks, because green tests shipped a broken product twice
395
+
396
+ Every item below is the shape of a defect that reached `main` or was found in the field (SAIP,
397
+ issues #58–#61, #77–#82) **while the suite was green**. They are not more unit tests: several of
398
+ this week's bugs had rows passing correctly around them. The gate is that a human or an agent
399
+ observes the affordance working on a real project, and that every new instrument is shown to be
400
+ capable of failing.
401
+
402
+ Run on a scratch clone of a real project (not this repo), at the pinned host and pinned window.
403
+
404
+ 1. **A continuation opens, not just a continuation created.** Fork (button or `chapters_continue`),
405
+ then require: the app's current session IS the child (read `dsh.sessions.current`, or the door's
406
+ own note — it now refuses to claim a switch it cannot verify), the child is in the sidebar, and
407
+ the state survives a reload. *Why: #77 — three evenings of "the client refuses to catalogue
408
+ plugin-created children" were a door calling `sessions.open`, a verb that does not exist
409
+ (`docs/contract.md` § Client Sessions Service). Green throughout, because no test read the id.*
410
+ 2. **The realm is mounted, not declared.** One session header must read `agentPreset: "chapters"`,
411
+ and the sink must show at least one `compactIfNeeded entered` line. *Why: the preset pin sat on
412
+ a retired row id (`agent-presets` → `agent-preset-registry`) and every acceptance boot ran the
413
+ standard realm for two days — zero `compaction/*` events in 15 logs, while leg verdicts looked
414
+ like product failures (#56, #78). "Declared" was asserted; "invoked" never was.*
415
+ 3. **Context relief actually happens, with numbers.** Force one oversized read at the pinned
416
+ window; require `compaction/*` events (or an arrival stub pair) in the durable log, then that the
417
+ NEXT turn answers a plain question. *Why: #78's decline is now reported with
418
+ `surface=/trigger=/window=/ratio=`, and a session that survives but answers a stale transcript
419
+ token is exactly what `oversized-turn.spec` caught — after #81, the wrong answer had been the rig
420
+ sending the instruction twice.*
421
+ 4. **A command reaches the human.** Run `/chapters-status` and check BOTH the durable
422
+ `command/done` AND the screen; today the screen half fails and that is #80, not a flake. A
423
+ delivered-but-invisible result is a feature the operator cannot use (#61a, #67).
424
+ 5. **A disabled feature is quiet in the durable log.** On a boot without `promptModel`,
425
+ `countGhostCommandRuns(log)` must be ≤ 1 (one capability probe per mount). *Why: #79 measured 4–26
426
+ `chapters-prompt` command pairs per session — a "once per mount" effect depending on `[props]`,
427
+ writing into the transcript this plugin archives, with the OFF test passing because it only
428
+ looked at the chip.*
429
+ 6. **The rig's pins reach the realm.** The boot's own `cordis.patch.yml` must carry this project's
430
+ `thresholdRatio` and arrival floor on the `chapters-engine` row. *Why: `Pins` were collected and
431
+ applied to nothing for a whole release cycle (leg headers claimed 0.5 while realm defaults
432
+ ran).*
433
+ 7. **The corpus is one behavior.** `CORPUS_HOST`'s `host` equals the pin, and every project's
434
+ `recordedAgainst` names the same code generation. If any leg was recorded before a rig-behavior
435
+ change (send timing, window enforcement), re-record it: a mixed corpus passes for the wrong
436
+ reason (#56, #81, #82).
437
+ 8. **A boot may read only tracked state.** List every file the e2e home is seeded from and prove each is
438
+ in `git ls-files` — a gitignored path in that chain makes the corpus a recording of one laptop.
439
+ *Why: #89, three layers deep. First the matcher folded no workspace root (CI's `/home/runner/…` vs
440
+ the recorded `/home/adrian/…`); then `spawn('dsh')` took PATH and got a **linked dev build** whose
441
+ version string matched the pin; and even with the pinned tarball booted by absolute path, CI sent a
442
+ 2-message first request against a 6-message tape — because `boot.ts` copied
443
+ `.dshdev-local/settings.yaml` (gitignored) with the provider route and `contextWindow: 32000`. Each
444
+ layer looked like the end of the story; the tape diff (`E2E_TAPE_DIFF` / `E2E_MISS_DUMP`) is what
445
+ said otherwise every time. Reproduce across machines before believing a green local chain.*
446
+
447
+ 9. **Instrumentation must be shown to be capable of failing.** For every gate this week added, name
448
+ the mutation that reddens it — the hygiene guard, the roster predicate, the report helper, the
449
+ window rows and the bounded-scroll row all have one. *Why: three bugs this week lived in code
450
+ written to observe the fourth — `\bpending\b` missing an escaped newline, `ids.size > 0`
451
+ disabling every #77 candidate, a helper resolving logs against an import-time home so a gate
452
+ could never pass, and an unbounded `scrollIntoViewIfNeeded` that hung a leg for 17 minutes. Each
453
+ looked like a product failure. **An unexercised assertion is a rumor of an assertion.**