@fractalsynapse/dsh-chapters 0.0.0-stage → 0.2.20

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 (195) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +76 -2
  3. package/cordis.patch.yml +363 -0
  4. package/docs/architecture.md +692 -0
  5. package/docs/autorun-guide.md +248 -0
  6. package/docs/backlog.md +2022 -0
  7. package/docs/contract.md +938 -0
  8. package/docs/development.md +990 -0
  9. package/docs/host-compaction-seam.md +225 -0
  10. package/docs/host-protected-head-ask.md +80 -0
  11. package/docs/knowledge-repo.md +1332 -0
  12. package/docs/plans-carriage.md +571 -0
  13. package/docs/project-state.md +642 -0
  14. package/docs/provider.md +98 -0
  15. package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
  16. package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
  17. package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
  18. package/docs/verify.md +561 -0
  19. package/lib/archive.d.ts +161 -0
  20. package/lib/archive.js +229 -0
  21. package/lib/archive.js.map +1 -0
  22. package/lib/arrival.d.ts +23 -0
  23. package/lib/arrival.js +125 -0
  24. package/lib/arrival.js.map +1 -0
  25. package/lib/artifact-query.d.ts +62 -0
  26. package/lib/artifact-query.js +187 -0
  27. package/lib/artifact-query.js.map +1 -0
  28. package/lib/attach.d.ts +46 -0
  29. package/lib/attach.js +120 -0
  30. package/lib/attach.js.map +1 -0
  31. package/lib/auto-save.d.ts +145 -0
  32. package/lib/auto-save.js +334 -0
  33. package/lib/auto-save.js.map +1 -0
  34. package/lib/aux-message.d.ts +17 -0
  35. package/lib/aux-message.js +20 -0
  36. package/lib/aux-message.js.map +1 -0
  37. package/lib/client.js +1419 -0
  38. package/lib/client.js.map +1 -0
  39. package/lib/codegraph.d.ts +76 -0
  40. package/lib/codegraph.js +190 -0
  41. package/lib/codegraph.js.map +1 -0
  42. package/lib/commands.d.ts +52 -0
  43. package/lib/commands.js +946 -0
  44. package/lib/commands.js.map +1 -0
  45. package/lib/compose.d.ts +30 -0
  46. package/lib/compose.js +185 -0
  47. package/lib/compose.js.map +1 -0
  48. package/lib/continue-core.d.ts +245 -0
  49. package/lib/continue-core.js +405 -0
  50. package/lib/continue-core.js.map +1 -0
  51. package/lib/diag.d.ts +55 -0
  52. package/lib/diag.js +186 -0
  53. package/lib/diag.js.map +1 -0
  54. package/lib/engine-core.d.ts +301 -0
  55. package/lib/engine-core.js +531 -0
  56. package/lib/engine-core.js.map +1 -0
  57. package/lib/engine-note.d.ts +35 -0
  58. package/lib/engine-note.js +32 -0
  59. package/lib/engine-note.js.map +1 -0
  60. package/lib/engine.d.ts +329 -0
  61. package/lib/engine.js +1179 -0
  62. package/lib/engine.js.map +1 -0
  63. package/lib/enrich-queue.d.ts +66 -0
  64. package/lib/enrich-queue.js +126 -0
  65. package/lib/enrich-queue.js.map +1 -0
  66. package/lib/enrich-store.d.ts +31 -0
  67. package/lib/enrich-store.js +103 -0
  68. package/lib/enrich-store.js.map +1 -0
  69. package/lib/enrich-wire.d.ts +163 -0
  70. package/lib/enrich-wire.js +353 -0
  71. package/lib/enrich-wire.js.map +1 -0
  72. package/lib/enrich.d.ts +102 -0
  73. package/lib/enrich.js +267 -0
  74. package/lib/enrich.js.map +1 -0
  75. package/lib/gitops.d.ts +107 -0
  76. package/lib/gitops.js +539 -0
  77. package/lib/gitops.js.map +1 -0
  78. package/lib/index.d.ts +241 -0
  79. package/lib/index.js +1162 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/indexing.d.ts +98 -0
  82. package/lib/indexing.js +223 -0
  83. package/lib/indexing.js.map +1 -0
  84. package/lib/injections.d.ts +90 -0
  85. package/lib/injections.js +165 -0
  86. package/lib/injections.js.map +1 -0
  87. package/lib/last-resort.d.ts +24 -0
  88. package/lib/last-resort.js +27 -0
  89. package/lib/last-resort.js.map +1 -0
  90. package/lib/log-shape.d.ts +38 -0
  91. package/lib/log-shape.js +93 -0
  92. package/lib/log-shape.js.map +1 -0
  93. package/lib/models-command.d.ts +74 -0
  94. package/lib/models-command.js +100 -0
  95. package/lib/models-command.js.map +1 -0
  96. package/lib/notice.d.ts +74 -0
  97. package/lib/notice.js +92 -0
  98. package/lib/notice.js.map +1 -0
  99. package/lib/plans.d.ts +154 -0
  100. package/lib/plans.js +315 -0
  101. package/lib/plans.js.map +1 -0
  102. package/lib/preset.d.ts +186 -0
  103. package/lib/preset.js +212 -0
  104. package/lib/preset.js.map +1 -0
  105. package/lib/prompt-keys.d.ts +7 -0
  106. package/lib/prompt-keys.js +8 -0
  107. package/lib/prompt-keys.js.map +1 -0
  108. package/lib/prompt-loop.d.ts +73 -0
  109. package/lib/prompt-loop.js +147 -0
  110. package/lib/prompt-loop.js.map +1 -0
  111. package/lib/prompt-push.d.ts +47 -0
  112. package/lib/prompt-push.js +115 -0
  113. package/lib/prompt-push.js.map +1 -0
  114. package/lib/prompt-runner.d.ts +48 -0
  115. package/lib/prompt-runner.js +90 -0
  116. package/lib/prompt-runner.js.map +1 -0
  117. package/lib/prompt-state.d.ts +142 -0
  118. package/lib/prompt-state.js +101 -0
  119. package/lib/prompt-state.js.map +1 -0
  120. package/lib/prompt.d.ts +270 -0
  121. package/lib/prompt.js +408 -0
  122. package/lib/prompt.js.map +1 -0
  123. package/lib/provider.d.ts +44 -0
  124. package/lib/provider.js +66 -0
  125. package/lib/provider.js.map +1 -0
  126. package/lib/redact.d.ts +24 -0
  127. package/lib/redact.js +85 -0
  128. package/lib/redact.js.map +1 -0
  129. package/lib/registry.d.ts +187 -0
  130. package/lib/registry.js +235 -0
  131. package/lib/registry.js.map +1 -0
  132. package/lib/removal.d.ts +159 -0
  133. package/lib/removal.js +556 -0
  134. package/lib/removal.js.map +1 -0
  135. package/lib/render.d.ts +97 -0
  136. package/lib/render.js +530 -0
  137. package/lib/render.js.map +1 -0
  138. package/lib/repair-run.d.ts +76 -0
  139. package/lib/repair-run.js +506 -0
  140. package/lib/repair-run.js.map +1 -0
  141. package/lib/repair.d.ts +208 -0
  142. package/lib/repair.js +357 -0
  143. package/lib/repair.js.map +1 -0
  144. package/lib/repo.d.ts +78 -0
  145. package/lib/repo.js +184 -0
  146. package/lib/repo.js.map +1 -0
  147. package/lib/rules-commands.d.ts +46 -0
  148. package/lib/rules-commands.js +379 -0
  149. package/lib/rules-commands.js.map +1 -0
  150. package/lib/rules.d.ts +267 -0
  151. package/lib/rules.js +478 -0
  152. package/lib/rules.js.map +1 -0
  153. package/lib/save-core.d.ts +72 -0
  154. package/lib/save-core.js +220 -0
  155. package/lib/save-core.js.map +1 -0
  156. package/lib/search.d.ts +112 -0
  157. package/lib/search.js +282 -0
  158. package/lib/search.js.map +1 -0
  159. package/lib/session-rules.d.ts +222 -0
  160. package/lib/session-rules.js +454 -0
  161. package/lib/session-rules.js.map +1 -0
  162. package/lib/signature.d.ts +45 -0
  163. package/lib/signature.js +199 -0
  164. package/lib/signature.js.map +1 -0
  165. package/lib/store.d.ts +156 -0
  166. package/lib/store.js +368 -0
  167. package/lib/store.js.map +1 -0
  168. package/lib/sync.d.ts +220 -0
  169. package/lib/sync.js +810 -0
  170. package/lib/sync.js.map +1 -0
  171. package/lib/tools.d.ts +113 -0
  172. package/lib/tools.js +1090 -0
  173. package/lib/tools.js.map +1 -0
  174. package/lib/treedx/client.d.ts +57 -0
  175. package/lib/treedx/client.js +82 -0
  176. package/lib/treedx/client.js.map +1 -0
  177. package/lib/treedx/provider.d.ts +39 -0
  178. package/lib/treedx/provider.js +479 -0
  179. package/lib/treedx/provider.js.map +1 -0
  180. package/lib/treedx/state.d.ts +27 -0
  181. package/lib/treedx/state.js +65 -0
  182. package/lib/treedx/state.js.map +1 -0
  183. package/lib/types.d.ts +210 -0
  184. package/lib/types.js +31 -0
  185. package/lib/types.js.map +1 -0
  186. package/lib/version.d.ts +9 -0
  187. package/lib/version.js +10 -0
  188. package/lib/version.js.map +1 -0
  189. package/lib/vocabulary.d.ts +41 -0
  190. package/lib/vocabulary.js +118 -0
  191. package/lib/vocabulary.js.map +1 -0
  192. package/lib/warnings.d.ts +10 -0
  193. package/lib/warnings.js +18 -0
  194. package/lib/warnings.js.map +1 -0
  195. package/package.json +151 -4
@@ -0,0 +1,1332 @@
1
+ # Knowledge Repository Design Record
2
+
3
+ The governing design for the **git-based shared knowledge pool**: multiple DSH harness instances,
4
+ on multiple machines, feeding and searching one private per-project repository of past
5
+ conversations — chapters, artifacts, a topic index, and rules — while keeping every individual
6
+ session's context small. Written to *govern*: implementation plans derive from this file, and
7
+ amendments are human decisions recorded in git history (see **Amendments** below).
8
+
9
+ Status: **agreed 2026-09-17, pre-implementation.** Phased build in §13.
10
+
11
+ ## 0. What this is, and what it is not
12
+
13
+ **What it is:** the plugin's existing artifacts (chapters, deferred tool results, the TOC
14
+ continuation mechanic) become the contents of a private repository per project, synced across
15
+ every harness the user connects to it — over **pluggable transport**: a git repo (§3, the
16
+ default) or a TreeDX service (`treedx+…`, Amendment 2026-09-21, transport). Compaction and forking remain the
17
+ *knowledge generators*; the repo is the *transport and the shared corpus*. Agents gain one
18
+ new capability: search and load of past work across their own and other machines' sessions,
19
+ plus a bounded set of
20
+ human-approved rules to live by.
21
+
22
+ **What it is not:**
23
+
24
+ - Not a general vector store or embedding search. Scoring is deterministic and inspectable (§8).
25
+ - Not a system-prompt injection channel. Rules enter context only through the continuation
26
+ notice (§7) — per-continuation, budgeted, append-only — never as a contributed header section
27
+ (invariant 3, `AGENTS.md`).
28
+ - Not a rewrite of what shipped. The MVP loop (engine, registry, TOC continuation, fork button)
29
+ is the foundation this record extends; §1 lists the invariants that must survive the extension.
30
+ - Not real-time collaboration. Sync points are deliberate (§5); the repo is a pool, not a shared
31
+ live document.
32
+
33
+ ## 1. Invariants the extension must not erode
34
+
35
+ Carried from `AGENTS.md` and this project's measured history, restated because the knowledge
36
+ layer tempts each one:
37
+
38
+ 1. **The compaction/fork critical path stays zero-model-token.** Model enrichment is strictly
39
+ post-archive, async, optional (§6). A fork must remain instant even when the model is down,
40
+ busy, or slow — the deterministic path is the product; enrichment is garnish.
41
+ 2. **Refuse with numbers, never truncate.** Applies to the rules core set rendered into the
42
+ notice (§7) and to search output packing (§8): over budget → refuse/explain with the
43
+ measured numbers, or the user curates; silent clipping is banned.
44
+ 3. **The local store is append-only.** Chapters are never renumbered, rewritten, or
45
+ re-chaptered after archive. Composition must be right at archive time (§4); enrichment can
46
+ relabel, never re-chapter.
47
+ 4. **Model proposes, plugin commits — extended.** The model proposes topics, summaries, and
48
+ rule text. The plugin owns the canonical vocabulary, the index, and rule status (§6, §7).
49
+ The model never writes to the repo directly; the plugin is the only writer.
50
+ 5. **Per-machine write namespaces.** Two machines never write the same file. Zero merge
51
+ conflicts by construction, not by hope (§3.3).
52
+ 6. **Budget the added context, never the corpus.** Continuation notices stay O(1) in corpus
53
+ size (§7.3). The corpus grows without bound; the head it pays does not.
54
+ 7. **Verbatim stays verbatim** except the redaction carve-out (§9) — and the carve-out is
55
+ credentials-only, marked, and deterministic.
56
+
57
+ ## 2. The knowledge repository
58
+
59
+ ### 2.1 One private repo per project, user-created
60
+
61
+ The user creates a private repository per project and registers it with
62
+ `/chapters-link` (four modes, live): **no args** shows the upstream + mirror state,
63
+ **a local path** binds a directory upstream, **a URL + token** binds a network git
64
+ upstream, **`treedx+<url>/<repo>` + token** binds a TreeDX service (managed repository
65
+ resolved-or-created by the link itself — the explicit human act; §15.1).
66
+ Profile config (`knowledgeRemote`) supplies the same URL as a lazy default; it never
67
+ creates anything by itself. A personal cross-project repo is the *user's* choice of an
68
+ additional project entry, not a built-in mode.
69
+
70
+ Per-harness credentials are **scoped, revocable tokens limited to that repository** —
71
+ deliberately better than SSH deploy keys: revocable, per-repo, no shell access.
72
+ **Credentials are mandatory for network upstreams** (loopback exempt, so local test
73
+ forges work) and **never needed for local-path upstreams**. Storage is under the DSH
74
+ home — `<DSH_HOME>/dsh-chapters/credentials/<projectKey>`, 0600 — never the project
75
+ tree: the workspace is the agent's reading room and only the plugin touches the token
76
+ (a same-uid hygiene boundary, not a cryptographic one; the hard guarantee is that no
77
+ code path ever echoes it into a context).
78
+
79
+ Why per-project: natural secret boundary (§9), natural permission boundary, and the
80
+ `projectKey` (§2.3) is what keeps a small model's session from wading into a foreign corpus by
81
+ accident.
82
+
83
+ ### 2.2 Repository layout
84
+
85
+ ```
86
+ <knowledge-repo>/
87
+ project.yml # projectKey, slug, created-by, created-at (plugin-maintained)
88
+ chapters/
89
+ <projectKey>/
90
+ <sessionId>/
91
+ <chapter-file>.md # frontmatter + verbatim body (today's format, §2.4)
92
+ artifacts/
93
+ <projectKey>/
94
+ <sha256-prefix>/<sha256> # deferred tool results, content-addressed (today's format)
95
+ collections/
96
+ <projectKey>/
97
+ <sessionId>.jsonl # per-turn signatures, one line per completed turn (§4.1)
98
+ index/ # LAYER 2 — derived, plugin-built, do not hand-edit (§10.2)
99
+ <topic-shard>.md # one shard per canonical topic, alphabetical
100
+ manifest.json # build manifest: file hash → last processed state (§10.2)
101
+ edits/
102
+ <harness-id>/
103
+ curation.jsonl # LAYER 1 — per-machine editable index facts (§10.1)
104
+ rules/
105
+ <projectKey>/
106
+ <rule-file>.md # rules-as-chapters: kind: rule (§7)
107
+ topics/
108
+ vocabulary.json # plugin-owned canonical vocabulary + aliases (§6.3)
109
+ ```
110
+
111
+ Machine-specific and derived state lives in machine-namespaced or manifest-tracked files (§10);
112
+ human-meaningful content (chapters, rules, vocabulary) is globally readable and git-diffable so
113
+ the user can see what any harness decided and revert it.
114
+
115
+ ### 2.3 `projectKey`: UUIDv5 over the normalized remote
116
+
117
+ The key must be **identical on every machine** for the same project — that is the whole point,
118
+ since local paths differ (`/Users/adrian/repo` vs `/home/adrian/Projects/repo`).
119
+
120
+ Canonicalization, exact:
121
+
122
+ 1. Take the project's git remote URL (`origin`, first if several).
123
+ 2. Lowercase.
124
+ 3. Normalize form: strip `https://` and `ssh://` schemes; convert `git@host:org/repo` →
125
+ `host/org/repo`; strip trailing `/` and `.git`.
126
+ 4. **Keep the host.** Two hosts, same path = different projects.
127
+ 5. `projectKey = UUIDv5(NAMESPACE_OID, canonical-string)` — standards-based, deterministic,
128
+ name-derived; the same canonical string always yields the same key, anywhere.
129
+
130
+ Fallbacks, all recorded in `project.yml` and the notice so they are visible:
131
+
132
+ - **Non-git workspace:** UUIDv5 over the absolute path, flagged `path-derived` (unstable across
133
+ machines — the notice says so).
134
+ - **User override:** a `projectKey:` line in config wins, for monorepo subdirectories and
135
+ multi-remote situations.
136
+
137
+ ### 2.4 Chapter frontmatter (extended)
138
+
139
+ Today's frontmatter (title, summary, ranges, sha, …) gains:
140
+
141
+ ```yaml
142
+ projectKey: 1c3f… # §2.3 — every chapter carries its project
143
+ kind: chapter # chapter | rule (§7)
144
+ harnessId: adrian-mbp # writing machine, human-readable
145
+ topics: [auth, middleware] # deterministic signature topics at archive time (§4);
146
+ # model-enrichment may later add semantic topics
147
+ generated: # provenance per field class (user-confirmed, §6)
148
+ title: {by: deterministic}
149
+ summary: {by: model, model: qwen3.8-flash-next, at: 2026-09-17T…} # or {by: deterministic}
150
+ topics: {by: deterministic}
151
+ ```
152
+
153
+ Provenance is load-bearing: it is what makes the emergent vocabulary auditable and enrichment
154
+ re-runnable (§6).
155
+
156
+ ## 3. Storage vs transport
157
+
158
+ ### 3.1 The store stays in the workspace; the clone is a mirror
159
+
160
+ Chapters must remain reachable by the existing `read` tool — a property proven load-bearing in
161
+ the r22 read-back test, and one the agent sandbox may gate for outside-workspace paths.
162
+ Therefore:
163
+
164
+ - **Storage** stays exactly where it is: `.dsh-chapters/<rootSessionId>/…` in the workspace.
165
+ - **Transport** is a git clone in the same workspace: `.dsh-knowledge/` (hidden; the plugin
166
+ writes/updates a gitignore hint in the project on first sync so the project's own repo
167
+ ignores it).
168
+ - Sync = copy-out (store → clone, commit, push) and copy-in (clone → store, new files only).
169
+ The clone is *transport*, never the read path's source of truth for the live session.
170
+
171
+ Failure domains stay separate: a broken remote never breaks compaction/fork (sync is
172
+ best-effort, §5.3), and a broken clone never corrupts the store.
173
+
174
+ ### 3.2 Git client: pure-JS SDK, not the executable
175
+
176
+ **Decision (user-directed):** no dependency on a `git` binary. A pure-JS client
177
+ (isomorphic-git-class) over **HTTPS** — which is what makes the scoped-token credential model
178
+ of §2.1 possible. Logged risks: pure-JS fetch/push is slower than native git (mitigation: the
179
+ knowledge repo is small relative to source repos; lazy-clone on first use); the repo grows a
180
+ dependency to own. Fallback, config-gated and documented: the CLI, for hosts where the SDK
181
+ chokes. SSH-only remotes are a known gap in this decision (HTTPS tokens are the supported path).
182
+
183
+ **Local-path upstreams bypass HTTP entirely** (`/chapters-link <path>`, added with the
184
+ P1 close-out): the pool is a directory (bare repo created on first sync if absent) and
185
+ transfer is isomorphic-git's own pack layer — `packObjects` fed an **explicitly computed
186
+ reachability closure** (commits → trees → blobs; packObjects packs exactly what it is
187
+ given, measured) written straight into the target's `objects/pack/` and `indexPack`ed,
188
+ then refs moved under the same ff-only rule. Zero network, zero credentials, still zero
189
+ git-binary. Two machines converging through one shared path — including a real fork
190
+ resolved by mirror rebuild — is covered by `tests/integration/sync-local-upstream.test.ts`.
191
+
192
+ **§3.1/§3.2 are the git provider's rules.** Since the 2026-09-21 transport amendment the transport behind them
193
+ is one implementation of a six-verb seam (`src/provider.ts`); the TreeDX service provider
194
+ keeps §3.1's store/mirror split verbatim (the mirror is provider-materialized content, not a
195
+ clone) and §5.2's rebuild-on-divergence verbatim. The per-verb mapping, failure classes, and
196
+ mirror state file are specified in [docs/provider.md](provider.md); API facts and the
197
+ [LIVE-GATED] list live in [spikes/treedx/FINDINGS.md](../spikes/treedx/FINDINGS.md).
198
+
199
+ ### 3.3 The no-conflict rule
200
+
201
+ Two machines never write the same file:
202
+
203
+ - `chapters/`, `artifacts/`, `collections/` are partitioned by **sessionId** (UUID — collision-free)
204
+ under the project key; a session belongs to exactly one machine.
205
+ - `edits/<harness-id>/` is partitioned by **harness id** — one writer per file, ever.
206
+ - `index/`, `topics/vocabulary.json`, `project.yml` are **plugin-derived, single-consumer
207
+ writes**: built by the syncing machine from append-only inputs. If a pull ever finds a
208
+ divergent derived file, the plugin **rebuilds it locally and commits the rebuild** — never
209
+ merge-resolves it (§10.2). Merge conflicts are structurally impossible for content and
210
+ structurally repaired for derived state.
211
+
212
+ ## 4. Chapter composition: topic-sequential, deterministic
213
+
214
+ The user requirement: **consecutive message collections that reference the same task merge into
215
+ one chapter; a chapter splits only when the task changes or the chapter outgrows a configured
216
+ limit.** This replaces size-only splitting as the primary axis and is the load-bearing new
217
+ algorithm of the whole record.
218
+
219
+ ### 4.1 Collection and signature
220
+
221
+ - **Collection = one completed turn**: the user message(s) of the turn, its assistant
222
+ messages, and its `turn/end`. (The "user + first + last assistant" slice is the *enrichment
223
+ input*, §6.1; composition works on the whole collection.)
224
+ - **At `turn/end`, synchronously** (millisecond-scale, no queue), the plugin extracts the
225
+ collection's **deterministic signature** and appends one line to
226
+ `collections/<projectKey>/<sessionId>.jsonl`:
227
+
228
+ ```json
229
+ {"seqs":[41,42,43,44,45],"paths":["src/auth/middleware.ts","src/auth/token.ts"],
230
+ "commands":["npm test","git diff"],"terms":["auth","refresh","jwt","middleware"],
231
+ "size":1830,"by":"deterministic","at":"2026-09-17T…"}
232
+ ```
233
+
234
+ - `paths`: file paths mentioned in tool calls/results (highest-signal source in coding
235
+ sessions).
236
+ - `commands`: command/argv tokens, normalized.
237
+ - `terms`: stopword-filtered keyword frequency, top-N.
238
+ - `size`: estimated tokens (cross-machine heuristic — fine for composition, not for
239
+ display; the TOC shows message counts, not tokens, §7.3, because tokenization is not
240
+ comparable across models).
241
+
242
+ Because the signature is computed *when the turn completes*, **no compaction or fork ever
243
+ needs unprocessed material**: every completed collection is already signed, and an
244
+ unfinished collection (the in-flight turn) is never in scope — the host compacts at
245
+ `agent/pre-step` only, never mid-step (measured r13/r17), and its retained tail always
246
+ contains the current turn. This is the answer to "compaction can happen at any step."
247
+
248
+ ### 4.2 Composition (at archive time)
249
+
250
+ When a compaction or fork archives a span of completed collections, chapters are composed by a
251
+ **greedy sequential merge**, deterministic and O(collections in span):
252
+
253
+ 1. Open chapter 1 at collection 1; accumulate its running signature (union, with
254
+ per-collection weighting).
255
+ 2. For each next collection: **merge** if
256
+ `overlap(collection, running) ≥ τ` **or** `primaryMatch(collection, running)` — **and**
257
+ `chapterSize < chapterLimit` — otherwise **close the chapter and open a new one**
258
+ at this collection.
259
+ 3. `overlap` is a normalized term overlap: shared paths (heavy weight — same file is
260
+ near-probably same task), shared command families, shared terms, over the union.
261
+ `τ` (merge threshold) and `chapterLimit` are config (§12), both in the same
262
+ "no hardcoded tunables" register as `chapterTokenTarget`.
263
+ 4. `primaryMatch`: the first path of either running member (`paths[0]` — the file a
264
+ turn is *about*, by causal order of touch) names the same file as the incoming
265
+ collection's primary, with a spelling tolerance (`src/render` = `src/render.ts`).
266
+ **Amendment r28 (2026-09-18, measured live):** the first real topic test — five
267
+ research turns on this repo — showed overlap ALONE never reaches τ for legitimate
268
+ same-topic pairs: real turns touch 5–6 files, one shared path dilutes in a union
269
+ of ~10 (measured 0.077 vs τ 0.3), while cross-topic pairs score 0.0–0.02. The
270
+ intent of rule 3's "same file is near-probably same task" is thus carried
271
+ explicitly: a shared primary is a first-class merge signal, not a diluted weight.
272
+ τ and the size cap are unchanged; every decision still logs the rule that fired.
273
+
274
+ Boundary, measured live (r29/r36): the host's pressure selection ends at
275
+ tool-pair balance points, blind to turn boundaries — shallow repairs slice
276
+ collections, and the completeness guard then correctly falls back to legacy
277
+ single-chapter fragments. Topic-sequential composition therefore governs the
278
+ turn-aligned archive moments (fork/continue, where the span is anchored at a
279
+ `turn/end`); the engine wiring (r29) is the opportunistic upgrade for deep
280
+ selections (first breach of a heavily-over window), never a fragment
281
+ regression. If engine-path merges ever need to be reliable under steady
282
+ pressure, the fix is cross-chapter collection stitching in the index (P2),
283
+ not selection tuning.
284
+
285
+ Honest quality bar: strong for coding sessions (files and commands leave loud signatures),
286
+ degrading to time/size splitting for pure-prose conversations — never worse than today's
287
+ behavior, never blocking.
288
+
289
+ **Amendment 2026-10-22 (#61, item 6) — a delegate's report to its parent IS conversation.** The
290
+ injection screen (H1) drops host-injected `user/message` events, and it was right to drop the project's
291
+ instruction files. But the same rule was also dropping two shapes the harness produces *about this
292
+ session's own work*:
293
+
294
+ | source (measured on 0.2.x logs) | content | was | now |
295
+ |---|---|---|---|
296
+ | `{ kind: 'agent-message', form: 'relay' }` | "Agent <id> sent a message: Both Starlight documentation files have been written…" | screened | **archived** |
297
+ | `{ kind: 'subagent-settled', form: 'notice' }` | "Background subagent <id> finished and will do no further work…" | screened | **archived** |
298
+ | `{ kind: 'agent-instructions', form: 'instructions' }` | a project's workspace instruction file | screened | **still screened** |
299
+
300
+ Measured cost of the old rule in the first `chapters`-preset project: **44 markers, 248,150 chars ≈ 62K
301
+ est tokens** of delegated results thrown away, including an 18,253-char spike report — so the work that
302
+ crossed the delegation edge was lost through **both** doors built to capture it (the parent's archive
303
+ dropped the report; the child's settlement had already archived its own bytes, so the parent's chapter
304
+ recorded a delegation with no outcome).
305
+
306
+ Still named by **producer AND form**, exactly like the `plan-mode` precedent it joins — the list cannot
307
+ widen by accident into the general plugin-source exemption, and an instruction file arriving with some
308
+ other form still screens. `isHumanAuthored` stays strict on purpose: the topic-signature layer keeps
309
+ ignoring these, because the r28 lesson (injected text moves chapter boundaries) is unchanged, and a
310
+ 2 KB report is exactly the kind of text that would drag a signature toward `docs/…` filenames.
311
+
312
+ ### 4.3 The completeness guard
313
+
314
+ Chapters include **only collections with a `turn/end`** inside the span. If a compaction
315
+ boundary ever lands mid-collection (the manual-`/compact` edge case — the automatic path
316
+ cannot produce one, §4.1), the composition clamps to the last completed collection and the
317
+ TOC notes the clamp. The unfinished collection stays in the live context, per the user
318
+ requirement, and composes at the next archive.
319
+
320
+ **Amendment 2026-10-22 (#59, measured in the first real `chapters`-preset project).** This guard is
321
+ one-directional: it keeps unfinished work OUT, and says nothing about what may silently fail to come
322
+ IN. Composing from collections alone means a completed turn that never got a signature vanishes — not
323
+ truncated, not deferred, **absent from every chapter and from every index**, with no warning. That is
324
+ what happened: `session-2edf9694` had 32 `turn/end` events and 31 collections, and the 203-event span
325
+ between chapters 27 and 28 — a complete turn including the maintainer's "I consider it foundational to
326
+ the project" hosting brief and its implementation — exists in no chapter in any tree, and the three
327
+ verbatim phrases from it match zero files in the store.
328
+
329
+ The guard is therefore **bidirectional** now:
330
+
331
+ 1. Nothing unfinished is archived (the rule as written above).
332
+ 2. **Nothing finished is dropped.** The composed ranges must cover `[spanStart..spanEnd]`
333
+ contiguously; an unsigned but *completed* span is absorbed into its neighbouring chapter (a
334
+ wider range, never a re-titled one), and every door asserts the coverage before writing,
335
+ refusing with the exact seqs rather than proceeding. The claim a chapter makes is
336
+ "everything in this range is here", so a hole is a false statement, not a small one.
337
+ 3. A door may not treat "no collection covered it" as "incomplete turn": completeness is a
338
+ `turn/end` fact, read from the log, not inferred from the presence of a signature — a lost
339
+ signature is a registry defect (see docs/architecture.md § The row is shared), and the note the
340
+ composer used to emit claimed the opposite while dropping completed work.
341
+
342
+ `coverage()` (`src/archive.ts`) existed for exactly this and was called in one door, for a warning
343
+ that got counted and thrown away. The same rule now covers model-submitted ranges:
344
+ `validateRanges` refuses a **gap**, not only an overlap — AGENTS.md already said "a gap silently
345
+ drops it"; the check did not exist.
346
+
347
+ ### 4.4 Consequence: composition is archive-time-only
348
+
349
+ Because chapters are append-only (invariant 3), the merge threshold must be right *when the
350
+ archive happens*. The per-collection signature is stored with provenance (§4.1) precisely so
351
+ any merge or split is **inspectable after the fact** — and so `τ` can be tuned for future
352
+ archives without pretending to rewrite old ones. Enrichment (§6) may later *relabel* a
353
+ chapter's topics but may never re-chapter it.
354
+
355
+ ## 5. The sync loop
356
+
357
+ ### 5.1 Points
358
+
359
+ | Event | Action |
360
+ |---|---|
361
+ | New chapters-preset session created | **pull** (fresh corpus before the first turn; §5.2 ordering) |
362
+ | Fork (button or command) | **pull → compose/archive → push** |
363
+ | Compaction finalization | **push** (debounced — see 5.2) |
364
+ | Idle (debounce expired after the last archive) | **push** |
365
+ | Pull failure / push failure | log, surface via `/chapters-status`, **never fail the archive** |
366
+ | `/chapters-link <path>` | binds a directory upstream (bare repo materialized on first sync); no credentials |
367
+
368
+ Compaction and fork are simultaneously push points *and* the right refresh points — the loop
369
+ is pull → archive new span → push, so a fork point always sees a current index.
370
+
371
+ ### 5.2 Ordering and serialization
372
+
373
+ - A **per-repo writer lock** serializes (a) store transactions (reserve → write → commit) and
374
+ (b) clone operations. A pull never lands mid-archive-write and a push never ships a
375
+ half-committed store.
376
+ - Push = `copy-out → commit → fetch → if diverged: rebase-apply (append-only ⇒ fast-forward
377
+ safe) → push`, with bounded retry. All git calls run with no TTY prompt (
378
+ `GIT_TERMINAL_PROMPT=0` equivalent in the SDK), hard timeouts, and network treated as
379
+ best-effort.
380
+ **Implementation note (r35, via §15):** divergence is resolved by REBUILDING the mirror,
381
+ not rebasing it — `remove → fresh clone → re-publish from the store → push`. Same
382
+ semantics (the local backlog reaches the remote without force; append-only content merges
383
+ trivially), fewer moving parts, and no rebase primitive needed (isomorphic-git 1.42 has
384
+ none). The rebuild is safe only because the mirror is transport (§3.1) — the workspace
385
+ store is the truth it re-publishes from. Diverged-vs-network is a machine-readable
386
+ failure CODE, not prose matching: a network error's message once contained the word
387
+ "diverge" and falsely triggered this path (caught by the real-HTTP test).
388
+ - Debounce: consecutive archive events within the configured window coalesce into one push.
389
+ The triggers live in both planes: the host plugin pulls before, and schedules after,
390
+ fork/continue archives; the realm engine schedules after compaction finalization and
391
+ pulls at each session's first `turn/start` (the "new session" refresh point, read
392
+ process-scoped). The file lock is what makes the two schedulers safe side by side.
393
+
394
+ ### 5.3 Failure semantics
395
+
396
+ Remote down, token revoked, clone corrupted — each degrades to *local-only mode*: the store
397
+ keeps working, pending sync accumulates, `/chapters-status` says so with numbers. Nothing in
398
+ the conversation path waits on the network.
399
+
400
+ ## 6. Model enrichment: async, provenance-tagged, re-runnable
401
+
402
+ ### 6.1 The pass
403
+
404
+ For each archived chapter, one structured call whose input is the cheap slice the user
405
+ specified — **the user request(s) plus first and last assistant message**, each capped, plus
406
+ the first ~200 tokens of the chapter body for grounding — and whose output is validated JSON:
407
+
408
+ ```json
409
+ {"title": "…one sentence…", "summary": "…short…", "topics": ["…up to 10…"]}
410
+ ```
411
+
412
+ - **Validation before commit:** schema-check; on failure, retry once; on second failure, keep
413
+ the deterministic values (title from the existing derivation, excerpt-labeled summary,
414
+ signature topics). A slow or failing model never blocks or blanks a chapter.
415
+ - **Thinking models:** the pass runs at low reasoning effort and tight `maxTokens`; on the
416
+ local server this is the only model work this feature ever forces, and it is batched,
417
+ below.
418
+ - **Topics are proposals:** they enter the chapter's frontmatter only through the
419
+ canonicalizer (§6.3) — unknown labels pass as `unvetted`.
420
+ - **What proves it live (#175):** the tape-backed `enrich` leg asserts the plumbing on canned
421
+ completions. `tests/e2e/model-activities-live.spec.ts` runs the same journey against the real
422
+ provider, where a REAL completion writes the `generated:` frontmatter (verbatim body preserved),
423
+ and the mining half asserts the durable rule file itself (§7.5). Live-class: excluded from the
424
+ model-free acceptance job by design, and the exclusion is stated wherever the claim is made.
425
+
426
+ ### 6.2 When it runs: idle-batched, never per-turn
427
+
428
+ **Measured constraint (r29-era):** the local server is **single-slot**
429
+ (`maxConcurrent: 1`). Per-turn model work would queue against the user's own conversation
430
+ turns. Therefore:
431
+
432
+ - **Per turn:** deterministic signature only (§4.1) — free, synchronous, no model.
433
+ - **Model enrichment:** batched at natural idle points — after a compaction/fork push
434
+ completes, or on an idle debounce — processing *archived* chapters only. It can be
435
+ disabled per-harness in config; a harness on a contended machine can run
436
+ signatures-only and still contribute a fully functional corpus.
437
+ - **Re-runnable:** a command re-annotates a chapter (or the whole project) with a newer model
438
+ or corrected vocabulary, overwriting only the model-owned fields (frontmatter provenance
439
+ records each overwrite). The verbatim body is never touched.
440
+
441
+ ### 6.3 Self-bootstrapping vocabulary (no seed list, no hand-editing)
442
+
443
+ Per the user requirement, the vocabulary is **plugin-owned and emergent** — the user neither
444
+ authors a seed list nor edits files:
445
+
446
+ 1. **First runs:** no canonical vocabulary. Topic labels are raw deterministic candidates;
447
+ enrichment topics enter flagged `unvetted`.
448
+ 2. **Merge signals**, watched by the plugin against its own corpus:
449
+ - two labels **co-occur** in the same chapter repeatedly;
450
+ - two labels share **high normalized token overlap** across chapters;
451
+ - **alias patterns** (substring/singular-plural: `auth` ⊂ `authentication`).
452
+ Past threshold, the plugin writes a **curation entry** into `edits/<harness-id>/`
453
+ (Layer 1, §10.1): `author: plugin`, `reason: <signal + counts>`, `merge: {from, to}`.
454
+ 3. The derived index and `topics/vocabulary.json` reflect curation entries on the next build;
455
+ the vocabulary file stays **diffable and git-revertable** — the user can see every
456
+ decision the plugin made, and undo one, without ever having to make one.
457
+ 4. Escape hatch (P3, optional): `/chapters-topic merge <a> <b>` — the same curation entry,
458
+ human-authored.
459
+
460
+ Provenance in frontmatter (§2.4) is what makes all of this auditable: any label on any
461
+ chapter traces to a deterministic pass, a specific model run, or a specific curation entry.
462
+
463
+ ## 7. Rules: chapters of a different kind
464
+
465
+ ### 7.1 Model
466
+
467
+ Rules reuse the chapter machinery end-to-end — same file format, registry, budget preflight,
468
+ search, and sync. Frontmatter: `kind: rule`, plus `category`, `status`, and provenance.
469
+ **Rules are another form of chapter to search when needed** (user's framing, adopted); the
470
+ plugin's modularity is what makes this nearly free — the chapter code paths are parameterized
471
+ by `kind`, not duplicated.
472
+
473
+ **Two scopes, one store, one format** (Amendment 2026-10-20, §15). Session rules are *not* a
474
+ second subsystem: they are `kind: rule` files in the same `rules/<harness>/` tree, with the same
475
+ write-once immutability, the same `list` surface, the same search index. What differs is **how
476
+ they get authored and how they become active**:
477
+
478
+ | Tier | Authored by | Activation | Transport | Carried by |
479
+ |---|---|---|---|---|
480
+ | **project** | `/chapters-rule add`, `chapters_rule_propose` (agent) | human `approve` → per-machine curation fact (§7.2) | synced to the pool | continue **and** fork |
481
+ | **session** | `ADD RULE:` marker (deterministic), enrichment (optional) | **derived** (§7.5) — no fact, no approval | local only; never staged (§7.5) | continue only — dropped by fork (§7.6) |
482
+
483
+ Extra frontmatter on every rule file: `scope: project | session` (absent ⇒ `project`, so every
484
+ pre-existing record keeps working byte-identically), `session: <sessionId>` (session tier only),
485
+ `derivedFrom: human | marker | model`, `pinned: true` (§7.4). Same `category`, `status`,
486
+ `sha256`, provenance chain as before.
487
+
488
+ **Why the split is safe.** A rule that only ever governs one machine's session tree is not shared
489
+ state, so §11.3's blast-radius argument does not apply to it — and the per-machine approval
490
+ boundary of §7.2 is left exactly as the 2026-09-20 amendment set it. Cross-checkout carry of
491
+ *project* rules needed no new mechanism: `projectKey` is UUIDv5 over the canonicalized remote
492
+ (§2.3) and `harnessId` defaults to the hostname, so several checkouts of one repository already
493
+ resolve to one rule namespace on one machine. §7.8 is about making the cases where that identity
494
+ *diverges* loud, not about building propagation.
495
+
496
+ ### 7.2 Lifecycle: proposed → core, approved in-session *(the project tier)*
497
+
498
+ This section governs `scope: project` — the shared, team-visible tier. The session tier's
499
+ activation is derived and needs no approval (§7.5).
500
+
501
+ - **Proposal:** a user command (`/chapters-rule add <category> <text>`) or an agent's async
502
+ derivation from a completed session (P3, always `status: proposed`, provenance-tagged with
503
+ the source session).
504
+ - **Review/approve:** entirely in-session DSH commands — the user never leaves the session,
505
+ and **command output never enters the context window** (verified 2026-09-17: command results
506
+ persist as `command/run` / `command/done` event types — a distinct type, not
507
+ user/assistant/tool — which are neither prefilled to the model nor priced by the compaction
508
+ engine; 24 such pairs observed in the r29 session's durable log from the e2e suite):
509
+ - `/chapters-rule list` (`--proposed`, `--category <name>`) — table: id, status, category,
510
+ one-line text, provenance.
511
+ - `/chapters-rule view <id>` — ONE rule in full: the complete body (untruncated — `list` shows only
512
+ the creation-time title, the first sentence cut at 60 chars), identity, this machine's effective
513
+ status, pin, and for session rules `active`/`reviewOnly`/`basis` (stated vs inferred). A rule that
514
+ cannot be read cannot be approved; added 2026-10-29 at the maintainer's direction (#174).
515
+ - `/chapters-rule approve <id>` — `proposed → core`.
516
+ - `/chapters-rule revoke <id>` — core back to the file, out of the notice.
517
+ - **`core` is the only tier that renders into the continuation notice** (§7.3).
518
+ **Amended 2026-09-20 (§15):** `approve` does not rewrite the rule file — it appends a
519
+ `rule-status` curation fact to the APPROVING machine's own `edits/<harness>/curation.jsonl`.
520
+ Effective status is per-machine (file otherwise `proposed`); a rule affects no other
521
+ machine's notice until that machine's human approves. Reason: authorship partition — a
522
+ shared-file status would let any machine rewrite another's authored bytes, which r37
523
+ showed is exactly where transport bugs live. The human
524
+ in the loop is the one who types `approve`; the git diff of the rule file is the audit
525
+ trail. This is the trust boundary for shared state (§11.3).
526
+
527
+ ### 7.3 The rules block: what a rebuilt head shows
528
+
529
+ One builder renders the rules block (`buildRulesSection`), and **both** places a head is rebuilt
530
+ use it — the continuation notice and the in-place compaction checkpoint (§7.6). The block is O(1)
531
+ in corpus size (§1 invariant 6) and, on an empty or unlinked corpus, renders **zero bytes**
532
+ (`kind: 'none'` → nothing at the call site): load-bearing for the tape corpus, which contains no
533
+ rules block anywhere (§13 P3).
534
+
535
+ Contents (all O(1), §1 invariant 6):
536
+
537
+ - **Active rules, verbatim,** selected by the three classes of §7.4 — pinned, then
538
+ relevance-selected, then index-only. Project tier and session tier render under their own
539
+ headers so the model can tell whose order it is reading.
540
+ - **A withheld line — mandatory.** When the caps drop anything, the block says so with numbers:
541
+ `9 project rules active, 4 shown — /chapters-rule list`. Selection by relevance is legitimate
542
+ (§7.4); *silent* selection is the exact failure this feature exists to remove, because a rule
543
+ the human believes is active and is not, is worse than a rule that was never captured.
544
+ - **The token ceiling stays refuse-don't-truncate:** if the selected set exceeds
545
+ `coreRulesBudgetTokens`, the builder returns `refusal` with per-rule token numbers, the caller
546
+ omits the block rather than clipping (§1.2), and the refusal is reported — never swallowed.
547
+ Omission stays non-blocking: rules are additive, and a broken mirror or an overflowing ruleset
548
+ must not stop a continuation or a compaction.
549
+ - **Precedence line** (§7.7): session rules refine and add; they never override a project rule.
550
+ - **Per-rule warrant** (§7.5, amended 2026-10-21): a session rule renders with how it was
551
+ established — `you said this` (marker), `stated by you, mined from ch.N`, or `inferred from ch.N
552
+ — nobody said it`. Inference is allowed to govern, but never anonymously.
553
+ - **The speak-up line**, present only when a block carries session rules: the agent names by id
554
+ which of them shaped its reply, or says none did. A project-only block keeps its exact pre-#50
555
+ bytes (verify.md R0 pins that), so nothing is spent where nothing is inferred.
556
+ - **Rules category index:** category names + one-line essence + paths, with the instruction
557
+ *load a rule category when this session's topics overlap a category* — i.e., the agent
558
+ reads the full category from the repo when (and only when) relevant.
559
+ - **Search instructions** for the chapters index (one paragraph, pointing at
560
+ `chapters_search` and the shard paths).
561
+ - **`Project: <slug> · <projectKey>` — prominently near the top** (user requirement): the
562
+ agent always knows which corpus it is in and where searches point.
563
+ - The existing sections (chapters list with single-sentence title, short summary, file link,
564
+ **message count** — not token count, since tokenization is not comparable across models —
565
+ and parent/fork links) unchanged.
566
+
567
+ ### 7.4 Topic-matched selection (raised from the old P3b sketch)
568
+
569
+ The record deferred "topic-matched auto-inclusion" because always-render-everything does not scale
570
+ against an O(1) head. With automatic capture the corpus of *active* rules now grows on its own, so
571
+ selection moves from garnish to requirement, and it is built here on machinery that already existed
572
+ purely for pull-based search.
573
+
574
+ Three render classes, in this order, under `projectRulesDisplayCap` / `sessionRulesDisplayCap`
575
+ (count) and `coreRulesBudgetTokens` (tokens):
576
+
577
+ 1. **Pinned** (`pinned: true`, human-set via `/chapters-rule pin`) — always rendered, first.
578
+ Ground rules are *old* by definition, so any scheme that ranks by recency or topicality evicts
579
+ them first; the pinned class is the floor that keeps that from being a surprise.
580
+ 2. **Relevance-selected** — `rules.selectRules`, the ONE selector for what a head shows:
581
+ pinned first, then topicality against a term bag, then the pre-#50 category→id order. The term
582
+ bag is the archived span's stored turn signatures for a rebuilt head and the task query for
583
+ the `chapters_rules` door — both paths share the selector, so the head and the door can never
584
+ disagree about what applies. `search.rankEntries` stays the *corpus* scorer (§8): ranking an
585
+ unbounded archive against a query and selecting a bounded set for one head are different jobs,
586
+ and this round kept one implementation of each rather than bending a rebuilt head to read index
587
+ shards. What is forbidden is a **third** rule-picker living in a door or a checkpoint — a
588
+ source-shape row fails if either stops calling the shared builder.
589
+ 3. **Index-only** — everything else: category + one-line essence + path, loadable. The corpus
590
+ grows without bound; the head does not.
591
+
592
+ `rulesRelevance: false` falls back to the pre-existing deterministic order (category, then id),
593
+ which is what the tapes were distilled against.
594
+
595
+ **Determinism is a requirement, not a preference.** Selection must be a pure function of (rule
596
+ files, stored topic signatures, caps, query) — no wall-clock timestamps, no map-iteration or
597
+ filesystem-readdir order — or two machines disagree about what the agent was told *and* every
598
+ replay prefix becomes unstable. Ranked-by-recency was rejected partly for this reason: recency
599
+ reads the clock.
600
+
601
+ ### 7.5 Session rules: capture and derived activation
602
+
603
+ Two capture paths, deliberately different speeds (this is §6.2's single-slot constraint applied to
604
+ rules, not a new policy):
605
+
606
+ - **Marker — deterministic, instant, zero model tokens (§1 invariant 1).** A line in a
607
+ *human-authored* message beginning `ADD RULE:` (§12 `rulesMarkerPrefix`) becomes an active
608
+ session rule at the existing `turn/end` listener — the same one the topic signatures collect on
609
+ (r35j). It is not a new archive door (§12 of `docs/architecture.md`: five moments, extended in
610
+ place, never copied). The rule body is the human's own words, verbatim.
611
+ - **Enrichment — idle-batched, `rulesEnrichment` default ON.** The §6.1 pass carries a
612
+ `rules: [{ text, category, quote }]` field through that pass's existing ladder: validation
613
+ before commit, one retry, deterministic fallback, provenance chain, and the body-hash guard
614
+ (mining writes its own rule files; it never touches chapter prose). `rules` is **optional in the
615
+ output contract**, so every pre-existing stub, fake and tape still validates, and a malformed
616
+ `rules` value cannot cost a chapter its annotation.
617
+ **What decides whether a mined rule governs is POLICY, not evidence** (Amendment 2026-10-21).
618
+ `rulesAutoActivate` defaults ON: a rule the model inferred from context applies to the session
619
+ tree the moment it is written. With it off, every mined rule is written `reviewOnly: true` and
620
+ stays inert until this machine's `approve` fact says core — activation still rides a curation
621
+ fact, so the write-once file is never rewritten (§1.3).
622
+ **Live proof (#175):** `model-activities-live.spec.ts` puts an explicit standing order in the
623
+ journey's only human message, drains enrichment against the real provider, and asserts the mined
624
+ rule FILE — `scope: session`, `derivedFrom: model`, and the `basis` warrant (`stated`/`inferred`,
625
+ decided by the verbatim-quote gate above; an inferred warrant must render with its "the model
626
+ wrote this" caveat) — then reads the rule back in full through `/chapters-rule view`. Unit rows
627
+ pin the gate; this leg pins the journey, against a real completion.
628
+ **The quote check survives, demoted to a label.** Matching the chapter's human bytes verbatim
629
+ classifies a rule `stated`; not matching classifies it `inferred`; the label is rendered *beside
630
+ the rule text, with the chapter it came from*, and it drives nothing. Assistant prose is not
631
+ admissible even as a label — a "quote" found only in the model's own restatement is `inferred`.
632
+ Why it stopped being a gate is the amendment's subject: requiring a human to have already said it
633
+ makes the tier redundant with the deterministic marker, and the premise of the tier is that the
634
+ human may not know the rule yet.
635
+ Measured before shipping, on three real chapters at the session model (§13 P3c, verify.md R5): 9
636
+ rules captured, 6 stated / 3 inferred, every inference confined to the chapter that had no human
637
+ text at all, and the model's self-declared `basis` less accurate than the check. It also exposed a
638
+ cost fact worth a decision: the hosted route counts reasoning tokens, so mining inside the
639
+ annotation call shares a budget sized for annotation (`ENRICH_MAX_TOKENS`) — `rulesModel` is the
640
+ clean bound, and an unexamined default is not.
641
+
642
+ Three properties worth stating: mining runs **before** the hash-guarded annotation write, so a
643
+ chapter whose enrichment is refused still contributes its standing orders; mining may use a
644
+ **dedicated model** (`rulesModel` / `/chapters-rule model`), which costs one extra bounded call
645
+ per chapter and, when set, replaces rather than supplements the annotation pass's answer; and the
646
+ agent must **name the session rules it applied** in its reply (§7.3) — which is where a wrong
647
+ inference becomes visible to the only party who can revoke it. That is the §1.4 division — model proposes, plugin commits — with the activation gate
648
+ moved to machine-local, where a wrong guess costs one session and not a team's corpus.
649
+
650
+ **Activation is derived, never a curation fact.** A session rule is active iff `scope: session` ∧
651
+ its `session` is on the current session's ancestor path ∧ it is not revoked. No
652
+ `rule-status` entry is written for it: `edits/<harness>/curation.jsonl` travels in the pool, and
653
+ machine-local activation volume does not belong in a file reserved for human status decisions —
654
+ nor should it, since another machine reads only its own harness directory and would see nothing.
655
+ `revoke` on a session rule writes the one exception: a local fact, because the store is write-once
656
+ (§1.3) and a delete would be a rewrite.
657
+
658
+ **Transport exclusion happens at one point.** `planStoreToRepo` is the single store→repo path
659
+ mapper (`rules/<harness>/*.md → rules/<projectKey>/<harness>/*.md`), so it is the single place that
660
+ skips `scope: session`. No provider verb, no sync-loop edit, no second filter — proven over the
661
+ git integration *and* the TreeDX stub, because §5's exit criterion is transport-grade or it is
662
+ nothing. Session bytes therefore never become another machine's `proposed` noise, which is what
663
+ keeps §7.2's "stop re-reviewing a growing list" goal intact for teammates.
664
+
665
+ **Loss is expected and re-derivable.** The store root is gitignored inside a re-clonable directory
666
+ (`docs/architecture.md § Storage`), so deleting a checkout destroys its session rules with
667
+ everything else in it. The marker half re-derives deterministically by replaying the parser over
668
+ the durable log (invariant 1 is what makes that possible); the enrichment half re-derives by
669
+ re-running the pass. The correct description of that event is "the ruleset was rebuilt", never
670
+ "sessions were corrupted".
671
+
672
+ ### 7.6 Carriage: where rules reach the model
673
+
674
+ Rules are delivered at the moments the head is rebuilt, plus on demand — **not** per request:
675
+
676
+ | Door | What renders | Mechanism |
677
+ |---|---|---|
678
+ | Continuation notice | project + session | `rulesSection` at the four door call sites (tools) — already shipped for project |
679
+ | Fork / branch notice | project only | the same `intent` axis that already decides plan bytes (§9–13 of `docs/plans-carriage.md`): `branch` ⇒ zero session bytes |
680
+ | In-place checkpoint | project + session | `rulesCarry`, the third lead section in `planSummarize`, beside `planCarry` and `plot` — both the composed and legacy paths |
681
+ | On demand | assembled set | `chapters_rules` tool (§8): verbatim text within budget + the withheld line |
682
+
683
+ **Why not "bundled with every request".** Two measured reasons, not stylistic. (a) Invariant 3:
684
+ the system prompt is process-global and `systemPrompt` is excluded from our injection list on
685
+ purpose — a per-session block cannot ride it, and the append-as-section precedent (`dsh-session-fork`)
686
+ was reviewed and rejected here. (b) Cache: on llama.cpp, the target hardware, a head-position
687
+ replacement reuses **nothing** (E3, r23/r28: `cacheReuse 0`), so a ruleset that churns mid-session
688
+ is a cache-wipe machine. Between rebuilds the human's original bytes are still live in the surface
689
+ anyway — they only genuinely die when the surface is replaced, and that is where they are now
690
+ re-stated. Plans arrived at the same conclusion for the same reason.
691
+
692
+ Empty set ⇒ the checkpoint carries zero rule bytes, which is what protects every already-committed
693
+ plot-carrying checkpoint (`src/engine-core.ts` records exactly that hazard).
694
+
695
+ ### 7.7 Precedence and conflicts
696
+
697
+ Two live tiers means two sources of "must", and a small model degrades badly on a contradiction,
698
+ so precedence is stated in the block rather than left to the reader:
699
+
700
+ - **Project rules are the team's order. Session rules refine and add; they never override.**
701
+ - A session rule that conflicts with an active project rule is *rendered and flagged*, not
702
+ resolved silently — the human's options are to promote a fix to the project rule or revoke the
703
+ session rule.
704
+ - **`/chapters-rule conflicts`** spends one bounded `ctx.llm` stream call over the assembled set,
705
+ reusing the conversation's own system prompt as the cache-friendly prefix (`docs/contract.md`),
706
+ and returns a **report** of contradictory pairs with rule ids. Like `elicitedPlot`, this is an
707
+ explicit, config-gated exception to zero-inference. It is never a mutation: rules are write-once
708
+ (§1.3) and the only sanctioned change is a curation fact. Enrichment may flag the same pairs in
709
+ its own pass; neither path edits a rule.
710
+ - Command output never enters the context window (§7.2 verified), so reviewing conflicts costs a
711
+ human no session tokens.
712
+
713
+ ### 7.8 Identity must fail loudly
714
+
715
+ The design goal "project rules carry across my checkouts of this repo" holds because identity is
716
+ remote-derived (§2.3). Where it does *not* hold, silence is the bug — ungoverned must never look
717
+ like governed:
718
+
719
+ | Divergence | Consequence | What the plugin must say |
720
+ |---|---|---|
721
+ | No readable remote ⇒ `path-derived` key | that checkout has a **private** rule namespace | `0 project rules — this checkout resolves project <key> from path <cwd>; link it with /chapters-link` |
722
+ | No `origin`, another remote listed first | key derives from the fork ⇒ a forked pool | report the remote string actually used |
723
+ | Linked via TreeDX rather than git | key is the **pool location** (`treedx/<host>/<repo>`), not the workspace remote | report `projectKeySource` so git/treedx keyed checkouts are visibly different projects |
724
+ | Fresh checkout, unlinked or not yet synced | no project ⇒ no rules at all | same explicit zero line, plus `/chapters-status` sync state |
725
+
726
+ `projectKeySource` (`override | remote | path-derived`) rides **`/chapters-status`** as an
727
+ `Identity:` line — naming the source AND where it came from (remote string / override / literal
728
+ path) — and the divergence branch is its own warning: `⚠ THIS CHECKOUT RESOLVES A DIFFERENT
729
+ PROJECT KEY … rules and chapters approved elsewhere will NOT appear here`. `/chapters-link`'s view
730
+ reports the same fact. The continuation head carries the zero-rules statement **only when the
731
+ session tier is governing that head**: bolting an "unlinked" line onto every unlinked workspace's
732
+ notice would spend real bytes on a warning most sessions do not need, while a head that already
733
+ carries local rules is exactly where a silent zero gets misread as "the team approved nothing".
734
+ `/chapters-rule active` prints the tier counts on demand, and `chapters_rules` names a missing
735
+ project in its note. A session-scoped archive depends on none of this — which is why an unlinked
736
+ checkout still gets its own rules.
737
+
738
+ ### 7.9 Command surface, and what the agent is told
739
+
740
+ **Principle: the machinery is invisible; the query is encouraged.** Capture, activation, budgeting
741
+ and carriage happen without the agent doing anything — no tool it must remember to call, no
742
+ command it must issue, no bookkeeping it can get wrong. What it *is* told, in every rebuilt head,
743
+ is to **look the rules up before starting a task**: `chapters_rules` with the task description,
744
+ named in the notice's `## Finding things` block (§8). Everything else about rules reaches it as
745
+ prose it simply reads.
746
+
747
+ | Surface | Tier | Notes |
748
+ |---|---|---|
749
+ | `/chapters-rule add <category> <text>` | project | unchanged — the only human door that writes shared rule bytes |
750
+ | `/chapters-rule list [--scope project\|session] [--proposed] [--category <c>]` | both | table: id, scope, status, category, one-line text, provenance, source seq |
751
+ | `/chapters-rule active` | both | **exactly what is or will be rendered**, with the withheld count and why — the command that makes §7.3's honesty checkable |
752
+ | `/chapters-rule approve / revoke <id>` | project (approve) · both (revoke) | per-machine curation facts (§7.2, §15 2026-09-20) |
753
+ | `/chapters-rule pin / unpin <id>` | both | the §7.4 always-shown class; **human-set only** — an auto-pinned rule is an ungoverned always-shown rule |
754
+ | `/chapters-rule conflicts [--model <pin>]` | both | report-only (§7.7) |
755
+ | `chapters_rule_propose` (tool) | project | agent-facing, stays `proposed` and machine-inert until a human approves — unchanged contract, do not widen |
756
+ | `chapters_rules` (tool) | both | the assembly door: verbatim selected set within budget + the withheld line |
757
+
758
+ Encouragement lives in the **notice**, deliberately: notice prose costs nothing against the tape
759
+ corpus (measured 2026-10-11 — no committed tape prefix contains a seeded notice), while a persona
760
+ or tool-description edit forces a re-record of every affected project (§13). A new tool or tool
761
+ parameter does change the header, so this feature's schema edits are sequenced into **one**
762
+ re-record rather than scattered across several.
763
+
764
+
765
+ ## 8. Search: `chapters_search`
766
+
767
+ ### 8.1 Contract
768
+
769
+ ```
770
+ chapters_search { query: string, maxTokens?: number, topic?: string, crossProject?: boolean }
771
+ → { matches: [...], shown: n, total: m, budget: {requested, used, remaining} }
772
+ ```
773
+
774
+ - `maxTokens` is **the agent's request** (user requirement: the model knows its own window;
775
+ the plugin measures and packs to it, the same way the continuation budget preflight does).
776
+ Default: small (config), overridable upward by the agent.
777
+ - **Deterministic scorer** (no model, reproducible, transparent):
778
+ - term hits on **topic (×2 weight), title, summary, category** (rules: category + text);
779
+ - **recency** boost: exponential decay on chapter date;
780
+ - small bonus for `kind: rule, status: core`.
781
+ - **Output lines:** `score | date | kind | title | path (topics: …)` — packed until
782
+ `maxTokens` is spent, most-relevant first; the response reports `total` so the agent can
783
+ re-request with a larger `maxTokens` to page (transparent, no hidden truncation).
784
+ - **`topic: <name>`** reads an entire index shard (a bounded, alphabetical file).
785
+ - **Default filter: the caller's `projectKey`** — cross-project search is an explicit
786
+ `crossProject: true`, so a small-model session never wanders into a foreign corpus by
787
+ accident.
788
+ - **Zero new trust surface:** every `path` returned is a plain file the existing `read` tool
789
+ opens (chapters in `.dsh-chapters/`, shards in `.dsh-knowledge/index/`). The search tool
790
+ adds a scorer and a formatter; it adds no new way to read.
791
+
792
+ **`chapters_rules` — the head's selector, assembled (§7.4, §7.9).** One door whose output is *the
793
+ text of the rules that apply to this task*, verbatim, within budget, plus the per-tier counts it
794
+ withheld. It calls `renderRulesBlock` — the renderer the continuation notice and the compaction
795
+ checkpoint also use — and nothing else; a rule-picker living in a door or a checkpoint is forbidden
796
+ by the same argument that made `exit_plan_mode` and `chapters_continue` share one builder. §7.4
797
+ keeps `rankEntries` (corpus ranking) and `selectRules` (head selection) as one implementation of
798
+ each genuinely different job.
799
+
800
+ - **It reads both corpora, which is where the trap is.** Project rules come from the pool mirror
801
+ under `rules/<projectKey>/<harness>/`; session rules live in the **workspace store** under
802
+ `rules/<harness>/` and are never staged (§7.5), so they carry no `projectKey` path prefix. The
803
+ `projectKey` prefix filter above must therefore not be the only scope for the session tier, or
804
+ session rules become invisible to the very door whose job is to find them — a silent zero, the
805
+ failure class §7.8 exists to eliminate. Scoping for session rules is by the ancestor-path
806
+ activation predicate instead, never by path prefix.
807
+ - **It is the encouraged step, not a required one.** Capture and carriage never depend on it
808
+ (§7.9); the notice names it in `## Finding things` as the thing to run before starting a task.
809
+
810
+ ## 9. Redaction: minimal, deterministic, marked
811
+
812
+ User position, adopted: **credentials only, deterministic patterns only, everything else
813
+ byte-identical** — the character of the conversation is historical value and stays intact.
814
+
815
+ - **Patterns (deterministic):** `AKIA…` (AWS), `sk-…` (OpenAI-shape), `ghp_/gho_/
816
+ github_pat_`, JWT `eyJ…` shape, `Authorization: Bearer …`, `token=…` in URLs, PEM private
817
+ key blocks. Deliberately conservative; new patterns are config additions, not magic.
818
+ - **Marker format:** `⟦redacted:credential sha256=8f3a1c2b⟧` — (a) obviously not a real value
819
+ to any model reading the chapter, (b) **deterministic per secret** (same secret → same
820
+ marker, so diffs and cross-chapter greps stay stable), (c) leaks nothing but a prefix hash,
821
+ which is also what lets a user ask "which secrets touched my sessions" without exposing
822
+ them.
823
+ - **Applied once, at the archive chokepoint** — render → redact → write — covering chapters
824
+ *and* artifacts, so a value is redacted at the moment it leaves the session log, wherever it
825
+ appears.
826
+ - **Honest limit:** pattern-based redaction is a speed bump, not a guarantee. The doc that
827
+ ships with the repo (and the notice) says the corpus is **plaintext memory**: per-project,
828
+ private, user-created, with a known-small redaction carve-out.
829
+
830
+ ## 10. The two-layer index
831
+
832
+ ### 10.1 Layer 1 — editable, per-machine, append-only
833
+
834
+ `edits/<harness-id>/curation.jsonl` — one JSON line per index fact, **one writer per file,
835
+ ever** (§3.3), zero conflicts by construction:
836
+
837
+ ```json
838
+ {"type":"topic-alias","from":"auth","to":"authentication","author":"plugin",
839
+ "reason":"co-occurrence in 14 chapters","session":"…","at":"…"}
840
+ {"type":"group","chapters":["…a","…b"],"label":"jwt-refresh-work","author":"user","at":"…"}
841
+ {"type":"weight","topic":"auth","weight":2,"author":"user","at":"…"}
842
+ ```
843
+
844
+ Fact types: `topic-alias` (merge/rename), `topic-pin`, `group` (sibling chapters that
845
+ belong together — the semantic grouping that composition cannot do alone), `weight`,
846
+ `note`, and the two rule facts (§7.2, §7.4): `rule-status` — per-machine approve/revoke,
847
+ the 2026-09-20 amendment's whole mechanism — and `rule-pin`, the always-rendered class,
848
+ which rides a fact for the same reason status does: a rule file is write-once and the
849
+ append-only rule has no exceptions. Authors are `plugin` or `user` (user entries via commands, P3). This is the
850
+ "improve over time" layer: curation accumulates as facts, and the derived index reflects
851
+ them — performance improves without anyone hand-editing, and every fact is inspectable and
852
+ revertable in git.
853
+
854
+ ### 10.2 Layer 2 — compressed, derived, incremental
855
+
856
+ `index/<topic-shard>.md` — one shard per canonical topic (alphabetical segmentation:
857
+ bounded files, and writes from different chapters usually touch different shards). Shard
858
+ content: a header line (topic, count, last-updated) then one line per chapter:
859
+ `date | title | path | (kind) | recency-hint`.
860
+
861
+ **Build is incremental** (user requirement: generation time must not scale with the corpus):
862
+ `manifest.json` records, per input file, the content hash last processed. A build touches
863
+ only (a) new chapters/artifacts/collections and (b) new curation entries since the last
864
+ build — O(delta). Because inputs are append-only, "changed" ≈ "new" in practice.
865
+
866
+ **Full rebuild stays possible** (deterministic from the tree) as the *recovery* path — a
867
+ machine with a lost manifest regenerates, and any machine can repair a divergent derived
868
+ file by rebuild-and-commit rather than merge-resolving (§3.3). Incremental is the fast path;
869
+ rebuild is the repair path.
870
+
871
+ ## 11. Trust and security
872
+
873
+ ### 11.1 Credentials
874
+
875
+ Scoped, revocable, per-repo, per-harness (§2.1). No SSH. A compromised harness exposes at
876
+ most one repo token, revocable in one click.
877
+
878
+ ### 11.2 Data exposure
879
+
880
+ The corpus is plaintext memory (chapters are verbatim by design). Mitigations are the
881
+ boundaries, not illusion: per-project repos, private, user-created; redaction carve-out
882
+ (§9); per-harness namespacing so one machine's sessions are always attributable.
883
+
884
+ ### 11.3 The blast-radius rule
885
+
886
+ Everything in the corpus that a *model* can influence and that other machines will *follow*
887
+ needs a human gate:
888
+
889
+ - **Chapters:** low blast radius — evidence, provenance-tagged, attributable. No gate.
890
+ - **Topics/vocabulary:** medium — affects search, not behavior. Plugin-autonomous with
891
+ git-visible curation entries (§6.3).
892
+ - **Rules:** high — they are prompts by another name, shared across machines. **Human
893
+ `approve` is mandatory** (§7.2). A proposed rule that is never approved affects nothing.
894
+ - **Session rules** (§15 amendment 2026-10-20) are the deliberate exception, and the exception is
895
+ *narrowed by scope, not by gate*: machine-local, never staged to the pool (§7.5), permanently
896
+ active for one session tree, and visible/reversible with one command (`active`, `revoke`).
897
+ Blast radius is that tree. What keeps it honest is structural, not evidentiary (§7.5, amended
898
+ 2026-10-21): machine-local scope and never staged, a per-chapter cap of 3 plus per-tier display
899
+ caps, the warrant rendered beside every rule with its chapter, the agent naming which rules it
900
+ applied, one-command `active`/`revoke` review, `rulesAutoActivate` for anyone who wants
901
+ review-before-trust, and a session rule never overriding a project rule (§7.7). What is *not*
902
+ permitted is a session rule migrating into the shared tier on its own — promotion is a human act,
903
+ exactly as approval is.
904
+
905
+ Git history + provenance makes every tier auditable and every mistake revertible.
906
+
907
+ ## 12. Configuration surface
908
+
909
+ No hardcoded tunables (existing hard rule). New config, all with defaults documented in
910
+ `dev/` templates:
911
+
912
+ | Key | Meaning | Default |
913
+ |---|---|---|
914
+ | `knowledgeRemote` | the project's repo URL (or `/chapters-link`) | none (local-only) |
915
+ | `projectKeyOverride` | wins over §2.3 derivation | none |
916
+ | `mergeThreshold` (τ) | collection-merge overlap, §4.2 | tuned in P1 |
917
+ | `chapterLimit` | max chapter size (tokens, heuristic) before forced split | 8000 (today's `chapterTokenTarget`) |
918
+ | `coreRulesBudgetTokens` | cap on the notice's core-rules section | refused-with-numbers on overflow |
919
+ | `syncDebounceMs` | coalesce window for pushes | 30000 |
920
+ | `toolResultArtifactTokens` | arrival-time artifacting floor (architecture.md amendment 2026-09-19): tool results at/above this many estimated tokens are stored as artifacts and reference-stubbed BEFORE the next request composes — the blob never enters any prompt | 8000 |
921
+ | `elicitedPlot` | when a compaction region holds no model-authored `PLOT:` note, spend ONE bounded stream call (≤8K chars in, ≤220 tokens out) to mint the working plot carried into the checkpoint; the approved, config-killed exception to zero-inference | true |
922
+ | `enrichment.enabled` / `.model` | per-harness enrichment toggle + model pin (built flat: `enrichmentEnabled`, `enrichmentModel`) | enabled, conversation model |
923
+ | `enrichmentMaxTokens` | One auxiliary completion budget (annotation+mining, conflicts). Must cover **reasoning** tokens, not just the visible answer — measured mining usage 1.1K–3.2K per chapter. The route measured here ignores `max_tokens`, so this is headroom for strict providers, not a live limit. No call site keeps a literal | 4000 |
924
+ | `enrichment.trigger` / `.idleMs` / `.batchCap` | P2 queue policy seam (built: `enrichmentTrigger/enrichmentIdleMs/enrichmentBatchCap`); realm owns auto, host plane is manual-only; `/chapters-enrich model` overrides durably | both / 60000 / 5 |
925
+ | `vocabulary.*` | built flat: `vocabApply` (false = **shadow**: candidates reported in the sync status line, nothing written), `vocabCoMin` (3), `vocabOverlapMin` (0.5); applied merges land as §10.1 `topic-alias` facts under `edits/<harness>/`; `topics/vocabulary.json` derived | shadow / 3 / 0.5 |
926
+ | `coreRulesBudgetTokens` | P3 §7.3 cap on the notice's core-rules block; overflow refuses with per-rule numbers | 1200 |
927
+ | `sessionRulesEnabled` | §7.5: the deterministic `ADD RULE:` capture path (zero model tokens) | true |
928
+ | `rulesMarkerPrefix` | §7.5 capture marker; matched at line start of **human-authored** messages only | `ADD RULE:` |
929
+ | `rulesEnrichment` | §7.5 model-derived capture: rides the §6.2 idle batch, never per-turn | true |
930
+ | `rulesAutoActivate` | §7.5 (amended 2026-10-21): may a mined rule govern on arrival? It **may**, by default — inferring what nobody said is the tier's purpose. Off ⇒ mined rules are `reviewOnly` and inert until `approve` | true |
931
+ | `rulesModel` | §7.5: `'provider/model'` for a dedicated mining call — one extra bounded call per chapter, replacing the annotation pass's answer. `''` ⇒ mine inside the enrichment call, at no extra inference cost. Runtime override `/chapters-rule model` (settings key `rules.model`) | `''` |
932
+ | `rulesMinedPerChapter` | §7.5 cap on mined rules per chapter — the cap counts rules **written**, not candidates examined, so a duplicate cannot eat a chapter's budget | 3 |
933
+ | `projectRulesDisplayCap` / `sessionRulesDisplayCap` | §7.4 per-tier count caps on rules **rendered** into a rebuilt head; anything dropped is announced by count, never silently | 12 / 12 |
934
+ | `rulesRelevance` | §7.4 relevance selection on; off ⇒ deterministic category→id order (the order tapes were distilled against) | true |
935
+ | — | §7.7 `/chapters-rule conflicts` has **no key of its own**: one bounded stream call on the conversation route the session already has (so it rides that route's cached prefix), report-only. No route yet ⇒ the verb refuses honestly. A gate of its own was rejected: a human types the verb, so `elicitedPlot`'s reason for a switch does not apply | — |
936
+ | `rulesCoreBonus` | P3 §8 query-time rank bonus for rules effective-core on this machine (never baked into shared shards) | 0.15 |
937
+ | `redaction.patterns` | additive to the built-in list (§9) | built-ins |
938
+ | `search.defaultMaxTokens` | default pack size for `chapters_search` | small, config |
939
+ | `knowledgeProvider` | transport policy (Amendment 2026-09-21, transport): `auto` dispatches on the remote scheme; an explicit `git`/`treedx` must match it — contradictions refuse, nothing falls back into a wrong pool | auto |
940
+ | `treedxFetchTimeoutMs` / `treedxWorkspaceTtlSeconds` / `treedxLeaseRetries` / `treedxLeaseRetryDelayMs` | TreeDX transport tunables (provider.md); lease retries bound the one-writable-lease-per-branch contention window | 15000 / 900 / 3 / 1000 |
941
+
942
+ ## 13. Phasing (each phase independently useful)
943
+
944
+ **P1 — Transport + deterministic corpus (zero new inference).**
945
+ Clone/sync loop (§3, §5); `projectKey` (§2.3); per-turn signatures at `turn/end` (§4.1);
946
+ topic-sequential composition replacing size-only splitting (§4.2–4.4) for the fork path
947
+ first (compaction spans use the same composer); derived sharded index + incremental manifest
948
+ (§10.2); `chapters_search` (§8); redaction at the archive chokepoint (§9); notice v2 —
949
+ `Project:` line, message counts, search instructions (§7.3).
950
+ *Exit criterion: two machines, one repo; a fork on machine A sees machine B's chapters via
951
+ search; sync failure degrades to local-only without breaking the fork.*
952
+
953
+ **P2 — Model enrichment + emergent vocabulary.**
954
+ Idle-batched enrichment pass (§6.1–6.2) with validation/fallback; provenance frontmatter
955
+ final; self-bootstrapping vocabulary + curation entries (§6.3); Layer 1 edit types finalized.
956
+ *Exit criterion: enriching a corpus is idempotent and re-runnable; vocabulary merges are
957
+ git-visible and revertible; disabling enrichment leaves a fully working corpus.*
958
+
959
+ **P2 STATUS (2026-09-20): BUILT.** The ladder (slice → validate → retry-once → keep
960
+ deterministic; `src/enrich.ts`), queue with the pluggable trigger seam (`both`/60s defaults,
961
+ realm owns auto, host is manual + `/chapters-enrich run|model|report`), provenance chains +
962
+ the body-hash-guarded frontmatter write as the ONLY sanctioned chapter mutation
963
+ (`enrich-store.ts`), the vocabulary canonicalizer shadow-by-default with applied merges as
964
+ git-visible `topic-alias` entries + derived `topics/vocabulary.json` (`vocabulary.ts` in the
965
+ sync pass), and index-level fragment stitching (pure read-view `stitchFragments` — adjacent
966
+ legacy fragments cohere into one search entry citing all members, nothing merged on disk).
967
+ Idempotent re-run / kill-switch parity / never-blank failure: unit + integration; the `enrich`
968
+ e2e project replays the whole ladder through the model tape. CLOSED 2026-09-20: two-machine grade proven (two-machine-enrich test — which caught and fixed r37, a commit-invisible-modification bug in the transport), and the four-project replay chain is green with zero misses.
969
+
970
+ **P3 — Rules.**
971
+ Rules-as-chapters (`kind: rule`); the in-session command family `/chapters-rule
972
+ list|add|approve|revoke` (§7.2 — command output verified context-free); core-rules section +
973
+ category index in the notice (§7.3); agent-side rule proposals (async, always `proposed`);
974
+ `/chapters-topic` escape hatch; topic-matched selection (§7.4 — deferred as P3b in this draft,
975
+ built later under P3c, not by design but by sequencing).
976
+ *Exit criterion: a rule proposed by a machine affects no other machine until a human types
977
+ `approve`; the core set overflows by refusing with numbers, never clipping.*
978
+
979
+ **P3 STATUS (2026-09-20): BUILT.** `src/rules.ts` (write-once proposed files, per-machine
980
+ status resolution, budgeted section with refuse-with-numbers) + `rules-commands.ts`
981
+ (`/chapters-rule add|list|approve|revoke`, host plane) + `chapters_rule_propose` tool +
982
+ notice seam (`assembleNotice` rulesSection — byte-identical ABSENT, golden-tested so the
983
+ tape corpus is untouched by construction) + transport (rules tree publishes; commit is
984
+ attempted even at copied===0 so fact-only passes travel — the change that made approvals
985
+ shareable) + scorer (category 1.5, rule bodies, per-machine core bonus at query time).
986
+ Exit-criterion tests: unit ('machine B sees nothing core'), transport-grade (three machines
987
+ over a real pool), e2e (`rules` project: UI add → approve → fork → the child's own log
988
+ carries the rule verbatim). §7.4 selection was left unbuilt in that round by design — the corpus
989
+ of active rules did not yet grow on its own.
990
+
991
+ **P3c — Two-tier rules (issue #50): BUILT, both capture speeds (2026-10-20).** §7.1's `scope`/`session`/
992
+ `derivedFrom`/`pinned` frontmatter (absent `scope` ⇒ `project`, so every pre-existing rule record
993
+ keeps working byte-identically); `ADD RULE:` capture at the existing `turn/end` listener;
994
+ `rulesEnrichment` capture (§7.5's second speed; governed on arrival by default per
995
+ Amendment 2026-10-21, the quote demoted to a rendered warrant); session-scope exclusion in
996
+ `planStoreToRepo`; the three render classes and per-tier caps in `buildRulesSection`; the third
997
+ checkpoint lead section; `intent` filtering at the four door sites; `chapters_rules`; the
998
+ `active`/`pin`/`conflicts` verbs; and §7.8's loud-identity lines.
999
+ *Exit criteria (each must be a red row before it is a fact):* an in-place compaction renders active
1000
+ rules verbatim into the checkpoint; `chapters_continue` carries project **+ session** while
1001
+ `chapters_fork` carries project only; `ADD RULE:` yields an active rule at **zero** model tokens;
1002
+ no session rule file appears in the pool over **either** provider; an unlinked or `path-derived`
1003
+ checkout says it rendered zero rules; the empty set still renders byte-identical ABSENT; selection
1004
+ is byte-stable across two machines with the same inputs.
1005
+ Build status, honestly split (each ✅ is a red-then-green row, not an intention):
1006
+ ✅ `scope`/`session`/`derivedFrom`/`pinned` frontmatter with absent-means-`project` byte-compat;
1007
+ ✅ tier-aware `buildRulesSection` with pinned→relevance→category/id selection, per-tier caps and the
1008
+ announced withheld count; ✅ `ADD RULE:` capture at the existing `turn/end` listener (zero model
1009
+ tokens); ✅ the one `planStoreToRepo` exclusion over **both** providers; ✅ `rulesCarry` as the third
1010
+ checkpoint lead section on both paths; ✅ `intent` filtering at the doors through one
1011
+ `renderRulesBlock`, with a source-shape row forbidding a door-local picker; ✅ `chapters_rules`;
1012
+ ✅ `active`/`pin`/`unpin`/`--scope`/`conflicts` and revoke-on-session; ✅ §7.8 identity lines at
1013
+ `/chapters-status` and `/chapters-link`. ❌ the §6.1 `rules: []` enrichment field and its quote gate.
1014
+ ❌ the browser plane: `npm run test:e2e:replay` **refuses by name** on the pinned host (it predates
1015
+ preset declaration rows — `tests/e2e/boot.ts:130`), so the `rules` journey and the R-rows over a
1016
+ live model are unverified, and the new tool schema is a standing re-record owed the moment
1017
+ `dev/dsh-host-lock` moves to 0.2.x. Sequencing then stays as planned: **one** whole-project
1018
+ re-record, never a series of per-spec ones (the 2026-09-23 lesson: a tape records the context it was
1019
+ distilled in).
1020
+
1021
+ ## 14. Open risks (honest register)
1022
+
1023
+ | Risk | State |
1024
+ |---|---|
1025
+ | Pure-JS git SDK performance at repo scale; SSH-only remotes | accepted (user-directed); CLI fallback config-gated |
1026
+ | Deterministic topic quality on pure-prose (non-code) sessions | degrades to today's time/size behavior; enrichment improves recall later |
1027
+ | Pattern redaction is a speed bump, not a guarantee | documented in-repo; patterns are config-extensible |
1028
+ | Single-slot model server contention | enrichment is idle-batched and per-harness disableable; signatures are model-free |
1029
+ | Cross-model topic vocabulary drift | canonicalizer + provenance + re-run (§6.3) — mitigated by design, observed in practice at P2 |
1030
+ | `mergeThreshold` mis-tune at archive time (composition is archive-time-only, §4.4) | signatures are stored with provenance; tune for future archives, inspect past ones |
1031
+ | TreeDX service down or token expired (Amendment 2026-09-21, transport) | same §5.3 degradation (offline mirror, deferred push, auth re-link surfaced with numbers); no corpus loss |
1032
+ | TreeDX corpus pull is per-file (`paths/list` + `files/read`): N+1 requests at large pools | acceptable at per-project corpus sizes (the record's own scaling premise); snapshot/`context` APIs are the Phase-F optimization |
1033
+ | P3c: a model-captured rule encodes a **mis-generalization** permanently for the session tree ("never run two GPU suites at once" → "never run tests concurrently") | narrowed by the verbatim-quote activation gate (§7.5), by scope (local, never the shared tier), and by one-command visibility (`active`, `revoke`); the residual risk is a wrong rule you did not look at — which is why the count is capped and the withheld line is mandatory |
1034
+ | P3c: relevance selection **ranks out the load-bearing rule**, reintroducing "lost in the noise" inside the fix for it | the pinned class is never subject to selection (§7.4); the withheld count renders even when nothing is selected |
1035
+ | P3c: a new tool/param changes every replay prefix | one whole-project re-record, sequenced deliberately; notice prose stays free (measured 2026-10-11) |
1036
+ | P3c: per-checkout identity divergence silently forks a project's rule namespace (§7.8) | stated, not assumed: `projectKeySource` reported in the zero-rules line, `active`, and `/chapters-status` |
1037
+
1038
+ ## 15. Amendments
1039
+
1040
+ This record governs implementation. An amendment is a **human decision** — discussed,
1041
+ accepted, and edited in with a commit message stating the reason — because the record's value
1042
+ is that every architectural fact in it was chosen, not generated. Machine-derived knowledge
1043
+ lives in the *corpus* (chapters, curation entries, vocabulary with provenance); machine
1044
+ opinions about *this document* do not.
1045
+
1046
+ ### Amendment 2026-09-21 — transport is pluggable; git is one provider
1047
+
1048
+ Accepted with the human-approved provider-plugin plan. §3.1/§3.2 stand as the git
1049
+ provider's rules; they are now one implementation behind a seam
1050
+ (`src/provider.ts`, contract in [provider.md](provider.md)). The six transport verbs
1051
+ `runSync` already injected (`GitDriver`) define the provider interface; a second
1052
+ implementation, `kind: 'treedx'`, transports the same mirror layout over TreeDX's
1053
+ no-clone HTTP API (`src/treedx/`), selected by the `treedx+<url>/<repo>` remote
1054
+ scheme (`/chapters-link mode 4`; §12 keys `knowledgeProvider`, `treedx*`). What the
1055
+ amendment changes in the record, precisely:
1056
+
1057
+ - §3.1 holds unchanged: the store is truth, the mirror is transport — in TreeDX
1058
+ mode the mirror is a provider-materialized plain directory instead of a clone.
1059
+ Search, rules, index build, notice assembly, and the `read` path see no
1060
+ difference.
1061
+ - §5.2's divergence rule generalizes: any head movement detected at pull or commit
1062
+ routes through the same rebuild-the-mirror-from-the-store path. Never a merge.
1063
+ - §2.3's identity rule extends: TreeDX keys derive from the canonical
1064
+ `host[:port]/repo` string under a `treedx/` namespace prefix, so a service at a
1065
+ git-colliding URL is never the same pool.
1066
+ - §11.3's blast-radius gate is provider-independent: per-machine rule approval is
1067
+ a local curation fact — no server, federated or otherwise, can pre-approve.
1068
+ - §5.3's degradation semantics are identical, plus one auth class (a rejected
1069
+ token surfaces a re-link instruction; it never enters the conversation path).
1070
+
1071
+ The TreeDX backend's API-level facts (and what remains [LIVE-GATED] pending a real
1072
+ container) are pinned in [spikes/treedx/FINDINGS.md](../spikes/treedx/FINDINGS.md);
1073
+ transport-grade proof runs over the stub service (`treedx-{provider,sync,link}.test.ts`)
1074
+ and the live exit criterion over `treedx-live.test.ts` when `scripts/treedx-local.sh`
1075
+ brings the service up.
1076
+
1077
+ ### Amendment 2026-09-21 — host-injected context is not conversation
1078
+ Accepted with the same plan (the human directive: the knowledge layer must not
1079
+ depend on any project's instruction files — every target project ships its own,
1080
+ unpredictably). The archive guarantee restates: **every byte of the CONVERSATION
1081
+ remains retrievable** in chapters or artifacts; host-injected context — project
1082
+ instruction files (`agent-instructions` events and the `instructions from:`
1083
+ reminder form), the runtime-context snapshot, the skill catalog, and this plugin's
1084
+ own TOC notice — is re-derivable project state and is EXCLUDED at the render
1085
+ chokepoint (`src/injections.ts`), each omission leaving a countable marker.
1086
+ `stats.messages` counts conversation only, so TOC `(N msgs)` stays honest. The
1087
+ durable session log is untouched by this (invariant: append-only) — originals live
1088
+ in the parent session; the screen is archive-time bytes, applied before redaction
1089
+ (§9) at the same chokepoint. The signature layer has applied the same predicate
1090
+ since r28; the two now share one implementation so they can never drift. The
1091
+ model-tape masking rule (AGENTS-BLIND, 2026-09-20) is the test-side half of the
1092
+ same position: development artifacts, never product facts.
1093
+
1094
+ ### Amendment 2026-09-20 — rule approval is a per-machine curation fact
1095
+
1096
+ Accepted as the P3 plan's single decision point (human-approved with the plan). §7.2's
1097
+ file-status rewrite replaced by `rule-status` facts in each machine's own edits file
1098
+ (see §7.2 note for the reason). Everything else in §7 stands.
1099
+
1100
+ ### Amendment 2026-10-10 — plans are documents of a third kind
1101
+
1102
+ Human decision, recorded here because it widens the corpus: besides transcript chapters and curated
1103
+ rules, a workspace's shared knowledge now also carries **plan documents** — one file per
1104
+ `exit_plan_mode` submission, `kind: plan`, written into `chapters/` so the §4 transport, the §5
1105
+ mirror and the §6 index carry and rank them with no special casing.
1106
+
1107
+ Rules of the amendment, all enforced by tests (verify.md P12–P14):
1108
+
1109
+ - **A plan document's body is the plan, byte for byte.** It is never a summary of one, and the
1110
+ enrichment ladder (§6.4) refuses to annotate it: a directive is approved text, and paraphrasing
1111
+ an order is the drift the whole store exists to prevent. The guard reads the file's own
1112
+ `kind: plan` frontmatter, so renaming cannot disarm it.
1113
+ - **Superseding never overwrites.** A revised plan is a new document; the earlier one keeps its
1114
+ bytes and its status changes only in the sense that the newest submission is operative. Within
1115
+ their classes the append-only rules stand; the one sanctioned deletion is operator removal of bad
1116
+ chapters (#255), which takes a removed chapter's plan documents and orphaned artifacts with it.
1117
+ - **Numbering is its own** (`nextPlanNumber`, registry-allocated under a distinct attempt key,
1118
+ never a directory scan), so a plan never consumes a chapter number, never reorders the TOC, and
1119
+ never collides with a concurrent sibling. Files are `plan-<NNN>-<slug>.md` precisely so a listing
1120
+ cannot read as two chapters claiming the same number.
1121
+ - **Redaction is unchanged and applies identically** (§9): the chokepoint runs over plan text on the
1122
+ way to both the blob and the document, so a credential pasted into a plan never reaches the pool.
1123
+ The one asymmetry a reader should know: a plan is *deliberately* a human-facing directive, so it
1124
+ keeps its own `sha256` (the blob's hash, identical bytes) plus `bodySha256` (the §2.2 anchor
1125
+ convention) — two handles, one purpose each, provably the same content.
1126
+ - **Host-injected context is still not conversation** — but with the one named exception recorded in
1127
+ `docs/plans-carriage.md` § 13: the harness's own `plugin: plan-mode / form: notice` mode-flip
1128
+ sentence is session state worth keeping, so it archives verbatim. Instruction files, runtime
1129
+ snapshots and skill catalogs screen exactly as before, and the topic-signature layer (§4.1)
1130
+ keeps the strict predicate so injected text cannot move a chapter boundary.
1131
+
1132
+ Design detail and measurements: `docs/plans-carriage.md` §§ 9–13.
1133
+
1134
+ ### Amendment 2026-10-11 — the index answers at home, and the child is told the verbs
1135
+
1136
+ Human directive: *"make sure everything is indexed correctly so that a new session can find the
1137
+ plans, rules, and other important details when it is asked."* Auditing that claim found the index was
1138
+ sound and the **access path was not**, in two specific ways — both now closed and pinned:
1139
+
1140
+ 1. **`chapters_search` required a pool mirror.** With nothing linked it returned an empty list and a
1141
+ suggestion to go link a remote — while every chapter, every `kind: plan` document and every rule sat
1142
+ in the workspace store, indexable. Search now has two doors over **one shared scorer**
1143
+ (`rankEntries`): the materialized mirror when a project is linked, and `buildLocalCorpus` over the
1144
+ store when it is not. Deliberate details:
1145
+ - **artifacts are not indexed** (they answer to `chapters_artifact`; a blob indexed twice is a second
1146
+ index, which §6 forbids);
1147
+ - **the door is named in the answer** — "searched this machine's store (N indexed document(s); no pool
1148
+ mirror …)" — because a session must never read *this machine only* as *the whole project*;
1149
+ - **"nothing matched" and "nothing here" are different results** (`empty` + `documents`), since they
1150
+ imply different next actions;
1151
+ - if the pool yields nothing, the local store is consulted before answering empty.
1152
+ 2. **The continuation notice taught only `read`.** A child was told chapters exist and how to open one;
1153
+ it was never told `chapters_search` or `chapters_artifact` exist. An installed tool that is never
1154
+ mentioned is a tool a small model will not reach for, so the notice gains a three-line
1155
+ `## Finding things` block naming all three verbs, plus the one fact a child cannot infer — that plans
1156
+ are searchable **as plans** (`kind: plan`) and rules arrive from other machines. `retrievalHints`
1157
+ (default on) is the switch; off, the notice is byte-identical to the previous shape.
1158
+
1159
+ Measured, not assumed: no committed tape contains a seeded notice anywhere in its prefixes (checked
1160
+ entry-by-entry across the corpus), so the notice change costs no re-record — and the eight-project chain
1161
+ replays green with it. That is luck of coverage rather than design: no current journey makes a
1162
+ *continuation child* speak, which is why the child-facing prose is now covered by golden rows here plus
1163
+ the live-model leg, and why a child that answers a question in a browser is on the open list.
1164
+
1165
+ Also landed: **the kernel-drift gate** (`tests/unit/host-seed-contract.test.ts`) — the seeded
1166
+ `plan/mode` continuation depends on the host knowing that type and treating it as log-only, read from the
1167
+ kernel's own exported predicates (`KNOWN_SESSION_EVENT_TYPES`, `isSurfaceEligibleType`,
1168
+ `isAppendSurfaceEvent`) rather than reimplemented, so a kernel change surfaces as a red row instead of a
1169
+ child that silently stops being gated.
1170
+
1171
+ ### Amendment 2026-10-20 — rules become two tiers: project (shared, approved) and session (local, derived)
1172
+
1173
+ Human directive, in the round that planned it: *"I end up giving the AI agent a bunch of rules
1174
+ embedded in my requests… these rules just get lost in the noise."* Auditing where they actually get
1175
+ lost found one concrete hole, not a vague one: the in-place checkpoint composes
1176
+ `leadSections = [planSection, plotSection]` (`src/engine-core.ts`). **A plan survives a compaction
1177
+ and a plot survives a compaction; a rule does not.** That, plus a rule set that only ever reaches a
1178
+ child through the continuation notice with no relevance selection, is the whole motivation.
1179
+
1180
+ Six decisions, each narrowing a temptation the discussion produced:
1181
+
1182
+ 1. **Two tiers, one store** (§7.1). Session rules are `kind: rule` files in the same tree with
1183
+ `scope: session` — same numbering, same write-once immutability, same `list`, same index. Absent
1184
+ `scope` means `project`, so every rule record on disk before this amendment keeps working with
1185
+ unchanged bytes. There is no second rules subsystem to drift.
1186
+ 2. **The global tier was not built, because it already existed.** The request was "carry across my
1187
+ checkouts of this repository", and `projectKey` is UUIDv5 over the *canonicalized remote* (§2.3)
1188
+ with `harnessId` defaulting to the hostname — so several checkouts of one repo already resolve to
1189
+ one rule namespace on one machine. The work became making the four ways that identity silently
1190
+ diverges say so (§7.8) instead of inventing a third namespace with a much larger blast radius.
1191
+ 3. **Session activation is derived, never a curation fact** (§7.5). Active iff `scope: session` ∧
1192
+ its session is on the current ancestor path ∧ not revoked. `edits/<harness>/curation.jsonl` is
1193
+ reserved for human status decisions *and it travels in the pool* — machine-local activation volume
1194
+ does not belong there. The 2026-09-20 amendment (approval is a per-machine fact, never a file
1195
+ rewrite) is **not** relaxed: it still governs every byte of the shared tier.
1196
+ 4. **Model-captured rules may auto-activate, and only on a verbatim quote** (§7.5, behind
1197
+ `rulesEnrichment`, default off — *decided and specified; the P3c build shipped the marker path
1198
+ first, so this half is still ahead of the code*). The rejected framing was "the model writes its own governing
1199
+ rules with no human gate"; the accepted one is that the machine-local, one-command-reversible,
1200
+ never-staged scope makes the blast radius one session tree — so the gate moves from *who clicks*
1201
+ to *what the rule is derived from*. An inferred rule with no human sentence behind it waits in
1202
+ `list --proposed`. And capture stays off the critical path entirely: the `ADD RULE:` marker is
1203
+ deterministic and costs zero model tokens (invariant 1), while the model pass rides §6.2's idle
1204
+ batch, because the local server is single-slot and per-turn work queues against the user's own
1205
+ turns.
1206
+ 5. **Rules are carried at the moments the head is rebuilt, never injected per request** (§7.6).
1207
+ Two measured reasons, not taste: invariant 3 (the system prompt is process-global and
1208
+ `systemPrompt` is excluded from our injections on purpose), and the llama.cpp number from
1209
+ E3/r23-r28 — a head-position replacement reuses **nothing** (`cacheReuse 0`), so a ruleset that
1210
+ churns mid-session is a cache-wipe machine on exactly the target hardware. Between rebuilds the
1211
+ human's own bytes are still in the surface; they die only when the surface is replaced, which is
1212
+ where they are now re-stated. `chapters_continue` carries both tiers, `chapters_fork` carries
1213
+ project only — the *same* `intent` axis that already decides plan bytes (`docs/plans-carriage.md`
1214
+ §§ 9–13), not a new distinction.
1215
+ 6. **§7.4 selection is raised from "P3b later" to required, with a floor.** Automatic capture means
1216
+ the active set now grows without a human noticing, so always-render-everything stops being
1217
+ affordable: pinned (human-set, always) → relevance-selected (the existing `search.rankEntries`,
1218
+ never a second ranker) → index-only. Per-tier count caps are **announced**, because selection
1219
+ that can silently drop the load-bearing rule would rebuild the original bug inside the fix —
1220
+ and a cap that *truncates* would violate §1.2, so the token ceiling still refuses with numbers
1221
+ and the count ceiling selects with a visible count. Selection must be a pure function of stored
1222
+ inputs (no clock, no readdir order) or two machines disagree about what the agent was told and
1223
+ the tape corpus cannot be stable.
1224
+
1225
+ Also recorded as accepted consequences: session-scope exclusion happens at the one store→repo mapper
1226
+ (`planStoreToRepo`) and is proven over **both** providers; a session rule never overrides a project
1227
+ rule, and conflicts are reported (`/chapters-rule conflicts`, one bounded stream call in the
1228
+ `elicitedPlot` family) rather than resolved (§7.7); losing a checkout loses its session rules, which
1229
+ is *re-derivation*, not corruption (§7.5, and the storage warning in `AGENTS.md`); and the agent-
1230
+ facing surface stays minimal by decision — capture and carriage are invisible to the model, and the
1231
+ one thing it is told to do is **look the rules up before starting a task** (§7.9), because a tool an
1232
+ instruction never names does not exist to a small model.
1233
+
1234
+ Specification: §7.1 and §7.3–§7.9. Build status and exit criteria: §13 P3c (specified, not yet
1235
+ built at the time of this amendment — this entry is the decision record, not a claim of behavior).
1236
+
1237
+ ### Amendment 2026-10-21 (b) — rules reach agents, and agents reach rules (#52)
1238
+
1239
+ Two user decisions in the same review round, both about the gap between "the rule exists" and
1240
+ "the agent in front of the work is told about it".
1241
+
1242
+ **1. The preset persona is the instruction channel.** Nothing told a brand-new session, or a delegated
1243
+ subagent, to use `chapters_rules` / `chapters_search`: a plugin must not contribute a system-prompt
1244
+ section (§1.3 / invariant 3), and the continuation notice only exists for continuation children. The
1245
+ sanctioned per-composition channel is the preset's OWN persona row — `@deepseek-ai/dsh-persona`,
1246
+ prefix/suffix — the mechanism already carrying the `PLOT:` discipline. The shipped suffix now adds a
1247
+ short "Chapters memory and rules" section: run `chapters_rules` with the task description before a
1248
+ task and NAME the rules you followed (or say none); for earlier work, decisions, or bug history run
1249
+ `chapters_search` FIRST instead of re-reading files or asking the human; `chapters_artifact` holds the
1250
+ oversized bytes; and a delegated child is covered by the same rules and the same archive. It is
1251
+ per-preset, so a session on `standard` sees none of it. Two couplings are pinned by behaviour, not
1252
+ comment: the suffix must remain matched by the plot template-echo guard (else every session inherits
1253
+ its own instruction as a "plot"), and no new prompt variable may appear (`{{model}}` only — an
1254
+ unregistered variable throws at assembly and kills every turn of every chapters session).
1255
+
1256
+ **2. Rule ancestry crosses the delegation edge.** The registry is the sole authority on continuation
1257
+ lineage and deliberately ignores the kernel header's `parentSession` — correct for numbering and the
1258
+ TOC, and it left a whole session shape with ZERO rules: a delegated subagent was an unrelated root by
1259
+ that view. So `session-rules.ruleAncestorSet` walks BOTH edges (registry `parentSession` ∪ header
1260
+ `parentSession`) transitively, READ-ONLY, writing nothing, and still only UPWARD — a child's
1261
+ inferences never govern the session that delegated them. Bounded (64 expansions) and loud when the
1262
+ cap bites (`lineage walk hit its cap…` inside the block, `LINEAGE TRUNCATED:` in `/chapters-rule
1263
+ active`); an unreadable header contributes no edge rather than breaking a turn. One walk, one builder:
1264
+ every door, the checkpoint and the surface call `ruleAncestorSet`, and an anti-drift row fails if a
1265
+ plane computes ancestry by hand again — hand-built ancestry is precisely how the gap appeared.
1266
+
1267
+ Consequence, recorded before anyone is surprised by it: this changes a PRODUCT PROMPT (the persona
1268
+ suffix) and adds a tool, so the browser-plane tapes owe one whole-project re-distill when the host
1269
+ lock moves to 0.2.x — one re-record pays both debts. Until that runs, no claim about browser-plane
1270
+ rule carriage is verified.
1271
+
1272
+ ### Amendment 2026-10-21 — a mined rule is governed by POLICY, not by proof that a human said it
1273
+
1274
+ Human directive, one day after the previous entry and against the feature as built: *"I don't like
1275
+ your approach of making mined rules governing if it appears verbatim in my own words… we need to
1276
+ assume that the human may not know, which is why they may be using AI in the first place."* The
1277
+ verbatim-quote gate built under §7.5 is **removed as a condition on activation**.
1278
+
1279
+ What was wrong was a category error, and it is worth stating plainly because it is the kind of
1280
+ error this project keeps making: the gate confused **where a rule came from** with **whether it is
1281
+ allowed to govern**. Requiring a human sentence behind every mined rule meant the model could only
1282
+ ever re-state what had already been said — which is exactly what the deterministic `ADD RULE:`
1283
+ marker does better, because there you know precisely what you got. A tier that can only confirm the
1284
+ known is not inference; it is a paraphrase of the marker with extra latency. And the honest failure
1285
+ mode it was built to prevent — the model inventing a rule the human never sanctioned — is the same
1286
+ failure mode it was built to *cause*: the useful case is the standing order nobody has articulated
1287
+ yet, and the value of asking a model to look is precisely that it may see one.
1288
+
1289
+ **What replaces it.** Four things, all structural — and the reason all four are needed is that
1290
+ evidence is no longer one of them:
1291
+
1292
+ 1. `rulesAutoActivate` (default **on**) is the single switch. Off ⇒ every mined rule is written
1293
+ `reviewOnly: true` and stays inert until this machine approves it. Review-before-trust is a
1294
+ posture a team can choose; it is no longer the default posture.
1295
+ 2. The quote check survives, **demoted to a label**: `stated` (a human sentence backs it) vs
1296
+ `inferred` (the model judged it, nobody said it). It renders beside the rule text *with the
1297
+ chapter it came from*, in the head, so no inferred rule can govern anonymously. Assistant prose
1298
+ stays inadmissible even as a label — a "quote" found only in the model's restatement is
1299
+ `inferred`, not `stated`.
1300
+ 3. The agent must **name by id the session rules that shaped its reply** (§7.3's speak-up line),
1301
+ or say none did. This is the mechanism the human asked for instead of a gate, and it is better:
1302
+ a wrong inference becomes visible at the moment it acts, to the only party who can revoke it,
1303
+ rather than being prevented in advance by a test the model can satisfy by quoting badly.
1304
+ 4. The caps stay: 3 mined rules per chapter (counting rules **written**), per-tier display caps
1305
+ with the withheld count announced, machine-local scope, never staged to the pool, `revoke` one
1306
+ command away.
1307
+
1308
+ Two things deliberately **not** relaxed: a session rule never overrides a project rule (§7.7), and
1309
+ a session rule never becomes a project rule without a human (`add`) — promotion is still a human
1310
+ act. The tier's warrant for governing itself got broader; its warrant for governing *anyone else*
1311
+ did not change at all.
1312
+
1313
+ **Honest limits, recorded at the time of writing.** No live model has mined a real chapter yet (the
1314
+ dev box's GPU driver was unreachable), so how often a 4B produces a *correct* inference — versus a
1315
+ confident wrong one that now governs by default — is unmeasured. The default is a bet on the
1316
+ visibility mechanisms above, and `docs/verify.md` R5 marks the quality half unverified. Anyone
1317
+ revisiting this should look at the ratio of `stated` to `inferred` on real archives first; if
1318
+ inferences are wrong more often than a human catches them in replies, the fix is
1319
+ `rulesAutoActivate: false`, not a new gate.
1320
+
1321
+ Implementation: `src/rules.ts` (`basis`/`chapter`/`reviewOnly`, `sessionRuleActive`),
1322
+ `src/session-rules.ts` (`captureMinedRules` policy + classification), `src/enrich-wire.ts`
1323
+ (`rulesAutoActivate`, `rulesModel`, dedicated mining call), `src/rules-commands.ts` (`model` verb,
1324
+ `active` posture). Reversal pinned by a test row, not just described: *"THE REVERSAL, PINNED: an
1325
+ inferred rule governs on arrival — no human said it."*
1326
+
1327
+ ---
1328
+
1329
+ *Cross-references: invariants and the four invariants — `AGENTS.md`; budget math —
1330
+ `docs/contract.md` (continuation budget) and `docs/architecture.md` (storage); the engine
1331
+ seam the composer hooks into — `docs/host-compaction-seam.md`; measured claims cited as
1332
+ `rNN` — `spikes/probe/FINDINGS.md`.*