@fractalsynapse/dsh-chapters 0.0.0-stage → 0.2.21

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 -3
@@ -0,0 +1,571 @@
1
+ # Plans in the Archive — design record
2
+
3
+ Written 2026-10-09. **Read before writing code.** Human decisions for this feature are recorded at
4
+ § Decisions Taken and are binding; anything marked *deferred* is a human call, not ours.
5
+
6
+ DSh ships plan mode (`@deepseek-ai/dsh-plan-mode`) and our own preset mounts it
7
+ (the `plan-mode` row of the registered preset: `PLAN_MODE_SECTION` in `src/preset.ts`; before the 0.2.0 floor it lived in `presets/chapters/agent.cordis.yml`). Today a session plan survives an archive only as the
8
+ first 400 characters of a tool-call line. That is the worst possible outcome for the one artifact in a
9
+ conversation that is *deliberately* decision-complete — and the reason the plugin exists at all: a
10
+ continuation that cannot see the approved plan either re-plans from recall (lossy) or implements
11
+ something nobody approved (worse).
12
+
13
+ ## Facts to re-verify on any DSH upgrade
14
+
15
+ | Fact | Where | Consequence for us |
16
+ |---|---|---|
17
+ | The plan is a **tool-call argument**, field `plan`, type string, required | `dsh-plan-mode/lib/index.js:230-236` | Detection is one predicate: `tool/call && name === 'exit_plan_mode'`. There is no plan document event. |
18
+ | The host validates `/^#\s+\S/` on the plan text | `index.js:256` | We can rely on a leading `#` H1 for the title. |
19
+ | Approval result is the canonical `{ approved: true }` + fixed render text | `index.js:237-251` | Verdict is derivable from the paired result, not from prose. |
20
+ | Rejection = failed call carrying review feedback; dismissal = failed call naming the takeover | `index.js` execute arm + package README § Model Experience | Three distinct non-approval shapes to classify, plus a fourth we must not invent. |
21
+ | State is one log-only whole-value event `plan/mode { active }`, appended at the next **accepted in-turn pre-step** | `index.js:374, 389` | An approved exit may not have its mode event yet at archive time → the *result* is the primary signal, the mode event secondary. |
22
+ | Mode state is recovered by **folding the log** (projection `apply`, `stateVersion: 3`) | `index.js:83-94` | Last logged value wins; we mirror that rule, we do not cache a verdict. |
23
+ | The mode-switch notice rides with `form: 'notice'` — v3 `source { kind: 'plugin', plugin: 'plan-mode' }`, v4 `{ kind: 'plan-mode' }` (measured dsh-plan-mode/lib/index.js:395-410 on 0.2.0-rc.2; issue #43) | `index.js:392-410` | The screen treats injected context as NOT conversation **except** this one sentence, which is archived verbatim (the named exception, `src/injections.ts:55-68`; both arms narrow by producer AND form). Amended 2026-10-10 — this row originally said "omits". |
24
+ | `/plan <message>` steers a real user message, `source.kind 'user'`; bare `/plan`, `/plan off` and their terminal results stay outside model history | `index.js:220` + README § Human Command | The instruction a human typed under plan guidance **is** conversation and is archived verbatim today. Nothing to change. |
25
+ | Freshly created agents begin plan mode **inactive**; `plan/mode` is per-session and not inheritable | README § Known Limitations | A `chapters_continue` / `chapters_fork` child cannot know it was mid-plan unless we carry that text. |
26
+ | `tool/call` data shape is `data.{callId, name, arguments}`, `arguments` an object **or** a JSON string | `src/types.ts:8`, precedent `src/render.ts:253` | The extractor must accept both, exactly as the renderer already does. |
27
+
28
+ ## What the pipeline loses today
29
+
30
+ | Loss | Line | Fix below |
31
+ |---|---|---|
32
+ | Full plan bytes never reach the store (400-char head only) | `src/render.ts:250-257` | § 2 + § 3 |
33
+ | Artifacts are written for `tool/result` payloads only | `src/render.ts:258-277`, `:126-156` | § 3 |
34
+ | `plan/mode` events fall to `default:` → `unrenderedSeqs` (a disclosed hole, no prose) | `src/render.ts:284-285` | § 2 |
35
+ | Nothing in the TOC names a plan or its verdict | `src/continue-core.ts:111-141` | § 4 |
36
+ | `chapters_search` resolves chapters + rules; artifacts travel (`src/sync.ts:160-215`) but are not index entries | `src/search.ts:76, 104-105` | § 8 |
37
+ | A plan in an **open** turn is above the archive ceiling (completed turns only, `src/tools.ts:186-189`) and is never archived | `src/tools.ts:194-198` | § 5 `livePlans` |
38
+
39
+ ## Decisions taken (human, 2026-10-09)
40
+
41
+ 1. **Home for plan bytes: artifact pointer + chapter block.** The verbatim plan goes to the
42
+ content-addressed artifact store; the chapter carries status, outline, a bounded head, and the path.
43
+ *Not* a first-class `kind: plan` corpus document — that stays deferred (§ Deferred), because it
44
+ widens the corpus and needs a `docs/knowledge-repo.md` amendment.
45
+ 2. **Mode carriage: notice-only.** The child is *told* about the active plan. We do not seed a synthetic
46
+ `plan/mode` event into the seed: the seed contract is pinned as one event at `seq 0`
47
+ (AGENTS.md invariant 2, `spikes/probe/FINDINGS.md`), and flipping the child's mode also changes the
48
+ composed `plan:policy` header section. Both belong to a measured spike, not to this feature.
49
+ 3. **Record before code.** This file is the reviewable artifact; implementation starts after it lands.
50
+
51
+ ## Design
52
+
53
+ ### 1. `src/plans.ts` — pure detector (no cordis, no fs, L0-testable)
54
+
55
+ ```ts
56
+ export type PlanStatus = 'approved' | 'rejected' | 'dismissed' | 'pending' | 'superseded' | 'failed'
57
+ export interface PlanDoc {
58
+ seq: number; resultSeq: number | null; callId: string
59
+ title: string; status: PlanStatus; text: string
60
+ sha256: string; bytes: number; estTokens: number
61
+ outline: string[] // H2/H3 headings, in order, verbatim
62
+ artifactPath: string // artifacts/<aa>/<sha>.txt
63
+ archived: boolean // false when seq > archiveCeiling
64
+ }
65
+ export function detectPlans(events, ceiling, opts): { plans: PlanDoc[]; modeActiveAt: (seq: number) => boolean }
66
+ ```
67
+
68
+ *Status-vocabulary amendment (2026-10-09, at implementation).* A fifth class appeared in the host's own
69
+ `execute` arm and is not a human verdict at all: the review could never open (no user-questions channel;
70
+ the plan-mode service reloaded mid-review; the call ran outside plan mode; the markdown had no `#`
71
+ heading). Labelling those `rejected` would credit the user with a decision they never made, so the union
72
+ carries `failed` and prints the host's own text as the verdict line. The four shapes the record named are
73
+ all still there.
74
+
75
+ Rules, each pinned by a unit row:
76
+
77
+ - **Pair by callId** using the existing `sameCall` logic (`src/render.ts:158-164`) — extract it, do not
78
+ duplicate it.
79
+ - **Verdict precedence**: paired `tool/result` first (`{approved:true}` → approved; `isError` +
80
+ "Keep planning" feedback → rejected; failed call naming takeover → dismissed); **no result event →
81
+ `pending`**, which must never be rendered as approval. The `plan/mode` fold is secondary evidence and
82
+ only breaks a tie.
83
+ - **`superseded`**: same H1 title, different sha256, later seq ⇒ newest is operative, earlier ones are
84
+ `superseded`. Deterministic; no model judgement.
85
+ - **Screen the body**: run `stripReminderSpans` over the plan text. A plan is model-authored bytes, but
86
+ an injected span riding inside it is host state (same reasoning as `src/injections.ts:62-70`).
87
+ - **Redaction rides for free**: build the doc through `artifactRef`, so the `render → redact → write`
88
+ chokepoint applies (`src/render.ts:179-182`). Never write an unredacted plan blob.
89
+ - Title = the first heading, H1 preferred; keep the 72-char cap that `deriveIdentity` already uses
90
+ (`src/engine-core.ts:251-262`).
91
+
92
+ ### 2. Chapter body: a labeled section **after** the transcript
93
+
94
+ `renderChapter` appends one block, after the verbatim transcript and before the closing of the body —
95
+ never interleaved, so the transcript stays exactly what the log said:
96
+
97
+ ```
98
+ ## Plans in this chapter
99
+
100
+ ### Plan — "Carry session plans into chapters" — APPROVED (call seq 412, result seq 419)
101
+ - full text: `artifacts/8f/8f3a1c….txt` (18.4 KB ≈ 4.6K tok) — chapters_artifact 'toc' | 'search' | 'read'
102
+ - outline: Goal · Success criteria · Changes by subsystem · Tests · Assumptions · Rollback
103
+ - head (first 900 chars, verbatim): `# Carry session plans into chapters …`
104
+ - mode: plan mode ACTIVE from seq 118 (log event plan/mode, seq 118)
105
+ - superseded by the same-titled plan at seq 455 — that one is operative
106
+ ```
107
+
108
+ Frontmatter gains `plans: [412:approved:8f3a1c9d]` — one `seq:status:sha8` triple per plan — and
109
+ `planMode: active|inactive|unknown`, BOTH emitted only when the chapter really carries a plan or a mode
110
+ flip. *Why the compact triple instead of the nested list this record first specified:* the only reader of
111
+ frontmatter is `indexing.parseFrontmatter`, which understands `key: value` and flat `key: [a, b]`. Nested
112
+ YAML maps parse as nothing at all — an index that looks populated and reads empty, the worst kind of lie
113
+ here. So the machine-readable home for the full facts is the **chapter record** (`ChapterRecord.plans`,
114
+ zod-validated in the registry, `src/store.ts`), which is what the notice builder consumes; the
115
+ frontmatter line is the human-visible claim of the same fact, and the prose lives in the body block.
116
+
117
+ Display bounds, added at 0.1.8 (two of them — the second is the same mistake one scale down):
118
+
119
+ - the OUTLINE line is an index, not the document: `PLAN_OUTLINE_MAX_ENTRIES` (12) at
120
+ `PLAN_OUTLINE_TITLE_MAX` (60) chars each, and when entries are dropped the block says
121
+ `+more (chapters_artifact 'toc' lists every heading)`. A silently shortened index is the lie this store
122
+ exists to avoid, so truncation is always announced and the full map is one query away.
123
+ - the TITLE is printed at most 120 characters
124
+ (`PLAN_TITLE_MAX`, `src/plans.ts`) — a 4 KB H1 would otherwise have been repeated in every chapter carrying it
125
+ AND in every child notice built from that path, unbounded and un-costed until the budget gate refused the
126
+ continuation. The artifact keeps the heading whole; only the display is bounded, which is the same
127
+ distinction `planInlineChars` draws for the body. Closed ATX headings (`# Title #`) lose their trailing
128
+ hashes in the title, not in the bytes.
129
+
130
+ The sha256 anchor convention must hash what `parseChapterFile`
131
+ returns — body **including** the trailing newline (`src/engine-core.ts:297-302`; the T9 lesson: a
132
+ mismatch silently blocked enrichment for an era).
133
+
134
+ `plan/mode` gets an explicit `case` so its handling is *stated* rather than a silent skip: it renders as
135
+ the `mode:` line above and does not enter `unrenderedSeqs`.
136
+
137
+ ### 3. Invocation artifacts, generally (the honest fix for "always inline")
138
+
139
+ Any `tool/call` whose serialized arguments exceed `callArgArtifactTokens` gets the same treatment:
140
+ full blob to the artifact store, bounded head + path inline. Plans are simply the case that earns the
141
+ mechanism. In the same commit, correct `docs/architecture.md:73` and `:97`, which claim invocations and
142
+ arguments "always stay inline" while the code caps at 400 (`src/render.ts:255`). The real rule:
143
+
144
+ > Invocations are always inline; a bounded head is always artifact-backed. Every byte of an
145
+ > invocation and its result remains retrievable.
146
+
147
+ ### 4. The notice: `plansSection`, built exactly like `rulesSection`
148
+
149
+ `assembleNotice` takes an optional `plansSection: string` (`src/continue-core.ts:111-141` precedent).
150
+ Empty or absent ⇒ **zero injected bytes**, which is what keeps this tape-safe (§ Tapes). Placement:
151
+ between the project line and the preamble, so it is above the fold:
152
+
153
+ ```
154
+ ## Plans on this path (3: 1 approved, 1 rejected, 1 pending)
155
+ - ACTIVE PLAN: "Carry session plans into chapters" — APPROVED (ch. 4, seq 412, sha 8f3a1c…, ~4.6K tok)
156
+ full text: .dsh-chapters/artifacts/8f/8f3a1c….txt — read it with chapters_artifact before implementing;
157
+ do NOT re-plan from recall.
158
+ - REJECTED v1 "…" (ch. 4, seq 388) — user feedback is archived in that chapter
159
+ - pending "…" (seq 501) — the review was never answered. NOT approved; do not implement it.
160
+ ```
161
+
162
+ Cost is counted, never exempted: the section is part of `noticeTokens` and flows through the same
163
+ `preflight` (`src/continue-core.ts:146-162`) — `tocTokens + handoffNoteTokens ≤ ratio × (window − header)`.
164
+ Over budget ⇒ refuse with numbers. **Do not "remove the token measurement" for plans.** The measurement
165
+ is the only thing that keeps the refuse-with-numbers rule true; exempting a multi-K-token prose form
166
+ would make the 8K chapter target and the child's cold head meaningless on exactly the 32K hardware this
167
+ project targets. Economy comes from *carriage* (artifact + bounded head), not from blinding the meter.
168
+
169
+ ### 5. `chapters_segment` reports the facts
170
+
171
+ Add to the output (`src/tools.ts:186-205`): `plans: [...]` (every field a model needs to choose ranges,
172
+ minus the text), `modeActive`, and `livePlans` — plans whose `seq > archiveCeiling` (an open turn:
173
+ precisely the mid-review case), explicitly labelled *not archived, parent log only*. Ranges stay the
174
+ model's call and chapter text stays the plugin's (invariant 4); the model never guesses a verdict again.
175
+
176
+ ### 6. Composition and titles
177
+
178
+ - Forced split points: an `exit_plan_mode` call seq is a topic boundary — feed them to
179
+ `composeChapters` (`src/compose.ts:78`) so a plan is never buried mid-chapter.
180
+ - `deriveIdentity` prefers the plan H1 when a span opens with a submission (`src/engine-core.ts:251`);
181
+ `HARNESS_PROSE` (`:248`) stays as-is — a plan is not harness prose.
182
+
183
+ ### 7. Config register (Schemastery `Config`, `src/index.ts:53-110`) — no hardcoded tunables
184
+
185
+ | Key | Default | Meaning |
186
+ |---|---|---|
187
+ | `planInlineChars` | 900 | Verbatim head in the chapter block. The artifact holds the rest. 0 = pointer only. |
188
+ | `callArgArtifactTokens` | **0 (off)** | Above this, a tool-call argument is artifact-backed. |
189
+ | `plansInNotice` | true | Renders zero bytes when the path holds no plans. |
190
+
191
+ *Two amendments, both made by measurement rather than taste.*
192
+
193
+ - **`planModeEvents` was cut.** A `plan/mode` event is either printed in the Plans block or it is a
194
+ silently dropped byte, and silently dropped bytes are the one thing this store does not do. There is no
195
+ honest second value for that knob, so the rule is unconditional: the flip prints, and the folded state
196
+ lands in `planMode:`.
197
+ - **`callArgArtifactTokens` ships at 0, not 600.** Turning it on changes chapter bytes wherever a `write`
198
+ call carried a whole file — and the recorded model tapes are full of such turns (§ Tapes). Plans are
199
+ unaffected: their carriage is unconditional and named. A deployment that wants argument blobs opts in
200
+ deliberately, knowing it owes a re-record.
201
+
202
+ ### 8. Search, and why no new index
203
+
204
+ A plan is discoverable through the **chapter that cites it**: the chapter's `topics` frontmatter is
205
+ already the index unit (`src/indexing.ts:140-152`, `src/search.ts:104`), so `chapters_search` returns
206
+ the chapter, whose body hands over the artifact path, and `chapters_artifact` (toc / search / read) does
207
+ the recursive descent — a Markdown plan has headings, so `action:'toc'` yields a line-numbered map for
208
+ free. **No second index** (hard rule; the TOC is the only index). Making plans corpus documents
209
+ (`kind: plan`, indexed like rules — the `sync.ts:203` "rules are chapters of a different kind, same
210
+ transport" precedent) stays deferred to § Deferred.
211
+
212
+ Registry note: **do not** use `state.plans` — that name is the compaction manifest
213
+ (`src/registry.ts:37`, `src/store.ts:67`). If a cross-session lookup is ever needed, name it
214
+ `submittedPlans`.
215
+
216
+ ## Invariant audit
217
+
218
+ | Invariant | This design |
219
+ |---|---|
220
+ | 1 — never rewrite the durable log | Read-only over the log; artifacts and chapters are appends. Original plan events stay in the parent forever. |
221
+ | 2 — never seed from parent events | Unchanged: still one synthetic notice event at `seq 0` (decision 2 — no `plan/mode` seed). |
222
+ | 3 — never contribute a system-prompt section | The plans section is per-continuation notice text, not a header contribution. We only *read* plan mode's contribution. |
223
+ | 4 — never let the model author chapter bodies | The plan text is copied from the log by the plugin; the model supplies ranges and gets facts back. |
224
+ | Hard rules | No new index; no renumbering (plan blobs are content-addressed, chapters untouched); ranges still validated (`validateRanges`, `src/render.ts:96-123`); never truncate to fit — the head cap is a *display* choice with the full bytes retrievable, and the budget still refuses. |
225
+
226
+ ## Tapes and e2e impact (measured, not assumed)
227
+
228
+ - `grep -rlo "exit_plan_mode\|plan mode\|plan-mode" tests/fixtures/` → **no hits** (2026-10-09). No
229
+ recorded scenario submits a plan, so a conditional plans section renders zero bytes in every replay:
230
+ **no re-record required** for the notice or renderer changes. Re-confirm after implementation — if a
231
+ product prompt *is* edited (personas, tool descriptions, notice text), the rule stands: re-record
232
+ **whole projects**, never one spec of a shared boot.
233
+ - New e2e project `plans` (own boot, own `E2E_PORT`, throwaway home `var/e2e-home-<port>`, one journey =
234
+ one boot — the fork- and fanout-carve-out lesson), recorded once then replayed. Prompts must be
235
+ tool-free **in words the model obeys** (the RR2 OOM lesson). **LANDED 2026-10-10 as the eighth chain
236
+ project** — but NOT from the live model: the box's `:8080` server closed the stream mid-turn
237
+ (`turn/end` `reason {kind:"error", code:"TRANSPORT"}`), so the tape was distilled through the scripted
238
+ upstream `tests/e2e/stub-model.ts` (wire format copied form-for-form from recorded llama.cpp traffic).
239
+ `tests/fixtures/model-tape/plans/PROVENANCE.md` states what that proves (the whole carriage, in a real
240
+ browser, against the real review channel) and what it does not (a model choosing to plan), and carries
241
+ the re-record command that retires the stub.
242
+ - **The threshold is pinned to 0.99 for this project (0.1.10), and the journey asserts its own
243
+ precondition.** At the stress regime's 0.9 the hosted runner crossed the compaction trigger and issued
244
+ a plot-elicitation call that no locally-distilled tape had ever seen — a MISS that only CI could
245
+ produce, after three local greens. So this journey pins its own determinism: no compaction may interrupt
246
+ it, and the spec fails if `compaction/*` appears, with the reason written where the assertion lives.
247
+ Determinism that only holds on the machine that recorded the tape is not determinism.
248
+ - **0.1.11: the pin is structural, not numeric.** 0.1.10's threshold pin did NOT stop the hosted runner
249
+ compacting (same annotated miss), so the journey now boots with `auto: false` + `elicitedPlot: false`
250
+ (`E2E_NO_AUX_MODEL=1` in the chain, applied by `boot.ts` into the throwaway preset). No auxiliary model
251
+ request can exist for a tape to be missing. The `compaction/*`-absent guard stays, to catch someone
252
+ undoing it.
253
+ - **The replay chain reports its own failure** (`scripts/ci-replay.sh`): project + failing tests, the
254
+ proxy's tape-miss journal tail, and the engine diagnostics tail that every boot already writes.
255
+
256
+ ### What the browser plane corrected (this is why it exists)
257
+
258
+ Every item below was an assumption the deterministic plane had encoded — and the live run said otherwise:
259
+
260
+ - **`planMode:` at a plan's chapter end is `inactive`, not `active`.** Approving a plan EXITS plan mode,
261
+ and the host appends that flip at the next accepted pre-step — inside the same turn, before
262
+ `turn/end`. My expectation said `active`; the archived chapter said `inactive`, and both mode flips
263
+ printed. The fold was right; my belief was wrong.
264
+ - **The toolbar chip is an EXIT control only.** `PlanModeControl` renders when
265
+ `plan.pending ? !plan.active : plan.active` (`dsh-client-ui-plan/lib/client.js:39-41`), so there is no
266
+ "press to turn plan mode ON". Entry is `/plan <message>` in the composer, which appends
267
+ `plan/mode {active:true}` before the turn and steers the text as one real user message. Learning that
268
+ cost one 40-minute timeout on a locator that could never appear.
269
+ - **`text=Approve this plan…` hangs; `getByRole('region', {name: /Approve this plan/})` does not.** The
270
+ question is the review REGION's accessible name, not matchable element text — read off the failure
271
+ snapshot, which is the only honest way to find this out.
272
+ - **A seeded event read out of the session log is JSON-escaped.** `expect(raw).toMatch(/ACTIVE PLAN: "x"/)`
273
+ can pass as a substring and fail as a regex; parse the event and assert on `data.content[0].text`.
274
+ - **`expect.poll(..., {intervals: 3000})` throws a TypeError** — intervals is an array. Trivial, but it
275
+ killed the first two recording runs.
276
+ - **A scripted upstream that string-matches the message array lies confidently.** The stub's first
277
+ `decide()` searched all messages for `exit_plan_mode`/`approved` — both words live in the SYSTEM PROMPT
278
+ (tool catalog + plan policy), so every turn answered as if the review were done and the review card
279
+ never opened. The honest signal is the transcript's SHAPE (`role:'tool'` exists only once a tool ran).
280
+ Pinned by `tests/unit/stub-model.test.ts`, because a stub that answers wrongly records a tape of a
281
+ session that never planned.
282
+ - **Acceptance assertions must be scoped to the session tree the run created.** Scanning the whole store
283
+ lets a previous run's plan satisfy the next run — a green that proves nothing.
284
+ - Coverage: `npm run coverage:ci` gate (≥85 all four) must stay green; `src/plans.ts` is pure and gets a
285
+ full-branch row like the other planners, and the new `render.ts` arms need the success/refusal
286
+ surface covered the way the 2026-09-23 sweep did it.
287
+
288
+ ## Verification rows to add to `docs/verify.md`
289
+
290
+ | # | Claim | Evidence |
291
+ |---|---|---|
292
+ | P1 | Verdict classification is right for all four shapes + superseded | `tests/unit/plans.test.ts` over host-shaped fixtures (mirror the real nesting the way `tests/unit/render.test.ts:18-22` mirrors tool-result shape) |
293
+ | P2 | A plan with no result is `pending`, and `pending` never renders as approval | unit + a notice golden test |
294
+ | P3 | Full plan bytes are retrievable: archive → artifact exists → `chapters_artifact` read returns them byte-identical (modulo redaction) | integration, same style as `corpus-hygiene.test.ts` |
295
+ | P4 | `plan/mode` no longer lands in `unrenderedSeqs`, and no other event type silently vanishes | `tests/unit/render.test.ts` rows (the `mystery/event` row at `:124-129` must stay) |
296
+ | P5 | The plans section is counted by `preflight` and an over-budget archive refuses **with numbers** | unit on `preflight` + integration on `runContinue` |
297
+ | P6 | A plan above the ceiling is reported as `livePlans` and carried, marked not-archived | integration on `chapters_segment` |
298
+ | P7 | Injected spans inside a plan body are screened; a plan quoting `AGENTS.md` in prose survives verbatim | `injections.test.ts` rows (H1 family) |
299
+ | P8 | Zero bytes when the path has no plans (tape neutrality) | notice golden test, both directions |
300
+ | P9 | Both carriage paths agree (continuation AND in-place engine compaction) | `tests/unit/plans-engine.test.ts` |
301
+ | P10 | A chapter's `artifacts/…` citation resolves whether the blob sits at the store root or inside the ancestry tree | `tests/unit/artifact-query.test.ts` + the integration row |
302
+ | P11 | The browser journey, once its tape is recorded | `tests/e2e/plans.spec.ts` (project `plans-dev`, self-skips without the live model) |
303
+
304
+ `docs/verify.md` § Plans Carriage is the ledger; this table is the design's own list of what it claims.
305
+
306
+ ## Landing stages
307
+
308
+ | Stage | Deliverable | Status (2026-10-09) |
309
+ |---|---|---|
310
+ | 0 | This record lands; no code | done — reviewed, decisions 1–3 held |
311
+ | 1 | `src/plans.ts` + `src/log-shape.ts` (the shared pairing readers) + unit rows | done — P1/P2/P7 |
312
+ | 2 | `render.ts` Plans block + frontmatter + invocation artifacts; `docs/architecture.md` wording fix | done — P3/P4/P8 |
313
+ | 3 | `chapters_segment` facts + composer split points + `deriveIdentity` preference | done — P6 |
314
+ | 4 | `plansSection` in `assembleNotice` + wiring + config register | done — P5/P8/P9 |
315
+ | 5 | Site docs (§9 anatomy, §16 tools, §17 configuration, §18 notice) | done in the same release |
316
+ | — | Seven-segment tape replay | green WITHOUT re-recording (2026-10-09, `npm run test:e2e:replay`, EXIT=0) |
317
+ | — | `plans` browser journey | LANDED as the eighth chain project (2026-10-10) and immediately **extended to two legs** so § 12's continue-vs-branch distinction is tested, not described: a `chapters_segment`→`chapters_continue` continuation (asserts `## Active plan`, the plan's document path, and NO seeded gate because approving a plan exits plan mode) and a fork-button branch (asserts **no** directive section, archive still cited). Replays in 17 s on a committed tape; the eight-project chain re-runs for the 0.1.9 ship. Its tape is scripted-upstream distilled — see § Tapes and `PROVENANCE.md` |
318
+ | 8 | `/chapters-continue` — the deterministic door for continuations (§ 15): continuation leg back in the tape chain, both intents covered in the browser, no frozen ranges |
319
+ | 7 | Journey made hardware-independent (0.1.10): 0.99 threshold pin + `compaction/*`-absent guard; replay chain annotates its own failure and ships the engine diagnostics | built — tape re-distilled under the pin, eight-project chain `CHAIN_EXIT=0` locally, hosted acceptance green at the 0.1.10 tag |
320
+ | 7 | Findability round (§ 14): local search door over a shared scorer, `## Finding things` in the notice (`retrievalHints`), kernel-drift gate on the seed contract | built — verify.md P18–P20; two pinned contract rows amended with reason; eight-project chain re-run |
321
+ | 6 | Second-round decisions (§§ 9–13): intent-aware notice, seeded plan mode for continuations, `kind: plan` documents, checkpoint carriage, the one injection-screen exception | built 2026-10-10 — verify.md P12–P17; 582 deterministic rows green, gate 98.05 / 86.44 / 93.67 |
322
+ | — | Follow-up, not blocking | re-record the `plans` tape from the live model — ATTEMPTED TWICE on 2026-10-10 and both times the model container dropped the stream mid-turn (`TRANSPORT`), one after 15 minutes with zero tool calls; `PROVENANCE.md` says what to check first. The carriage evidence does not change, only the claim about model behaviour |
323
+ | — | Browser-plane ledger | `npm run coverage:e2e` over all EIGHT replays: **79.92 stmts / 64.64 branch / 76.28 funcs** — the non-gated plane, reported rather than tuned (the deterministic gate is the enforced one) |
324
+
325
+ Deterministic ledger at landing: **549 unit+integration rows, zero failures** with the TreeDX container
326
+ up (80 of them new, across `plans`, `render-plans`, `continue-plans`, `plans-segmentation`, `plans-tools`,
327
+ `plans-engine`, `log-shape` and the `plans-carriage` integration), `npm run coverage:ci` **98.11 % stmts /
328
+ 86.44 % branch / 93.8 % funcs** — the gate (≥85 on all four) green, branches *up* from the 85.61 % the
329
+ record started at. The seven-segment tape replay then ran against the BUILT artifacts: EXIT=0, 8 tests,
330
+ with no tape re-recorded.
331
+
332
+ ### Two things the implementation found that the record had not predicted
333
+
334
+ - **`renderChapter` cannot pair a verdict inside its own range.** A composed chapter boundary can fall
335
+ between an `exit_plan_mode` call and the `tool/result` that answers it, so a range-local scan printed
336
+ APPROVED plans as PENDING — the exact inversion this feature exists to prevent. The scan therefore runs
337
+ over the whole event list the caller passes (`buildFinalizedChapters` now hands it the full shadowed
338
+ span instead of one chapter's slice) and only the *display* is range-filtered. Pinned by P9.
339
+ - **An ancestor path's plans cannot be ordered by `seq`.** Every session numbers its own events from zero,
340
+ so a root chapter's seq 40 and a child chapter's seq 3 are incomparable numbers, and sorting the notice
341
+ rows by seq would crown the wrong plan as operative — the exact failure mode the block exists to remove.
342
+ The rows therefore travel in **causal order**: the registry walk's root-first chapter sequence, plans
343
+ within a chapter by seq, then the caller's unarchived tail, and `buildPlansSection` prints that order and
344
+ takes the last row as operative without re-sorting. (This is the same flattening rule the chapter list
345
+ already follows — reading order IS causal order — applied to a new record type; pinned by a unit row.)
346
+ - **Deferred artifacts were already unreachable, for every blob, not just plans.** `writeArchive` writes
347
+ to `<storeRoot>/<rootSession>/artifacts/…`, the arrival stubber writes to `<storeRoot>/artifacts/…`, and
348
+ `resolveArtifactWithMirror` looked only at the second place plus the mirror — so a chapter's own
349
+ `artifacts/aa/<sha>.txt` citation, the standard form the body prints, could 404 on the machine that
350
+ wrote it. The resolver now walks the session trees between the store root and the mirror (P10), which
351
+ is what makes "every byte remains retrievable" true rather than intended.
352
+
353
+ ## 14. Findability: a plan nobody can reach is a plan nobody follows
354
+
355
+ The four decisions made plans *carried*. The audit that followed ("make sure a new session can find the
356
+ plans, rules, and other important details when asked") found the index was already correct and the
357
+ **access path** was not — two separate defects, both now pinned:
358
+
359
+ 1. **`chapters_search` needed a pool mirror.** Unlinked, it answered `[]` plus "link a remote" — while
360
+ every `kind: plan` document sat in the workspace store two directories away. Search now has a local
361
+ door over the **same scorer** (`rankEntries`), falls back to it when the pool has no match, names
362
+ which door answered, and distinguishes an empty workspace from a missed query. Artifacts stay out of
363
+ the index on purpose: they answer to `chapters_artifact`, and indexing them twice is the second index
364
+ the record forbids.
365
+ 2. **The child was never told the verbs.** The notice taught `read` and nothing else; `chapters_search`
366
+ and `chapters_artifact` were installed but unmentioned — so for a small model they did not exist. The
367
+ notice gains `## Finding things` (three lines) and says the one thing a session cannot infer: that
368
+ plans are searchable **as plans**.
369
+
370
+ Measured before believing: **no committed tape prefix contains a seeded notice anywhere** (checked
371
+ entry-by-entry across all eight projects), so the notice change costs zero re-records and the chain
372
+ replays green with it. That is coverage luck, not design — no current journey makes a *continuation
373
+ child* speak — which is why the child-facing prose now has golden rows, why the live-model leg exists,
374
+ and why "a child answers a findability question in a browser" is on the open list rather than claimed.
375
+
376
+ Two rows in the suite pinned the OLD contract and failed loudly, which is the only reason this section
377
+ is honest: `no mirror ⇒ honest empty` became `no mirror ⇒ the local answer, with the door named`. Amended
378
+ in place with the reason in the diff, because a silently-passing old expectation would have meant the
379
+ feature shipped broken and green.
380
+
381
+ ## 15. The deterministic door: `/chapters-continue`
382
+
383
+ Attempt 5 settled an argument that had cost three releases. Two facts, both measured:
384
+
385
+ 1. **A tape cannot carry a model-authored range.** The continuation leg broke on the runner because the
386
+ recorded `chapters_continue` arguments said `endSeq: 30` while the runner's own ceiling was 29.
387
+ `validateRanges` refused — correctly. Nothing about pinning a threshold can fix a number frozen in a
388
+ recording.
389
+ 2. **A small local model will not reliably chain two tool calls.** Live, it called `chapters_segment` and
390
+ then answered in prose. So the leg could not be made green by asking the model harder.
391
+
392
+ Both have the same cure: the plugin already knew how to create a child without a model. `/chapters-fork`
393
+ derives everything itself — *"no model involvement — `deriveRanges` is deterministic, so a click needs no
394
+ agent turn and no proposed ranges."* The gap was that this door was hard-wired to `intent: 'branch'`, and a
395
+ branch by decision 3 asserts **nothing** about plans. So every property of the *continuation* shape
396
+ (`## Active plan`, `## Previous plans on this path`, the seeded `plan/mode`, the `plan document:` citation)
397
+ was provable only through the model's door.
398
+
399
+ **`/chapters-continue` is that same builder with the other intent.** Archive semantics are identical by
400
+ user decision (everything since the watermark). It is one `deterministicCommand(door)` producing both
401
+ defs — name, verbs, intent, and the no-directive prose are the only differences, and
402
+ `tests/unit/continue-command.test.ts` pins that the two doors **archive identically** (same ranges, same
403
+ titles, same plan facts) so the factoring cannot drift into two implementations. Consequences:
404
+
405
+ - the continuation leg is **back in the tape chain**, replaying on every push with no GPU and no frozen
406
+ number (it needs no model request at all — the child is created by the command, and asserted from its
407
+ durable log without the UI switching to it);
408
+ - the browser plane now covers **both intents**, which is where decision 3 actually lives;
409
+ - and the tool `chapters_continue` keeps existing for the model's own use — it is simply no longer the
410
+ only way to learn whether a continuation carries its plan.
411
+
412
+ ### The seeded-gate case: measured, and why there is no browser row for it yet
413
+
414
+ The open question was whether the host gives a completed turn to archive while a plan review is still
415
+ pending. Answered from attempt 5's own log (no speculation): `plan/mode` active at seq 4,
416
+ `exit_plan_mode` at 20, the verdict at 21, `plan/mode` inactive at 23, `turn/end` at **41** — the turn
417
+ containing a submission does not end until the review is answered. So:
418
+
419
+ - a session whose *only* turn is pending has **no ceiling**, and `/chapters-continue` refuses with its
420
+ first guard (`nothing to archive yet — the conversation has no completed turn`);
421
+ - the seeded case is reachable in a browser only when an earlier turn already closed — which means a
422
+ second distilled session (a plain turn, then `/plan`), not a rewording of the current one;
423
+ - the plugin-side behaviour for that shape **is** pinned at L0: a pending-review fixture yields
424
+ seed `['user/message', 'plan/mode']`, the notice line `Plan mode is ACTIVE in this session`, and the
425
+ plan marked PENDING/un-stored (`continue-command.test.ts`).
426
+
427
+ So the honest statement is: the seeded continuation is covered deterministically and structurally, and
428
+ NOT yet observed in a real browser. An attempt at that browser row on 2026-10-11 was built and then
429
+ REVERTED, unshipped, for two reasons worth recording so the next person does not re-derive them:
430
+ (a) the scripted upstream had to learn to answer PLAINLY in plan mode, and its decision rule first keyed
431
+ on the LAST user message — but the harness appends its own injected user messages (instructions, runtime
432
+ snapshot, skill catalog) AFTER the human's, so the ask was invisible and the journey hung on a review card
433
+ that could never appear; that ordering trap is now pinned by a unit row; and (b) a stale stub process
434
+ serving pre-edit logic made the failure look like a product problem. Neither is a product defect. The
435
+ shape is sound — `plan/mode {active:true}` logged, turn completed at seq 21, no tool call — so a future
436
+ leg needs the plain branch plus a fresh distillation, and nothing else. Building that row needs new stub rules and a fresh distillation; it is
437
+ recorded as the remaining piece rather than quietly claimed.
438
+
439
+ ## Deferred (human decisions, not ours)
440
+
441
+ - ~~`kind: plan` corpus documents~~ **decided and built** (§ 11).
442
+ - ~~Seeding `plan/mode`~~ **decided and built** (§ 10), for continuations only.
443
+ - ~~Whether the mode-switch notice stays screened~~ **decided and built** (§ 13).
444
+ - **Per-message anchoring** — still open, and decision 3 clarified rather than settled it: a fork
445
+ splits the task, so the interesting anchor is *"branch from THIS plan"*, which `deriveRanges`
446
+ already supports mechanically but the button still forks at conversation end. Needs the UI
447
+ affordance and an answer on how the child is told what it did not inherit.
448
+ - Whether the `plan-mode` mode-switch *notice* should stay screened out of chapters (decision 2026-09-21
449
+ says injected context is not conversation; a human-initiated mode flip arguably is). Small text, but
450
+ it is the one place the blanket screen drops something a reader might want.
451
+
452
+ ## Proposed AGENTS.md lines (deliberately NOT applied yet — it is the unimplemented-feature rule)
453
+
454
+ ```
455
+ - `docs/plans-carriage.md` — **read before writing code in this area**: how a session plan reaches the
456
+ chapters, the TOC, and the artifact store; and why the token meter is never exempted.
457
+ ```
458
+ plus a find-fast row `plans in chapters / active plan in TOC → docs/plans-carriage.md (+ site §§ notice, archive anatomy)`.
459
+
460
+
461
+ ## Decisions taken (second round, 2026-10-10) — all four deferred items resolved
462
+
463
+ The human answered the deferred list, and #2 came with a distinction the first round
464
+ had not drawn: **a continuation extends the same stream of work; a fork splits it.**
465
+ That single difference decides what each child is told about the operative plan, and
466
+ it is now the axis the whole carriage hangs on.
467
+
468
+ | # | Decision | What it means in code |
469
+ |---|---|---|
470
+ | 1 | **`kind: plan` documents — yes** | each plan becomes its own indexed document (§ 11) |
471
+ | 2 | **Seed plan mode into children — yes, but only for continuations** | `plan/mode {active:true}` as the child's own second seed event (§ 10), `intent === 'continue'` only |
472
+ | 3 | **Fork = split the task; compaction/continuation = keep it** | `PlanIntent` selects the notice shape: `## Active plan` + `## Previous plans` for a continuation, NOTHING for a branch (§ 12) |
473
+ | 4 | **Targeted exemption for the mode-switch notice — yes** | `isSessionStateNarration` archives it verbatim; the general screen is unchanged (§ 13) |
474
+
475
+ Plus, from the same message: *"I support anything that is going to keep the agent anchored to
476
+ the current task"* — which is why the operative plan is now carried into every **in-place
477
+ checkpoint** as well, the way the plot is (§ 9).
478
+
479
+ ### 9. The checkpoint carries the operative plan (the plot mechanism, applied to directives)
480
+
481
+ `extractPlot` proved the pattern: state that must survive a shadowed region travels *inside*
482
+ the checkpoint text, bounded, with its provenance. `carryActivePlan` does the same for plans —
483
+ with one deliberate difference in **scope**: the plot reads the region being condensed, while
484
+ the operative plan is chosen **session-wide, newest submission first**, because "what am I
485
+ building" is not a property of the region being hidden. `planSummarize` prefixes it on both the
486
+ composed and the legacy path.
487
+
488
+ Two honesty notes recorded rather than smoothed:
489
+ - **No sha is cited in the checkpoint.** At summarize time the chapter does not exist yet, and
490
+ an unverifiable citation is worse than none — the carry says "the archive chapter holding
491
+ this plan cites its complete text" instead.
492
+ - **Truncation is announced** (`carried truncated — …`), never silent, even though the
493
+ bound (`planCheckpointChars`, 1600) is a display economy choice and the archive holds all.
494
+ - The pre-existing asymmetry that `plot` is prefixed only on the legacy path was **left
495
+ alone**: changing it would rewrite checkpoint bytes for every plot-carrying session, i.e. the
496
+ committed tapes. Plans are new, so carrying them on both paths costs no re-record.
497
+
498
+ ### 10. Seeding plan mode — measured against the installed kernel, not assumed
499
+
500
+ The claim needed proof, because invariant 2 pins the seed as *one synthetic event at seq 0*.
501
+ Probed directly against the shipped `Session` (`0.1.5-rc.1`) in the exact production header
502
+ shape (`isSeeded:false`, `inheritedEventCount:0`):
503
+
504
+ | probe | result |
505
+ |---|---|
506
+ | is `plan/mode` a kernel-known event type? | **yes** — it is in `KNOWN_SESSION_EVENT_TYPES` (56 types) |
507
+ | seed `[notice@0, plan/mode@1]` accepted? | **yes** — log becomes `0:user/message → 1:plan/mode → 2:session/end-seed` |
508
+ | is it surface-visible? | **no** — surface nodes stay `[0]`, matching the host's own log-only `plan/mode` |
509
+ | does the HOST's own projection fold it? | **yes** — `planProjectionDefinition.apply` over the child's log yields `active: true`; the one-event control yields `active: false` |
510
+
511
+ So the seeded child is genuinely gated, not merely told. The surface validator narrows by type
512
+ rather than whitelisting (`assertSessionEventEnvelope` checks the envelope and continuity),
513
+ which is why this works without touching the kernel. **Invariant 2 is intact in substance**: the
514
+ seeded event is a synthetic fact this plugin authors about the boundary, never a slice of the
515
+ parent's log — the rule's letter ("exactly one event") is amended here and in AGENTS, and the
516
+ amendment is only safe because it was measured.
517
+
518
+ Seeding is refused — correctly, not defensively — when the fold says inactive, which is the
519
+ normal case after an approval: **approving a plan exits plan mode**, so a continuation whose plan
520
+ was approved gets `## Active plan … APPROVED` as a work order to implement, and no gate.
521
+
522
+ ### 11. Plans as `kind: plan` documents
523
+
524
+ A plan is now also a file of its own: `<storeRoot>/<rootSession>/chapters/plan-<NNN>-<slug>.md`,
525
+ frontmatter leading with `kind: plan`, then `status`, `planSeq`, the citing `chapter`, `sha256`
526
+ (the blob's hash — the same bytes, so the two handles cannot drift silently), `bodySha256`,
527
+ `tokens`, `topics`. The body is the plan and nothing else: no transcript, no wrapper.
528
+
529
+ Choices worth their reasons:
530
+ - **In `chapters/`, not a new tree.** The pool transport, the mirror rehydrate (0.1.5) and the
531
+ corpus index then carry plan documents with zero special casing. `loadCorpus` no longer
532
+ flattens a file's own kind back to the directory's, so `kind: plan` survives indexing and
533
+ `chapters_search` can answer *"what did we plan about X"* with the directive itself.
534
+ - **Its own registry counter** (`nextPlanNumber`, allocated through `reservePlan` under a
535
+ distinct attempt key). A plan never consumes a chapter number or reorders the TOC, and names
536
+ are still allocated through the registry rather than by scanning a directory — so a
537
+ concurrent sibling cannot pick the same file. `plan-NNN-` (not `NNN-plan-`) so a listing
538
+ never reads as two chapters claiming 001.
539
+ - **The blob stays.** Two handles, one purpose each: the content-addressed blob is what a
540
+ chapter cites without knowing any number, and what `chapters_artifact` takes (it accepts a
541
+ bare sha); the document is what the index searches and what a human browses. Same bytes, and
542
+ the record ties them with one sha.
543
+ - **Enrichment skips plan documents.** The ladder annotates narrative; a directive's body is
544
+ approved text, and paraphrasing an order is precisely the drift this store exists to prevent.
545
+ Guarded on the frontmatter `kind`, not the filename, so renaming cannot disarm it.
546
+ - **Superseded plans keep both documents.** Evolution is history: the v1 file says
547
+ `status: superseded` and is never rewritten, the v2 file says `approved`.
548
+
549
+ ### 12. The notice is intent-aware
550
+
551
+ `buildPlansSection(…, { intent, modeActive, seeded })`:
552
+ - **`continue`** → `## Active plan` (the operative directive, with its verdict, chapter, blob
553
+ and document paths) and, only when earlier submissions exist, `## Previous plans on this
554
+ path` marked *history — cited for evolution and status, not directives*. If plan mode was
555
+ seeded, the block says the directive is live and implementation waits for an approval; if
556
+ it was not, the child is told plainly that it starts outside plan mode.
557
+ - **`branch`** → **empty string, zero bytes.** A fork splits off a new direction; re-asserting
558
+ the parent's directive into the branch would be an order nobody gave. The plans are still
559
+ fully present — in the chapters the TOC already cites, and as searchable `kind: plan`
560
+ documents.
561
+
562
+ ### 13. The one exception to the injection screen
563
+
564
+ `isSessionStateNarration` matches `source.kind === 'plugin' && plugin === 'plan-mode' &&
565
+ form === 'notice'` — the harness's own "The user switched this session to plan mode." — and
566
+ `screenConversation` archives it verbatim instead of marking it omitted. Named by plugin AND
567
+ form, so it cannot widen by accident; an instruction file or a runtime snapshot arriving under
568
+ any other shape still screens. `isHumanAuthored` deliberately stays strict, because the topic-
569
+ signature layer is what decides chapter boundaries and the r28 lesson was that injected text
570
+ poisons that signal. The archive gains a fact about the session; the index gains nothing it
571
+ should not have.