@treeseed/dsh-chapters 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cordis.patch.yml CHANGED
@@ -10,17 +10,41 @@
10
10
  # Two planes, two surfaces:
11
11
  # HOST PLANE — the `config:` block below (defaults shown commented; set any
12
12
  # under your profile's plugin entry).
13
- # REALM PLANE — the compaction ENGINE row in the preset
14
- # (presets/chapters/agent.cordis.yml); shared names behave per
13
+ # REALM PLANE — the compaction ENGINE row inside the preset's composition (the
14
+ # `preset-chapters` row at the foot of this file); shared names behave per
15
15
  # plane. Design record: docs/knowledge-repo.md §12.
16
16
  #
17
+ # PRESET DELIVERY — the preset is a DECLARATION ROW, exactly like the host's own presets.
18
+ # `preset-chapters` (the id is the host convention `preset-<id>`) is inserted by this
19
+ # file, its `config` IS the preset definition (`id/name/description/order/plugins`), and
20
+ # because it is an ordinary Loader entry you can see it in the plugin list and the config
21
+ # editor, override it from your profile patch, or `disabled: true` it.
22
+ # Its row module is ours — '@treeseed/dsh-chapters/preset' (src/preset.ts) — not the
23
+ # host's '@deepseek-ai/dsh-agent-preset'. It mirrors that plugin line for line (injects
24
+ # `agentPresets`, preserves child `!!js` expressions, yields the registry disposer to
25
+ # Cordis) and adds one thing: rows this host cannot import are dropped at boot and NAMED,
26
+ # because a single unresolvable child row makes the WHOLE preset `broken` — which is the
27
+ # same user-visible failure as no preset at all (measured: `DECLARED BUT BROKEN —
28
+ # tool-schedule … never started` on a host where that package was never published). The
29
+ # host package cannot be named directly because a row that fails to import aborts the
30
+ # plugin tree of any host predating declaration rows; ours resolves everywhere our
31
+ # package runs. Support floor: 0.2.0.
32
+ # After activation the plugin then ASKS THE ROSTER, and the mounted rows: file presence
33
+ # is not preset existence, and "declared" is not "usable". The `dsh-chapters:` lines at
34
+ # startup say which of the four states you are in.
35
+ #
17
36
  # config:
18
- # # ---- identity & install --------------------------------------------
19
- # installChaptersPreset: true # copy presets/chapters into the home's
20
- # # .agent-presets at boot; a user-edited
21
- # # preset is never clobbered (write-once)
22
- # fallbackPreset: 'chapters' # preset mounted for children when the
23
- # # caller's own preset cannot be resolved
37
+ # # ---- identity -------------------------------------------------------
38
+ # installChaptersPreset: true # INERT since 0.1.18 — the preset is the `preset-chapters`
39
+ # # row above, so `disabled: true` on THAT row is the switch.
40
+ # # Kept so an existing profile patch still parses.
41
+ # compactionThresholdRatio: 0.9 # INERT since 0.1.18 for the same reason: it is
42
+ # # `thresholdRatio` on the preset's chapters-engine row now.
43
+ # # fallbackPreset: 'chapters' # preset mounted for children when the
44
+ # # caller's own preset cannot be resolved — and
45
+ # # the id the roster check looks for. This id is
46
+ # # DURABLE STATE: every child's log records it,
47
+ # # so renaming it orphans those sessions.
24
48
  # harnessId: '<hostname>' # per-machine identity: git commit author,
25
49
  # # token scoping, edits/<harness>/ curation
26
50
  # # partition, rule-approval authorship
@@ -98,3 +122,182 @@
98
122
  - insert:
99
123
  - id: '@treeseed/dsh-chapters'
100
124
  name: '@treeseed/dsh-chapters'
125
+
126
+ # ── the `chapters` agent preset — a DECLARATION ROW, exactly as @deepseek-ai/dsh-web-app
127
+ # ships its own presets/standard.patch.yml. Row id `preset-chapters` follows the host's
128
+ # `preset-<id>` convention; `config.id: chapters` is DURABLE STATE (every continuation
129
+ # child records it as `agent-preset/selected`, so renaming it orphans those sessions).
130
+ #
131
+ # The row's `name` is OUR plugin (`@treeseed/dsh-chapters/preset`), which mirrors
132
+ # @deepseek-ai/dsh-agent-preset line for line — `inject: ['agentPresets']`,
133
+ # `static [EntryGroup.key] = true` so child `!!js` expressions survive until the child
134
+ # activates, `[Service.init]` yields the registry disposer so Cordis owns its lifetime —
135
+ # and drops rows THIS host cannot import before registering. Why not the host package
136
+ # directly: a row naming @deepseek-ai/dsh-agent-preset is unresolvable on a pre-0.1.7
137
+ # host and an unresolvable row aborts that host's whole plugin tree, while one stale
138
+ # child row (e.g. @deepseek-ai/dsh-tool-schedule, unpublished at 0.2.0-rc.2) makes the
139
+ # ENTIRE preset `broken` — measured both ways. Same row, same visibility in
140
+ # `dsh --dump-default-config`/the config editor, no boot that it can brick.
141
+ #
142
+ # Four deltas from the host's own standard list, each ours deliberately:
143
+ # 1. compaction-basic -> chapters-engine (the zero-inference summarize())
144
+ # 2. tool-result-pruner.thresholdChars 8192 -> 1000000 (dormant by choice: chapters
145
+ # defer oversized results at ARCHIVE time; an in-surface prune is a prefix cliff
146
+ # on local hardware — and that plugin REJECTS unknown config keys, so the three
147
+ # keys below are the whole surface)
148
+ # 3. the PLOT: persona suffix, with NO {{cwd}} (hosts from 0.2.1-alpha.2 register only
149
+ # provider/model and an unregistered variable throws at prompt assembly, killing
150
+ # every turn of every chapters session)
151
+ # 4. tool-ralph ENABLED (the host standard list mounts it disabled)
152
+ # `noUnusedLocals`-equivalent discipline applies: keys the host dropped between versions
153
+ # are not carried here (toolFilter was one — alpha.4 throws on it).
154
+ - insert:
155
+ - id: preset-chapters
156
+ name: '@treeseed/dsh-chapters/preset'
157
+ config:
158
+ id: chapters
159
+ name: Chapters
160
+ description: Full coding agent; context relief via zero-inference chapter compaction.
161
+ order: 0
162
+ plugins:
163
+ - id: persona
164
+ name: '@deepseek-ai/dsh-persona'
165
+ config:
166
+ prefix: You are a coding agent powered by the {{model}} model.
167
+ suffix: "When a task spans multiple turns, end each reply with one line beginning 'PLOT:' (max 60 words — objective, current hypothesis, immediate next step). A checkpoint carries this plot forward; if it looks stale, revise it on your next plot line."
168
+ - id: agent-instructions
169
+ name: '@deepseek-ai/dsh-agent-instructions'
170
+ config:
171
+ maxBytes: 65536
172
+ - id: time-context
173
+ name: '@deepseek-ai/dsh-time-context'
174
+ - id: tool-bash
175
+ name: '@deepseek-ai/dsh-tool-bash'
176
+ disabled: !!js process.platform === 'win32'
177
+ - id: tool-pwsh
178
+ name: '@deepseek-ai/dsh-tool-pwsh'
179
+ disabled: !!js process.platform !== 'win32'
180
+ - id: tool-fs
181
+ name: '@deepseek-ai/dsh-tool-fs'
182
+ - id: tool-fs-search
183
+ name: '@deepseek-ai/dsh-tool-fs-search'
184
+ config:
185
+ sampleOverCapGlobResults: false
186
+ - id: tool-jobs
187
+ name: '@deepseek-ai/dsh-tool-jobs'
188
+ - id: tool-schedule
189
+ name: '@deepseek-ai/dsh-tool-schedule'
190
+ - id: skill-filesystem
191
+ name: '@deepseek-ai/dsh-skill-filesystem'
192
+ - id: tool-skill
193
+ name: '@deepseek-ai/dsh-tool-skill'
194
+ - id: command-goal
195
+ name: '@deepseek-ai/dsh-command-goal'
196
+ - id: tool-goal
197
+ name: '@deepseek-ai/dsh-tool-goal'
198
+ - id: planning
199
+ name: cordis:group
200
+ group: true
201
+ isolate:
202
+ planMode: true
203
+ config:
204
+ - id: plan-mode
205
+ name: '@deepseek-ai/dsh-plan-mode'
206
+ config:
207
+ section: |
208
+ You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
209
+
210
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
211
+
212
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
213
+
214
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
215
+
216
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
217
+
218
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
219
+ - id: compaction
220
+ name: cordis:group
221
+ group: true
222
+ isolate:
223
+ compaction: true
224
+ toolResultPruner: true
225
+ config:
226
+ - id: chapters-engine
227
+ name: '@treeseed/dsh-chapters/engine'
228
+ config:
229
+ thresholdRatio: 0.9
230
+ - id: command-compact
231
+ name: '@deepseek-ai/dsh-command-compact'
232
+ - id: tool-result-pruner
233
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
234
+ config:
235
+ thresholdChars: 1000000
236
+ headChars: 4096
237
+ tailChars: 1024
238
+ - id: delegation
239
+ name: cordis:group
240
+ group: true
241
+ isolate:
242
+ workflowEngine: true
243
+ config:
244
+ - id: tool-subagent-control
245
+ name: '@deepseek-ai/dsh-tool-subagent-control'
246
+ - id: tool-subagent-list-agents
247
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
248
+ - id: tool-subagent
249
+ name: '@deepseek-ai/dsh-tool-subagent'
250
+ config:
251
+ provider: spawn
252
+ toolName: subagent
253
+ modelSelectionSettings: true
254
+ backgroundMode: continuable
255
+ - id: tool-subagent-fork
256
+ name: '@deepseek-ai/dsh-tool-subagent'
257
+ config:
258
+ provider: fork
259
+ toolName: subagent_fork
260
+ backgroundMode: continuable
261
+ - id: tool-subagent-codex
262
+ name: '@deepseek-ai/dsh-tool-subagent'
263
+ disabled: true
264
+ config:
265
+ provider: codex
266
+ toolName: subagent_codex
267
+ backgroundMode: one-shot
268
+ maxDepth: provider-managed
269
+ - id: tool-subagent-claude-code
270
+ name: '@deepseek-ai/dsh-tool-subagent'
271
+ disabled: true
272
+ config:
273
+ provider: claude-code
274
+ toolName: subagent_claude_code
275
+ backgroundMode: one-shot
276
+ maxDepth: provider-managed
277
+ - id: workflow-ptc
278
+ name: '@deepseek-ai/dsh-workflow-ptc'
279
+ config:
280
+ provider: spawn
281
+ - id: tool-workflow
282
+ name: '@deepseek-ai/dsh-tool-workflow'
283
+ - id: tool-ralph
284
+ name: '@deepseek-ai/dsh-tool-ralph'
285
+ config:
286
+ subagentProvider: spawn
287
+ maxRounds: 64
288
+ - id: tool-ask-user
289
+ name: '@deepseek-ai/dsh-tool-ask-user'
290
+ - id: tool-todo
291
+ name: '@deepseek-ai/dsh-tool-todo'
292
+ config:
293
+ allowParallelInProgress: true
294
+ - id: tool-web
295
+ name: '@deepseek-ai/dsh-tool-web'
296
+ config:
297
+ fetch: true
298
+ searchTimeoutMs: 60000
299
+ - id: present
300
+ name: '@deepseek-ai/dsh-tool-present'
301
+ - id: tool-plugin-manager
302
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
303
+ disabled: true
package/docs/backlog.md CHANGED
@@ -95,3 +95,82 @@ about.
95
95
  machine-only when you expected the pool, that note is the first thing to read.
96
96
  - **`/chapters-continue` vs `/chapters-fork`** is the intent line: continue carries the operative plan
97
97
  and may seed plan mode; fork carries nothing.
98
+
99
+ ## Preset delivery on 0.1.7+/0.2.x — landed, and what it exposed (2026-10-16)
100
+
101
+ **Landed:** the `chapters` preset now EXISTS on a declaration-row host, by `agentPresets.register()` at
102
+ activation (`src/preset.ts`), with rows the host cannot import dropped and named, a held disposer, and a
103
+ deferred ROSTER check (`present` / `DECLARED BUT BROKEN` / `absent` / `unknown`) as the only health signal.
104
+ **Support floor is 0.2.0** (user decision 2026-10-16): the legacy `.agent-presets` copy, the
105
+ `presets/chapters/` tree, `installChaptersPreset`'s copy arm and `preset-install.test.ts` are DELETED; a
106
+ pre-0.2.0 host gets `delivery: REFUSED` with its tools intact. The engine row's `thresholdRatio` became
107
+ the host-plane config `compactionThresholdRatio` (no hardcoded tunables). Verified on real boots:
108
+ 0.2.0-rc.2 → `register: pruned, roster: present, inventoryRows: 28, unusableRows: []`;
109
+ 0.2.1-alpha.4 → `register: registered, roster: present, inventoryRows: 29, unusableRows: []`
110
+ (after dropping `toolFilter`, which alpha.4 THROWS on — see the config-key row of the drift test). `tests/unit/preset-declaration.test.ts`
111
+ (13 rows, `src/preset.ts` at 100%) is the drift gate, and it now RUNS on this box instead of skipping.
112
+
113
+ **Why the row is not a shipped patch layer** — measured, because it looks like the obvious fix and it is a
114
+ boot-breaking one: `dsh.bundle.patch` as an ARRAY throws `ERR_INVALID_ARG_TYPE` at
115
+ `dsh-app-boot/lib/index.js:853` on every host before 0.1.7-alpha.1, and a row naming
116
+ `@deepseek-ai/dsh-agent-preset` aborts that host's plugin tree (`Cannot find package`). `disabled: !!js`
117
+ cannot gate it either: the Loader evaluates `disabled` before services mount, so the row is disabled on the
118
+ NEW host too (measured). Full record: `docs/contract.md` § Presets are declaration rows.
119
+
120
+ **Open, in order:**
121
+
122
+ 0. **Re-anchor the e2e host, then re-distill the tapes.** `tests/e2e/boot.ts` now refuses by name
123
+ while `dev/dsh-host-lock` pins a pre-0.2.0 host, and the refusal lifts itself when the lock moves —
124
+ that is the upgrade path, not a TODO. `ci.yml`'s `host-capability` job reads that same lock and
125
+ SKIPS the acceptance job with a step-summary reason plus a `::warning::` while the pinned host
126
+ predates declaration rows: visibly skipped, never red-by-accident, never green-by-omission.
127
+ Publishing is unaffected — the release gate is the deterministic suite. The per-boot row pins (`thresholdRatio`, `enrichmentEnabled`,
128
+ `auto`, `elicitedPlot`, `toolResultArtifactTokens`) and the `[e2e] boot pins:` line were deleted with
129
+ the YAML they patched; they come back expressed as CONFIG on the registered row (`compactionThresholdRatio`
130
+ exists now; the rest need the same treatment). Until then the browser acceptance chain does not run, and
131
+ `docs/verify.md`'s P23 is retired rather than passing.
132
+ 1. **The roster verdict is a boot log line, and a user has no terminal.** 0.1.18 made the preset a
133
+ visible Loader row (`preset-chapters`) and put the mounted-row count in that line, so the
134
+ "is it actually usable" fact is one grep away — but it is still a grep. Candidate: surface it in
135
+ `/chapters-status`. Nothing else in the plugin reports health to a human today.
136
+ 2. **`installChaptersPreset` and `compactionThresholdRatio` are accepted-and-inert** since 0.1.18 (the
137
+ preset is a row; the engine threshold is row config). They must be DELETED in the next minor — a key
138
+ that silently does nothing is its own drift — and the removal needs the same care: check no shipped
139
+ profile patch sets them, then drop both from `Config` and from `cordis.patch.yml`'s header.
140
+ 2. **`compositionInventory()` is used by the dev witness only.** Making the boot check itself compare
141
+ mounted rows would catch a row that resolves but never activates — the `broken` flag does not always
142
+ cover that, and the witness proves the shape of the answer already.
143
+ 3. **A reset registry can overwrite a chapter — no guard exists today.** `writeArchive`
144
+ (`src/archive.ts:106`) reserves numbers from the registry and writes them; nothing checks whether that
145
+ path already exists on disk. So the exact incident shape — registry gone (host reinstall) while chapter
146
+ files remain, or restored by rehydration — lets a NEW archive reuse number 001 and clobber a surviving
147
+ file, which breaks the append-only rule from the inside. Two rows are missing: at boot, report files the
148
+ registry does not know (`<store>/<root>/chapters/*.md` vs registry rows, counted); and refuse a
149
+ reservation whose target path exists, with the numbers.
150
+ 4. **Rehydration restores bytes, not rows.** `rehydrateStoreFromMirror` (`src/sync.ts:476`) fills absent
151
+ chapter and artifact files only. Search works off the files' own frontmatter, but numbering, ancestry and
152
+ collections are registry state, so a rehydrated store can be searched and cannot be continued cleanly.
153
+ Decide: rebuild rows from frontmatter (honest — the bytes are the ground truth), or refuse continuation
154
+ over a partially restored store with numbers. Do not let it look like a fresh chain.
155
+
156
+ ## Host bug we cannot fix: the workspace directory-picker modal (2026-10-16)
157
+
158
+ Reported by the user as "the directory selection modal crashes before I can select a workspace", and it
159
+ survived a host upgrade. Root-caused with real boots (`var/picker-probe/FINDINGS.md`, kept out of the
160
+ repo's claims — it is host code): the picker's CLIENT bundle and the shell's client module table must come
161
+ from the same host generation. The 0.1.x picker asks for `IconFolderOpen16`/`IconFolderClose16`;
162
+ 0.2.0-rc.2's shell exports only the `…Regular` spellings, with no compat alias in either direction. The
163
+ slot boundary catches it, so the symptom is a rendered-then-dead dialog:
164
+ `Error: Minified React error #130 … got: undefined`, logged as
165
+ `slot entry crashed in 'sidebar.workspaces.directoryFlow'`
166
+ (`@deepseek-ai/dsh-client-ui-renderer/lib/client.js:618`). A stale profile-local copy of
167
+ `@deepseek-ai/dsh-client-ui-directory-picker-*` under `$DSH_HOME/profiles/<p>/node_modules` shadows the
168
+ upgraded global install and reproduces "the new host did not fix it" exactly.
169
+
170
+ Nothing in this plugin contributes to that slot (its client half registers
171
+ `conversation.chat.assistant-actions` only), so the repo can ship no fix — only the recipe, which the
172
+ site documents: purge the stale picker copies, and if the auto-resolved backend is not the one you want,
173
+ pin the pair deliberately in the profile patch (`disabled: true` on `directory-picker`, insert
174
+ `@deepseek-ai/dsh-host-directory-picker-browse` + `@deepseek-ai/dsh-client-ui-directory-picker-browse`).
175
+ Upstream ask: compat aliases for the renamed icon exports, or a boot-time shell↔plugin version handshake
176
+ that rejects a mismatched bundle instead of crashing at render.
package/docs/contract.md CHANGED
@@ -244,12 +244,13 @@ The subclass seam, with everything measured by rounds 12-17 (sources: the instal
244
244
  `resolveConfig` **rejects unknown keys at runtime** (`BasicCompactionConfig:
245
245
  unknown key "artifactStoreRoot"`, r18) — peel engine-specific keys in the
246
246
  subclass constructor before `super(ctx, baseConfig)`.
247
- - Preset delivery: profile `!!js` evaluates `new Function("ctx", …) with (ctx)` —
248
- `dshHomePath`/`process` reachable, **`require` is not** (r20 boot-killer) — so a
249
- patch cannot name a package-relative `agentPresets` roots path. The plugin
250
- copies its `presets/chapters/` into `$DSH_HOME/.agent-presets/` at boot,
251
- write-if-missing (`installChaptersPreset`, schemastery default import —
252
- version line `@deepseek-ai/schemastery@3.x`, NOT host rc.x).
247
+ - Preset delivery: the plugin registers the `chapters` declaration row at activation
248
+ (`src/preset.ts` -> `ctx.get('agentPresets').register(...)`). The old copy of
249
+ `presets/chapters/` into `$DSH_HOME/.agent-presets/` is GONE with the 0.2.0 support
250
+ floor; the section "Presets are declaration rows" records why the row is not a shipped
251
+ patch layer (`!!js` evaluates `disabled` before services mount, and an array
252
+ `dsh.bundle.patch` throws on a pre-0.1.7 host). Related: profile `!!js` reaches
253
+ `dshHomePath`/`process` but NOT `require` (r20 boot-killer).
253
254
  - `ctx.logger` output is invisible to headless `dsh web` (r19); engine failures
254
255
  mirror to `$DSH_CHAPTERS_ENGINE_ERRORS` when set. Engine finalization runs on
255
256
  BOTH the success and rejection paths of `compactIfNeeded`/`compactNow` — the
@@ -354,6 +355,92 @@ Entry `apply(ctx, config)`. Build with `tsc -p tsconfig.json` and **build before
354
355
  UI (later phase) additionally needs `dsh.client` and a `tsdown.client.config.ts` — see
355
356
  `dsh-session-fork/package.json` for the shape.
356
357
 
358
+ ### Presets are declaration rows now (host ≥ 0.1.7-alpha.1) — measured, with the incident
359
+
360
+ A user preset used to be a directory `$DSH_HOME/.agent-presets/<id>/` holding `preset.yml` +
361
+ `agent.cordis.yml`. The host's own migration note is blunt: *"Nothing reads that directory any more."*
362
+ (`@deepseek-ai/dsh-agent-preset/skills/editing-cordis-compositions/SKILL.md`). A preset is now an
363
+ `@deepseek-ai/dsh-agent-preset` **declaration row** (`config: {id, name?, description?, order, plugins}`;
364
+ `dsh-agent-preset/lib/index.js:13-20`) carried by a patch layer, and `AgentPresetRegistry.register()` is
365
+ fed only by such rows (`dsh-agent-preset-registry/lib/index.js:470`, installed 0.2.0-rc.2). Nothing
366
+ crashes when a plugin misses this: the bundle boots, its tools register, Settings says "enabled and
367
+ running", the preset is simply absent — and a session's preset id is **durable state** (`resumeObserved()`
368
+ → `presets.resolve(id)`, `dsh-api-session-controller/lib/index.js:404`, `:364`; `resolve()` throws
369
+ `agent-preset/not-found`, `dsh-agent-preset-registry/lib/index.js:596`), so every chapters session refuses
370
+ to OPEN while its log sits there intact.
371
+
372
+ **This bundle DOES ship the row — in its one existing patch layer, with our own row module.**
373
+ `cordis.patch.yml` ends with `- insert: [{id: preset-chapters, name: '@treeseed/dsh-chapters/preset',
374
+ config: <the preset definition>}]`, so the preset is an ordinary Loader entry: visible in the plugin
375
+ list and `dsh --dump-default-config`, overridable or disable-able from a profile patch, positioned by
376
+ `config.order`. The row module (`src/preset.ts`) mirrors `@deepseek-ai/dsh-agent-preset`'s plugin line
377
+ for line and adds the pruning described below. Why not the host package directly, measured on the
378
+ 0.1.5-rc.1 closure and on 0.2.0-rc.2:
379
+
380
+ - `dsh.bundle.patch` as an **array** (what a second layer requires) dies at profile load on a pre-0.1.7
381
+ host: `TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received an
382
+ instance of Array` (`dsh-app-boot/lib/index.js:853`, `join(packageDir, declared)`). Array support and
383
+ declaration rows arrive in the SAME release, 0.1.7-alpha.1 — so one layer, still a string, is both
384
+ enough and the only boot-safe shape.
385
+ - a row naming `@deepseek-ai/dsh-agent-preset` in ANY layer a legacy host reads aborts its plugin tree:
386
+ `plugin tree failed to load: … failed to import loader entry … Cannot find package` (the package is
387
+ first published at 0.1.7-alpha.1). A row naming our own module resolves wherever our package runs.
388
+ - **`disabled: !!js` cannot capability-gate a root row**: the Loader evaluates `disabled` before services
389
+ are mounted (`cordis-plugin-loader/lib/index.js:348` → `with (ctx) { eval(expr) }`), so
390
+ `get('agentPresets')?.register === undefined` is true on the MODERN host too and the row never
391
+ registers — measured (`preset-v2 … failed to import`, roster without it, while the same row with
392
+ `disabled: !!js false` registered normally). Bare `get(...)` also throws there; only `ctx` is bound.
393
+ - and the row must be *ours* anyway for the reason below: it is the one place that can drop a child the
394
+ host cannot load before the mount is attempted.
395
+
396
+ Three consequences of reading the registry rather than the filesystem:
397
+
398
+ - **A row that does not resolve is dropped, never mounted.** `mountPreset` throws if ANY enabled row failed
399
+ (`dsh-agent-preset-registry/lib/index.js:272`), the registry stores it as `record.broken` (`:541`), and
400
+ `retain()` then refuses with `agent-preset/invalid` (`:651`) — so one stale row makes the whole preset
401
+ unusable, which is the same user-visible failure as a missing preset. Measured on 0.2.0-rc.2 with a row
402
+ for `@deepseek-ai/dsh-tool-schedule` (published only from 0.2.1-alpha.1):
403
+ `preset "chapters" is DECLARED BUT BROKEN — tool-schedule …: never started`. `pruneResolvableRows`
404
+ (`src/preset.ts`) drops such rows and NAMES them in the mount line; `resolvableFromThisPackage` is the
405
+ probe, asked with the row's full specifier so a lost subpath export is caught too.
406
+ - **A held disposer, not an effect.** `register()` returns an unregister disposer, and wiring it through
407
+ `ctx.effect` un-registers the preset the moment our activation finishes — measured: `register()`
408
+ succeeded while the roster came back WITHOUT `chapters`. The same apply-fiber trap bit the storage
409
+ domain in r35. The disposer is held in a module array instead.
410
+ - **The roster is the health check, and it must be asked after activation.** The registry re-audits a mount
411
+ by awaiting the settled loader tree (`:562`), and its doc says callers "must not run inside a Host row's
412
+ own activation, which the settlement would wait on". So `reportPresetRoster()` runs deferred, and
413
+ `compositionInventory()` rows say which mounted rows are actually `active` — `broken` alone hides a row
414
+ that resolved but never started.
415
+
416
+ Rows are renamed and dropped between host versions, so re-anchor `src/preset.ts` against the installed
417
+ host's own `@deepseek-ai/dsh-web-app/presets/standard.patch.yml` after an upgrade. Measured drift across
418
+ the five published versions (rc.1, rc.2, alpha.1, alpha.2, alpha.4): alpha.1 ADDS `dsh-time-context` +
419
+ `dsh-tool-schedule` + `toolFilter.deny[schedule_*]`; alpha.2 DROPS the persona `suffix` (with its
420
+ `{{cwd}}`), `backgroundMode`, and the codex/claude-code/ralph rows; alpha.4 rewrites the plan-mode
421
+ sentence, drops `toolFilter`, adds `dsh-plugin-market`. rc.1 and rc.2 are byte-identical.
422
+ `tests/unit/preset-declaration.test.ts` is what turns the next rename into a red row instead of a broken
423
+ user session — and it pins that `dsh.bundle.patch` is still a string.
424
+
425
+ - **Prompt variables are a closed set, and violating them throws per turn, not at boot.** `dsh-agent-loop`
426
+ registers `provider`/`model`/`cwd` through 0.2.0-rc.2 (`lib/index.js:1564-1566`) and only `provider` and
427
+ `model` from 0.2.1-alpha.2 (`:1591-1592`); `interpolate()` rejects any other reference with `prompt
428
+ variable "{{x}}" has no value for this assembly` (`dsh-system-prompt/lib/index.js:171`). There is no
429
+ `{{cwd}}` worth writing: `dsh-working-directory` (host plane) already prints `Current working directory:
430
+ "…"` into the runtime context. This is what broke every new chapters session immediately AFTER the
431
+ delivery fix above — the preset composed fine, the legacy `persona.suffix` interpolated `{{cwd}}`, and
432
+ assembly threw on every turn. Pinned by the `{{variable}}` row of the test.
433
+ - **Some host plugins reject unknown config keys**, so a preset row's config is not free-form:
434
+ `dsh-compaction-tool-result-pruner`, `dsh-compaction-basic` and `dsh-plan-mode` throw on any key they do
435
+ not know (`PlanModeConfig has unknown key(s) …`); `dsh-tool-subagent` and `dsh-tool-ralph` are plain zod
436
+ objects that strip them. Keep exactly `{thresholdChars, headChars, tailChars}` on the pruner and exactly
437
+ `{section}` on plan mode.
438
+ - **Persona wording is load-bearing for the engine.** `src/engine-core.ts`'s `PLOT_TEMPLATE_ECHO` suppresses
439
+ a "plot" that is really our own instruction quoted back (the 2026-09-25 audit: eight checkpoints of a
440
+ real run inherited boilerplate, and because the result was non-null the bounded elicited fallback never
441
+ ran). It matches phrases inside the suffix, so reword the suffix and the guard in the same change — the
442
+ gate asserts that coupling by behaviour, not by copying the regex.
443
+
357
444
  ## Trigger Constraints
358
445
 
359
446
  Two different actions, two different timings — Phase 0b measured the seam so this split is now factual,
@@ -355,7 +355,7 @@ happened in these notes before it was caught.
355
355
  | `src/notice.ts` + `test/notice.test.ts` — TOC-notice builder, kernel-rejection reproduction, vocabulary pins + drift guard (Phase 0b check 5) | **done**, 10 tests |
356
356
  | `src/registry.ts` + `src/store.ts` — pure numbering/ancestry/plan state; zod domain spec, storage-domain + node-fs adapters | **done**, 13 + 7 tests; durability witness passed |
357
357
  | `src/engine-core.ts` + `src/engine.ts` — ChaptersCompactionEngine | **done & boot-proven** (r18/r19: realm subpath row mounts; real cascade finalizes; catch-path fix — FINDINGS § Phase 1) |
358
- | `presets/chapters/` + copy-on-boot install | **done & boot-proven** (r21; `!!js` has no `require`, so copying is the delivery mechanism — r20) |
358
+ | preset delivery | **0.1.18: the preset IS a declaration row** — `preset-chapters` in `cordis.patch.yml`, mounted by `@treeseed/dsh-chapters/preset` (`src/preset.ts`), which registers what the host can load and drops what it cannot. The `presets/chapters/` + copy-on-boot mechanism is deleted with the 0.2.0 floor. Verify.md P24 (rows + three-host boots) |
359
359
  | `npm test` (unit + integration) | **188 passing, <2 s** — unit imports no harness services; integration runs the sync loop against a fake git driver AND a real git smart-HTTP server (git-http-backend on loopback; the git binary is test-only scaffolding, the product stays pure-JS) |
360
360
  | 20-check verify.md pass | **r26/26b**: 18 mechanical on `.dshdev2` (refusals with numbers, tamper marks, idempotent retry, cross-boot registry + resume); human rows per verify.md evidence map |
361
361
  | dsh-session-fork coexistence | **moot at this version** (r26a pair-boot): the example fails to boot the installed host ALONE (unguarded `webServer` read in the installed dsh-client-connection) — assumed-neutral until a version-matched pair-boot |
@@ -465,3 +465,66 @@ commands were *verified*.
465
465
 
466
466
  `docs/contract.md`'s file:line citations are from the `examples/` snapshot (rc.1-era), not the rc.2 fork —
467
467
  re-anchor them against whichever version a test actually boots.
468
+
469
+ ### Running the TS tests on this box (node 22, no native type stripping)
470
+
471
+ Measured 2026-10-16: `npm test` (`node --test tests/unit/*.test.ts`) needs node ≥ 23 for type
472
+ stripping, and this machine's `/usr/bin/node` is **v22.22.1 compiled WITHOUT TypeScript support**
473
+ (`ERR_NO_TYPESCRIPT`, not merely a missing flag). The package's `engines` floor is node 24, and CI
474
+ installs it; locally, run the same files through `tsx`:
475
+
476
+ ```bash
477
+ npm install --no-save --no-audit --no-fund tsx unrun # both, in ONE command
478
+ node --import tsx --test tests/unit/*.test.ts tests/integration/*.test.ts
479
+ npx c8 --check-coverage --statements 85 --branches 85 --functions 85 --lines 85 \
480
+ node --import tsx --test tests/unit/*.test.ts tests/integration/*.test.ts
481
+ ```
482
+
483
+ Two traps measured while finding that:
484
+
485
+ - **`npm install --no-save A` then `--no-save B` removes A.** npm prunes what is not in the manifest,
486
+ so unsaved helpers must be installed in a single command (`unrun` disappeared the moment `tsx` was
487
+ added, and `npm run build` — which bundles the client through `tsdown` — failed with
488
+ `Cannot find module "unrun"`).
489
+ - `tsconfig.test.json` (committed) transpiles `src` + `tests` to `var/tsbuild` with `--noCheck` for
490
+ the rare case you want emitted JS instead of a loader. It is NOT how the suite is meant to run;
491
+ tests import `.ts` sources directly and are not typechecked by design (`npm run typecheck` covers
492
+ `src/` only).
493
+
494
+ Never fix a missing test runner by editing the tests. The suite is the contract; the runner is local.
495
+
496
+ ### Verifying against a host version that is not the installed one
497
+
498
+ A preset/delivery change must be proven on EVERY host release the package claims to serve, and the
499
+ installed host is one data point. Recipe measured 2026-10-16 (this is how 0.1.17 was verified on
500
+ 0.2.0-rc.2, 0.2.1-alpha.4 and the pinned 0.1.5-rc.1 in one sitting):
501
+
502
+ ```bash
503
+ # 1. a second host, installed as its own tree — never over the running one
504
+ mkdir -p var/host-versions/a4 && cd var/host-versions/a4
505
+ echo '{"name":"a4-host","private":true,"dependencies":{"@deepseek-ai/dsh":"0.2.1-alpha.4"}}' > package.json
506
+ npm install --no-audit --no-fund # ~1 min, 616 packages
507
+ # 2. a THROWAWAY home for it (profiles/web/package.json listing the bundles + this plugin),
508
+ # then boot THAT bin with DSH_HOME pointed at the new home:
509
+ DSH_HOME=$PWD/var/a4-home DSH_CHAPTERS_WITNESS=$PWD/var/a4-home/w.json \
510
+ node var/host-versions/a4/node_modules/@deepseek-ai/dsh/lib/bin.js web --port 0 --no-open
511
+ ```
512
+
513
+ The witness JSON is the answer — `presetRegister`, `presetDroppedRows`, `presetRoster` and
514
+ `presetInventoryRows`/`presetUnusableRows` say what a log line only hints at, and `presetUnusableRows: []`
515
+ is the assertion that the composition actually ACTIVATES rather than merely being declared.
516
+
517
+ Three traps, all paid for:
518
+
519
+ - **Never symlink a host tree into an npm-managed profile's `node_modules`.** `npm install` treats what it
520
+ does not recognize as extraneous and **deletes it through the link** — it ate a 500-package host closure
521
+ this way ("the closure is gone, `ls` returns nothing"). Copy instead (`cp -r`), or point the boot's own
522
+ installation anchor at the other tree and install ONLY the plugin into the profile.
523
+ - **`npm pack` + install-from-tarball, not `file:`-linked sources.** The artifact users get is the packed
524
+ `files` list; a linked working tree hides a missing `files` entry (e.g. an asset the preset needs).
525
+ - Boot with `timeout 45 …` and read the exit code: **124 means it stayed up** (healthy), `1` means the
526
+ plugin tree refused to load. A crashed boot and a slow boot both print to the same log; the code is the
527
+ difference between "no preset" and "no harness".
528
+
529
+ Prune after: `var/host-versions/`, the throwaway homes and `var/coverage` are all regenerable in minutes and
530
+ together cost ~2 GB — this repository's contract is the *measurements written down*, not their scratch.
@@ -4,7 +4,7 @@ Written 2026-10-09. **Read before writing code.** Human decisions for this featu
4
4
  § Decisions Taken and are binding; anything marked *deferred* is a human call, not ours.
5
5
 
6
6
  DSh ships plan mode (`@deepseek-ai/dsh-plan-mode`) and our own preset mounts it
7
- (`presets/chapters/agent.cordis.yml:109-133`). Today a session plan survives an archive only as the
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
8
  first 400 characters of a tool-call line. That is the worst possible outcome for the one artifact in a
9
9
  conversation that is *deliberately* decision-complete — and the reason the plugin exists at all: a
10
10
  continuation that cannot see the approved plan either re-plans from recall (lossy) or implements
package/docs/verify.md CHANGED
@@ -152,7 +152,8 @@ renderer, the segmentation tool, and the child's notice.
152
152
  | P10 | Artifact citations resolve wherever they were written | a chapter's `artifacts/aa/<sha>.txt` reference resolves whether the blob sits at the STORE ROOT (arrival stubs) or inside the ANCESTRY TREE (`<store>/<root>/artifacts/…`, where `writeArchive` puts deferred results and every plan) — before this fix the second form was unreadable through `chapters_artifact`, which is what "every byte remains retrievable" actually promised | `artifact-query.test.ts` rows + integration `plans-carriage.test.ts` (`resolveArtifactWithMirror` over the real tree) |
153
153
  | P11 | The browser plane runs in the chain | `/plan <message>` → a real `exit_plan_mode` call → the host's review card opened and answered by a real CLICK → fork; asserts the submission's bytes are the artifact's bytes (or provably only-redacted), `planMode: inactive` from the fold, a `kind: plan` document on disk, and that the BRANCH child carries **no** `## Active plan` and no call-out while still citing the archive; plus a guard that no `compaction/*` entered the journey | `tests/e2e/plans.spec.ts` = chain project `plans` (13 s replay, no LLM; 3-entry tape). **Scope, learned at cost:** the continuation leg was REMOVED from this journey because a tape freezes the model's tool ARGUMENTS — my scripted upstream derived `chapters_continue` ranges from the ceiling the live segment tool reported while distilling (30 here), and the runner's session lands at 29, so `validateRanges` refused the stale input (correctly). A journey whose recorded request embeds a machine-specific seq cannot be hardware-neutral. Continuation semantics — `## Active plan`, `## Previous plans`, the seeded `plan/mode`, the checkpoint carry — are covered by the deterministic plane (`continue-plans`, `plans-engine`, `plan-documents`, the § 10 kernel probe) and the in-place path by the heavy/arrival/enrich/fanout journeys. Tape provenance: scripted upstream, see `tests/fixtures/model-tape/plans/PROVENANCE.md` |
154
154
 
155
- | P23 | A partially-wiped preset **self-heals and says so** | `installChaptersPreset(srcDir, dstDir, log, parent)` validates the **whole shipped file set**, not just `preset.yml`; missing shipped files are restored **additively** (files the user already has are never overwritten, so edits survive); statuses `installed / repaired / complete / failed / no-source` are returned, not just logged; the mount line now carries the status and logs **UNHEALTHY** on `failed` instead of claiming it mounted; the dev witness reports `presetComplete` + `presetMissing` (it had the same one-file blindness, in the very tool a user reaches for when this breaks) | unit `preset-install.test.ts` (7 rows, real temp dirs — the function previously hard-resolved its own paths so it was untestable): fresh install, complete-left-alone, **THE INCIDENT** (delete `agent.cordis.yml` → `repaired`, user's `preset.yml` edit preserved, warn names it), nested subdirectory, failure→`failed`+warn+error carried, source-without-manifest→`no-source` and no partial dir left |
155
+ | P23 | **Retired in 0.1.17 with the legacy copy** (support floor 0.2.0): `installChaptersPreset` and `presets/chapters/` are deleted, so there is no installed file set to half-delete. The key is ACCEPTED-and-inert since 0.1.18 — removing it would make an existing profile patch fail to parse, and a bundle that will not mount is worse than a key that does nothing | see P24 |
156
+ | P24 | The preset is a **visible declaration row**, and the row composes on every host in range | `preset-chapters` is inserted by our own `cordis.patch.yml` with the preset definition in its `config`, mounted by `@treeseed/dsh-chapters/preset` (`src/preset.ts`) — the visible Loader entry the host's own presets are, overridable and `disabled`-able from a profile patch, with the registration's lifetime yielded to Cordis (an `apply()`-scoped disposer un-registered it: measured as `register()` succeeding with the roster WITHOUT `chapters`). Rows the host cannot import are dropped and NAMED, because one unresolvable child makes the whole preset `broken` | 15 rows in `preset-declaration.test.ts`, **each verified to fail on the mutation it guards** (ralph re-disabled, an unknown pruner key, `toolFilter` restored, `{{cwd}}` restored, the durable id renamed) — plus real boots of the packed tarball on 0.2.0-rc.2 (`roster: present`, `inventoryRows: 28`, `unusableRows: []`, tool-schedule pruned), 0.2.1-alpha.4 (`registered`, `inventoryRows: 29`, nothing pruned) and 0.1.5-rc.1 (boots; the row says the host cannot register rows) |
156
157
  | P21 | The continuation has a **human-pulled door**, and the two doors cannot drift | `/chapters-continue` = `deterministicCommand({intent:'continue'})`, the same builder as `/chapters-fork`; only name/verbs/intent/no-directive prose differ; archive semantics identical (everything since the watermark). CLIENT SIDE, same discipline: the assistant row registers both doors from ONE runner (`src/client/doors.ts` `makeDoorRunner` + a two-row `DOOR_ACTIONS` table) — a composer-only continue left the user with a created child and no affordance | unit `continue-command.test.ts` (7 rows) incl. the **no-drift row**: same events in ⇒ same ranges, titles and plan facts out of both doors; the no-plan case, the refusal naming its own door, and approved ⇒ NOT seeded vs still-planning ⇒ seeded `['user/message','plan/mode']`. Unit `client-doors.test.ts` (7 rows) pins the two client rows (distinct id/order/label/command, the fork label frozen for the specs), that ONE child-id regex parses BOTH door wordings, that both rows fire their own command through the same body, and — by reading the server's own templates — that the reply wording the client parses is still what `deterministicCommand` emits. Browser: project `fork` asserts BOTH buttons render |
157
158
  | P22 | The continuation shape is covered **in the browser**, on every push | the leg that a tape could not carry (§ 15) is back, driven by the command — no model request, no frozen `endSeq`; asserted from the child's durable log without switching the UI: `## Active plan`, the operative plan by title with its real verdict, `plan document:` and `chapters_artifact` citations, `## Finding things`, no seeded gate after approval, exactly one synthetic notice (invariant 2) | `tests/e2e/plans.spec.ts` project `plans` — replays in ~14 s with no LLM/GPU, eight-project chain green |
158
159
  | P18 | Search answers **without a pool linked** — the findability audit | `buildLocalCorpus` walks the workspace store (chapters, `kind: plan` documents, rules) and answers through the SAME scorer as the mirror door (`rankEntries`), so ranking cannot drift between doors; artifacts are deliberately not indexed (they answer to `chapters_artifact`); the answer names its door ("searched this machine's store, N docs, no pool mirror") so a session never reads *this machine only* as *the whole project*; "nothing matched" (`documents>0`) is distinguished from "nothing here" (`empty`); a pool miss falls back to the local store before answering empty | unit `local-corpus.test.ts` (5) + integration `find-without-a-pool.test.ts` through the real tool wrapper — the empty answer used to be born in the wrapper, not the pure function |