@treeseed/dsh-chapters 0.1.16 → 0.1.17

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,40 @@
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
15
- # plane. Design record: docs/knowledge-repo.md §12.
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.
17
+ #
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.
16
30
  #
17
31
  # config:
18
32
  # # ---- 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)
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.
22
42
  # fallbackPreset: 'chapters' # preset mounted for children when the
23
- # # caller's own preset cannot be resolved
43
+ # # caller's own preset cannot be resolved — and
44
+ # # the id the roster check looks for. This id is
45
+ # # DURABLE STATE: every child's log records it,
46
+ # # so renaming it orphans those sessions.
24
47
  # harnessId: '<hostname>' # per-machine identity: git commit author,
25
48
  # # token scoping, edits/<harness>/ curation
26
49
  # # partition, rule-approval authorship
package/docs/backlog.md CHANGED
@@ -95,3 +95,75 @@ 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. The per-boot row pins (`thresholdRatio`, `enrichmentEnabled`,
125
+ `auto`, `elicitedPlot`, `toolResultArtifactTokens`) and the `[e2e] boot pins:` line were deleted with
126
+ the YAML they patched; they come back expressed as CONFIG on the registered row (`compactionThresholdRatio`
127
+ exists now; the rest need the same treatment). Until then the browser acceptance chain does not run, and
128
+ `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.
133
+ 2. **`compositionInventory()` is used by the dev witness only.** Making the boot check itself compare
134
+ mounted rows would catch a row that resolves but never activates — the `broken` flag does not always
135
+ cover that, and the witness proves the shape of the answer already.
136
+ 3. **A reset registry can overwrite a chapter — no guard exists today.** `writeArchive`
137
+ (`src/archive.ts:106`) reserves numbers from the registry and writes them; nothing checks whether that
138
+ path already exists on disk. So the exact incident shape — registry gone (host reinstall) while chapter
139
+ files remain, or restored by rehydration — lets a NEW archive reuse number 001 and clobber a surviving
140
+ file, which breaks the append-only rule from the inside. Two rows are missing: at boot, report files the
141
+ registry does not know (`<store>/<root>/chapters/*.md` vs registry rows, counted); and refuse a
142
+ reservation whose target path exists, with the numbers.
143
+ 4. **Rehydration restores bytes, not rows.** `rehydrateStoreFromMirror` (`src/sync.ts:476`) fills absent
144
+ chapter and artifact files only. Search works off the files' own frontmatter, but numbering, ancestry and
145
+ collections are registry state, so a rehydrated store can be searched and cannot be continued cleanly.
146
+ Decide: rebuild rows from frontmatter (honest — the bytes are the ground truth), or refuse continuation
147
+ over a partially restored store with numbers. Do not let it look like a fresh chain.
148
+
149
+ ## Host bug we cannot fix: the workspace directory-picker modal (2026-10-16)
150
+
151
+ Reported by the user as "the directory selection modal crashes before I can select a workspace", and it
152
+ survived a host upgrade. Root-caused with real boots (`var/picker-probe/FINDINGS.md`, kept out of the
153
+ repo's claims — it is host code): the picker's CLIENT bundle and the shell's client module table must come
154
+ from the same host generation. The 0.1.x picker asks for `IconFolderOpen16`/`IconFolderClose16`;
155
+ 0.2.0-rc.2's shell exports only the `…Regular` spellings, with no compat alias in either direction. The
156
+ slot boundary catches it, so the symptom is a rendered-then-dead dialog:
157
+ `Error: Minified React error #130 … got: undefined`, logged as
158
+ `slot entry crashed in 'sidebar.workspaces.directoryFlow'`
159
+ (`@deepseek-ai/dsh-client-ui-renderer/lib/client.js:618`). A stale profile-local copy of
160
+ `@deepseek-ai/dsh-client-ui-directory-picker-*` under `$DSH_HOME/profiles/<p>/node_modules` shadows the
161
+ upgraded global install and reproduces "the new host did not fix it" exactly.
162
+
163
+ Nothing in this plugin contributes to that slot (its client half registers
164
+ `conversation.chat.assistant-actions` only), so the repo can ship no fix — only the recipe, which the
165
+ site documents: purge the stale picker copies, and if the auto-resolved backend is not the one you want,
166
+ pin the pair deliberately in the profile patch (`disabled: true` on `directory-picker`, insert
167
+ `@deepseek-ai/dsh-host-directory-picker-browse` + `@deepseek-ai/dsh-client-ui-directory-picker-browse`).
168
+ Upstream ask: compat aliases for the renamed icon exports, or a boot-time shell↔plugin version handshake
169
+ 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,98 @@ 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 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:
377
+
378
+ - `dsh.bundle.patch` as an **array** (what a second layer requires) dies at profile load on a pre-0.1.7
379
+ 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.
401
+
402
+ Three consequences of reading the registry rather than the filesystem:
403
+
404
+ - **A row that does not resolve is dropped, never mounted.** `mountPreset` throws if ANY enabled row failed
405
+ (`dsh-agent-preset-registry/lib/index.js:272`), the registry stores it as `record.broken` (`:541`), and
406
+ `retain()` then refuses with `agent-preset/invalid` (`:651`) — so one stale row makes the whole preset
407
+ unusable, which is the same user-visible failure as a missing preset. Measured on 0.2.0-rc.2 with a row
408
+ for `@deepseek-ai/dsh-tool-schedule` (published only from 0.2.1-alpha.1):
409
+ `preset "chapters" is DECLARED BUT BROKEN — tool-schedule …: never started`. `pruneResolvableRows`
410
+ (`src/preset.ts`) drops such rows and NAMES them in the mount line; `resolvableFromThisPackage` is the
411
+ probe, asked with the row's full specifier so a lost subpath export is caught too.
412
+ - **A held disposer, not an effect.** `register()` returns an unregister disposer, and wiring it through
413
+ `ctx.effect` un-registers the preset the moment our activation finishes — measured: `register()`
414
+ succeeded while the roster came back WITHOUT `chapters`. The same apply-fiber trap bit the storage
415
+ domain in r35. The disposer is held in a module array instead.
416
+ - **The roster is the health check, and it must be asked after activation.** The registry re-audits a mount
417
+ by awaiting the settled loader tree (`:562`), and its doc says callers "must not run inside a Host row's
418
+ own activation, which the settlement would wait on". So `reportPresetRoster()` runs deferred, and
419
+ `compositionInventory()` rows say which mounted rows are actually `active` — `broken` alone hides a row
420
+ that resolved but never started.
421
+
422
+ Rows are renamed and dropped between host versions, so re-anchor `src/preset.ts` against the installed
423
+ host's own `@deepseek-ai/dsh-web-app/presets/standard.patch.yml` after an upgrade. Measured drift across
424
+ the five published versions (rc.1, rc.2, alpha.1, alpha.2, alpha.4): alpha.1 ADDS `dsh-time-context` +
425
+ `dsh-tool-schedule` + `toolFilter.deny[schedule_*]`; alpha.2 DROPS the persona `suffix` (with its
426
+ `{{cwd}}`), `backgroundMode`, and the codex/claude-code/ralph rows; alpha.4 rewrites the plan-mode
427
+ sentence, drops `toolFilter`, adds `dsh-plugin-market`. rc.1 and rc.2 are byte-identical.
428
+ `tests/unit/preset-declaration.test.ts` is what turns the next rename into a red row instead of a broken
429
+ user session — and it pins that `dsh.bundle.patch` is still a string.
430
+
431
+ - **Prompt variables are a closed set, and violating them throws per turn, not at boot.** `dsh-agent-loop`
432
+ registers `provider`/`model`/`cwd` through 0.2.0-rc.2 (`lib/index.js:1564-1566`) and only `provider` and
433
+ `model` from 0.2.1-alpha.2 (`:1591-1592`); `interpolate()` rejects any other reference with `prompt
434
+ variable "{{x}}" has no value for this assembly` (`dsh-system-prompt/lib/index.js:171`). There is no
435
+ `{{cwd}}` worth writing: `dsh-working-directory` (host plane) already prints `Current working directory:
436
+ "…"` into the runtime context. This is what broke every new chapters session immediately AFTER the
437
+ delivery fix above — the preset composed fine, the legacy `persona.suffix` interpolated `{{cwd}}`, and
438
+ assembly threw on every turn. Pinned by the `{{variable}}` row of the test.
439
+ - **Some host plugins reject unknown config keys**, so a preset row's config is not free-form:
440
+ `dsh-compaction-tool-result-pruner`, `dsh-compaction-basic` and `dsh-plan-mode` throw on any key they do
441
+ not know (`PlanModeConfig has unknown key(s) …`); `dsh-tool-subagent` and `dsh-tool-ralph` are plain zod
442
+ objects that strip them. Keep exactly `{thresholdChars, headChars, tailChars}` on the pruner and exactly
443
+ `{section}` on plan mode.
444
+ - **Persona wording is load-bearing for the engine.** `src/engine-core.ts`'s `PLOT_TEMPLATE_ECHO` suppresses
445
+ a "plot" that is really our own instruction quoted back (the 2026-09-25 audit: eight checkpoints of a
446
+ real run inherited boilerplate, and because the result was non-null the bounded elicited fallback never
447
+ ran). It matches phrases inside the suffix, so reword the suffix and the guard in the same change — the
448
+ gate asserts that coupling by behaviour, not by copying the regex.
449
+
357
450
  ## Trigger Constraints
358
451
 
359
452
  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 | **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) |
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,30 @@ 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.
@@ -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,7 @@ 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 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 |
156
156
  | 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
157
  | 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
158
  | 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 |
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import Schema from '@deepseek-ai/schemastery';
2
+ import { type RegisterOutcome } from './preset.ts';
2
3
  import { type SyncScheduler } from './sync.ts';
3
4
  /** The cordis surface this entry touches; widened in later stages. */
4
5
  interface HostCtx {
@@ -26,6 +27,7 @@ interface HostCtx {
26
27
  }
27
28
  export interface Config {
28
29
  installChaptersPreset: boolean;
30
+ compactionThresholdRatio: number;
29
31
  artifactStoreRoot: string;
30
32
  chapterTokenTarget: number;
31
33
  toolResultDeferFloorTokens: number;
@@ -91,39 +93,59 @@ export declare function apply(ctx: HostCtx, config: Config): Promise<void>;
91
93
  * before anything archives.
92
94
  */
93
95
  export declare function createScheduler(store: import('./store.ts').RegistryStore, domain: import('./store.ts').DomainLike, config: Config, debounceOverride?: number, onSyncDone?: (cwd: string, ok: boolean) => void, warn?: (m: string) => void): SyncScheduler;
94
- export type PresetInstallStatus = 'installed' | 'repaired' | 'complete' | 'failed' | 'no-source';
95
- /** What the preset install actually did — the caller must not report success on a 'failed'. */
96
- export interface PresetInstallReport {
97
- status: PresetInstallStatus;
98
- /** Shipped files absent from the installed copy (the partial-preset case). */
99
- missing: string[];
100
- dstDir: string;
101
- error?: string;
102
- }
103
96
  /**
104
- * Install the `chapters` preset into the user preset directory, and — the part that
105
- * used to be missing — tell the truth about what happened.
97
+ * Why delivery is a `register()` call and not a file, in one incident: hosts from
98
+ * 0.1.7-alpha.1 stopped reading `$DSH_HOME/.agent-presets/` — a preset is an
99
+ * `@deepseek-ai/dsh-agent-preset` declaration row — while the plugin kept copying,
100
+ * kept logging that it had mounted, and the preset was simply GONE. That is worse
101
+ * than a crash, because `resume` resolves the preset id recorded in the session's
102
+ * own log and throws `agent-preset/not-found`, so every chapters session became
103
+ * unopenable while its durable log sat there intact and uncorrupted.
106
104
  *
107
- * The never-clobber rule exists because the installed copy is user-owned: the user may
108
- * have edited `agent.cordis.yml` to retune the compaction row. But the OLD guard tested
109
- * for `preset.yml` ALONE, and the preset is really two files — `agent.cordis.yml` is the
110
- * one that names the engine realm row. So a partially wiped home (an interrupted
111
- * uninstall/reinstall is what triggered this) left a preset directory that passed the
112
- * guard forever: the installer returned early on every boot, the preset could not mount,
113
- * and the plugin still logged that it had mounted. Sessions that had chosen the preset
114
- * then looked corrupted, while their logs and data were untouched — the break was in
115
- * composition, which is invisible if the report lies.
105
+ * Shipping the row as a patch layer instead is what a reader will reach for, and it
106
+ * is a boot-breaking choice on the hosts this plugin used to support: `dsh.bundle.patch`
107
+ * as an array throws `ERR_INVALID_ARG_TYPE` at legacy `dsh-app-boot/lib/index.js:853`,
108
+ * an unresolvable row aborts a legacy plugin tree, and `disabled: !!js` cannot gate the
109
+ * row because the Loader evaluates `disabled` before any service is mounted (all three
110
+ * measured). Support floor is 0.2.0 now, so the row is registered here through the
111
+ * host's own `agentPresets.register()` — the same call its declaration-row plugin makes.
112
+ */
113
+ /**
114
+ * Whether a row's module can be imported from THIS package's resolution family.
116
115
  *
117
- * The repair is therefore *additive*: missing shipped files are restored, files the user
118
- * already has are never overwritten. That keeps the user's edits intact AND makes a
119
- * half-deleted preset self-healing, because restoring a missing file is not clobbering
120
- * anything.
116
+ * The preset mount resolves its rows from the registry's context, which shares this Node
117
+ * resolution root; a false answer here means a row that would activate as failed — and one
118
+ * failed row makes the whole preset `broken`, which is worse than a missing tool.
121
119
  *
122
- * Exported for testing with real temp directories; `srcDir`/`dstDir` are parameters so no
123
- * test has to touch the developer's DSH home.
120
+ * Never throws: an unresolvable specifier is the answer. Two limits of the probe are the
121
+ * host's own, not ours to paper over: a RELATIVE specifier resolves without touching the
122
+ * filesystem (Node answers the URL, not existence), so a file-URL row would always be
123
+ * reported resolvable; and a name that resolves is not yet a name that ACTIVATES — a plugin
124
+ * that rejects its config throws later, which is what the roster line reports as
125
+ * `DECLARED BUT BROKEN`. Subpath exports are the useful case, and they are the ones a host
126
+ * rename eats first: `@scope/pkg/no-longer-exported` answers `ERR_PACKAGE_PATH_NOT_EXPORTED`.
124
127
  */
125
- export declare function installChaptersPreset(srcDir: string, dstDir: string, log?: {
126
- info?: (m: string) => void;
127
- warn?: (m: string) => void;
128
- }, userPresetRoot?: string): PresetInstallReport;
128
+ export declare function resolvableFromThisPackage(specifier: string): boolean;
129
+ /** What the host roster says about our preset. */
130
+ export type RosterVerdict = {
131
+ state: 'present';
132
+ broken?: string;
133
+ } | {
134
+ state: 'absent';
135
+ rows: string[];
136
+ } | {
137
+ state: 'unknown';
138
+ error: string;
139
+ };
140
+ /** Ask the host which presets exist. Tolerates both shapes the service has
141
+ * offered: the bare row array (`list()`) and the remote envelope (`{presets}`). */
142
+ export declare function readRosterVerdict(presets: {
143
+ list?: () => Promise<unknown>;
144
+ } | undefined, presetId: string): Promise<RosterVerdict>;
145
+ /** The remedy, in the two shapes an absent preset can have on a supported host. */
146
+ export declare function rosterRemediation(presetId: string, outcome: RegisterOutcome): string;
147
+ /** Deferred boot check: one truthful line about the preset, plus the verdict for the witness.
148
+ * Exported so the four arms of that line are tested without booting a host — the line IS the
149
+ * health signal a user reads, and an untested health signal is how the silent case survived. */
150
+ export declare function reportPresetRoster(ctx: HostCtx, presetId: string, outcome: RegisterOutcome): Promise<RosterVerdict>;
129
151
  export {};