@treeseed/dsh-chapters 0.1.17 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cordis.patch.yml CHANGED
@@ -10,36 +10,37 @@
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 inside the preset's composition
14
- # (src/preset.ts for hosts that register rows; for pre-0.1.7
15
- # hosts, presets/chapters/agent.cordis.yml); shared names behave
16
- # per plane. Design record: docs/knowledge-repo.md §12.
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
+ # plane. Design record: docs/knowledge-repo.md §12.
17
16
  #
18
- # PRESET DELIVERY — one mechanism, and the host floor is 0.2.0:
19
- # Hosts from 0.1.7-alpha.1 compose presets from '@deepseek-ai/dsh-agent-preset'
20
- # DECLARATION ROWS and read nothing in $DSH_HOME/.agent-presets. This plugin registers
21
- # the `chapters` row itself at activation, through the host's own agentPresets.register()
22
- # — the same call the host's declaration-row plugin makes (src/preset.ts; why not a
23
- # shipped patch layer is recorded there with the boot-breaking failures measured on
24
- # 0.1.5-rc.1). Rows this host cannot import are PRUNED and named in the mount line,
25
- # because ONE unresolvable row makes the whole preset broken and every chapters
26
- # session unopenable. Hosts below 0.2.0 have no register(): the mount line says
27
- # `delivery: REFUSED`, the tools still work, and the preset cannot exist there.
28
- # Either way the boot then ASKS THE ROSTER — file presence is not preset existence,
29
- # and "declared" is not "usable". Read the `dsh-chapters:` lines at startup.
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.
30
35
  #
31
36
  # config:
32
- # # ---- identity & install --------------------------------------------
33
- # installChaptersPreset: true # register the chapters declaration row at
34
- # # activation. Turning it off means a preset you
35
- # # declare yourself (a patch layer in this or
36
- # # another bundle); ours then stands down as
37
- # # "already-declared" rather than throwing a
38
- # # duplicate-preset error through our own row.
39
- # compactionThresholdRatio: 0.9 # the registered preset's engine row: share of the
40
- # # window that triggers a chapter checkpoint. This is
41
- # # the knob a preset YAML used to carry in the home.
42
- # fallbackPreset: 'chapters' # preset mounted for children when the
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
43
44
  # # caller's own preset cannot be resolved — and
44
45
  # # the id the roster check looks for. This id is
45
46
  # # DURABLE STATE: every child's log records it,
@@ -121,3 +122,182 @@
121
122
  - insert:
122
123
  - id: '@treeseed/dsh-chapters'
123
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
@@ -121,15 +121,22 @@ NEW host too (measured). Full record: `docs/contract.md` § Presets are declarat
121
121
 
122
122
  0. **Re-anchor the e2e host, then re-distill the tapes.** `tests/e2e/boot.ts` now refuses by name
123
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. The per-boot row pins (`thresholdRatio`, `enrichmentEnabled`,
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`,
125
128
  `auto`, `elicitedPlot`, `toolResultArtifactTokens`) and the `[e2e] boot pins:` line were deleted with
126
129
  the YAML they patched; they come back expressed as CONFIG on the registered row (`compactionThresholdRatio`
127
130
  exists now; the rest need the same treatment). Until then the browser acceptance chain does not run, and
128
131
  `docs/verify.md`'s P23 is retired rather than passing.
129
- 1. **The roster check is the only evidence we have that a preset composes — and it is a boot-time log
130
- line.** A user cannot see it without a terminal. Candidate: surface `delivery:` + the roster verdict in
131
- `/chapters-status` (and a warning row in the Settings plugin panel), so "installed and running" stops
132
- being the whole answer. Nothing else in the plugin reports health to a human today.
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.
133
140
  2. **`compositionInventory()` is used by the dev witness only.** Making the boot check itself compare
134
141
  mounted rows would catch a row that resolves but never activates — the `broken` flag does not always
135
142
  cover that, and the witness proves the shape of the answer already.
package/docs/contract.md CHANGED
@@ -369,35 +369,29 @@ running", the preset is simply absent — and a session's preset id is **durable
369
369
  `agent-preset/not-found`, `dsh-agent-preset-registry/lib/index.js:596`), so every chapters session refuses
370
370
  to OPEN while its log sits there intact.
371
371
 
372
- **This bundle does NOT ship the row as a patch layer.** It registers the row itself at activation,
373
- through the host's own `ctx.get('agentPresets').register(definition)` — the same call the host's
374
- declaration-row plugin makes (`dsh-agent-preset/lib/index.js`, `[Service.init]`). The reason is two
375
- boot-breaking failures measured on the pinned legacy closure (`@deepseek-ai/dsh 0.1.5-rc.1`, the host
376
- `dev/dsh-host-lock` freezes for tape bytes) against the installed 0.2.0-rc.2:
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:
377
379
 
378
380
  - `dsh.bundle.patch` as an **array** (what a second layer requires) dies at profile load on a pre-0.1.7
379
381
  host: `TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received an
380
- instance of Array` — legacy `dsh-app-boot/lib/index.js:853` does `join(packageDir, declared)`. Array
381
- support (`bundlePatchFiles`) and declaration rows arrive in the SAME release, 0.1.7-alpha.1, verified
382
- across the published `dsh-app-boot` tarballs. A two-layer manifest is therefore exactly as incompatible
383
- with 0.1.5/0.1.6 as it looks, and the host does not warn — it just never comes up.
384
- - The **row itself** in any layer a legacy host reads is fatal the same way:
385
- `dsh: plugin tree failed to load: … failed to import loader entry preset-chapters
386
- (@deepseek-ai/dsh-agent-preset): Cannot find package` (measured; `@deepseek-ai/dsh-agent-preset` is
387
- first published at 0.1.7-alpha.1, and 0.1.5-rc.1 ships `dsh-agent-presets`, plural, whose service has
388
- no `register()` — `this.ctx.agentPresets.register is not a function`).
389
- - **`disabled: !!js` cannot gate the row either**, which is the tempting fix. The Loader evaluates a row's
390
- `disabled` before services are mounted (`cordis-plugin-loader/lib/index.js:348` →
391
- `evaluate(this.ctx, expr)` = `with (ctx) { eval(expr) }`), so `get('agentPresets')` is undefined *on the
392
- modern host too* and the row is disabled everywhere — measured on 0.2.0-rc.2: `preset-v2 … failed to
393
- import`, roster without it, while the same row with `disabled: !!js false` registered normally. Bare
394
- `get(...)` also throws in that scope (`ctx` is bound, `get` is not).
395
-
396
- The registration path is verified on both mechanisms with real boots: on 0.2.0-rc.2 the witness reports
397
- `register: pruned, roster: present, inventoryRows: 28, unusableRows: []`; on 0.1.5-rc.1 it reports
398
- and the same build on 0.1.5-rc.1 logs
399
- `delivery: REFUSED — this host predates preset declaration rows (< 0.2.0)` — the tools work,
400
- the preset cannot exist there, and 0.2.0 is now the support floor.
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.
401
395
 
402
396
  Three consequences of reading the registry rather than the filesystem:
403
397
 
@@ -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
- | preset delivery | **rewritten in 0.1.17**: the row is registered at activation (`src/preset.ts`). The `presets/chapters/` + copy-on-boot mechanism it replaced is deleted with the 0.2.0 support floor (docs/contract.md) |
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 |
@@ -492,3 +492,39 @@ Two traps measured while finding that:
492
492
  `src/` only).
493
493
 
494
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.
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 | **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 health evidence is the ROSTER check and the mounted rows — pinned by P24/P25 below | unit `preset-declaration.test.ts` (14 rows) + real boots on 0.2.0-rc.2 and 0.2.1-alpha.4 |
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 |
@@ -0,0 +1,76 @@
1
+ export interface SubprocessLike {
2
+ resolveExecutable(name: string): Promise<string>;
3
+ spawn(opts: {
4
+ argv: string[];
5
+ cwd: string;
6
+ stdio: {
7
+ stdin: string;
8
+ stdout: {
9
+ maxBytes: number;
10
+ };
11
+ stderr: {
12
+ maxBytes: number;
13
+ };
14
+ };
15
+ graceMs: number;
16
+ signal?: AbortSignal;
17
+ }): {
18
+ done: Promise<{
19
+ exitCode: number;
20
+ }>;
21
+ collected: {
22
+ stdout?: {
23
+ readFrom(n: number): {
24
+ text: string;
25
+ };
26
+ };
27
+ stderr?: {
28
+ readFrom(n: number): {
29
+ text: string;
30
+ };
31
+ };
32
+ };
33
+ };
34
+ }
35
+ /**
36
+ * Resolve the codegraph CLI executable. Returns null if unavailable.
37
+ * Cached after first attempt — call freely.
38
+ */
39
+ export declare function resolveCodegraph(subprocess: SubprocessLike | undefined): Promise<string | null>;
40
+ /** Reset the cached resolution (test seam only). */
41
+ export declare function resetCodegraphResolution(): void;
42
+ /** Check whether a project has a CodeGraph index at or above the given path. */
43
+ export declare function hasCodegraphIndex(cwd: string): boolean;
44
+ /** Find the nearest indexed ancestor directory. */
45
+ export declare function findCodegraphRoot(cwd: string): string | undefined;
46
+ /** Extract code-shaped tokens from text (file names, camelCase, PascalCase, snake_case). */
47
+ export declare function extractCodeTokens(text: string, limit?: number): string[];
48
+ interface RunResult {
49
+ text: string;
50
+ ok: boolean;
51
+ }
52
+ /**
53
+ * Run a codegraph CLI command. Returns empty text on any failure — callers
54
+ * treat failure as "no data", never as an error.
55
+ */
56
+ export declare function runCodegraphQuiet(subprocess: SubprocessLike | undefined, argv: string[], cwd: string, timeoutMs: number): Promise<RunResult>;
57
+ /**
58
+ * Query CodeGraph for symbols matching a token. Returns symbol names that
59
+ * exist in the index. Fire-and-forget friendly — errors return [].
60
+ */
61
+ export declare function querySymbols(subprocess: SubprocessLike | undefined, root: string, token: string, limit?: number, timeoutMs?: number): Promise<string[]>;
62
+ /**
63
+ * Extract CodeGraph symbols from a set of events by verifying code-shaped
64
+ * tokens against the project index. Budget: max 3 queries, 5s each.
65
+ * Returns [] on any failure — callers treat this as optional enrichment.
66
+ */
67
+ export declare function extractCodegraphSymbolsFromEvents(subprocess: SubprocessLike | undefined, cwd: string, events: readonly {
68
+ type?: string;
69
+ data?: Record<string, unknown>;
70
+ }[], limit?: number): Promise<string[]>;
71
+ /**
72
+ * Run codegraph explore and return the raw text output. Capped and
73
+ * fire-and-forget — errors return ''.
74
+ */
75
+ export declare function exploreCodegraph(subprocess: SubprocessLike | undefined, root: string, query: string, maxChars?: number, timeoutMs?: number): Promise<string>;
76
+ export {};