deepseek-coordination 0.1.1 → 0.1.5

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/README.md CHANGED
@@ -68,15 +68,24 @@ deepseek-coordination/
68
68
  ├─ src/host/docs/os-spec.md full specification: contracts, error codes, golden matrix
69
69
  ├─ src/host/docs/permission-model.md the authorization model and its residual risk
70
70
  ├─ src/plugin/index.js host plugin: registers the `dsh_os` tool
71
+ ├─ src/plugin/invoke.js shared invocation layer: artefact/edition resolution, spawn, envelope parsing
72
+ ├─ src/plugin/settings.js settings service + the panel's two HTTP routes (`/api/dsh-os/*`)
73
+ ├─ src/plugin/takeover.js `tools/execute` interception: answers the built-in read/write/edit/pwsh through dsh-os
74
+ ├─ src/plugin/workflow.js intent-first workflow: the `workflow_plan` tool, its phase gates and its prompt section
75
+ ├─ client.js browser half: the `dsh.client` artifact that renders the settings panel
71
76
  ├─ src/permission/index.js consent delegate: the only writer of permission.json, registers five `os_permission_*` tools
72
77
  ├─ tools/build.ps1 assembles the three generated scripts; `-Check` fails on drift
73
78
  ├─ tools/parse-check.ps1 parse-checks each module under the runtime that will execute it, and reports non-ASCII bytes and a BOM
74
79
  ├─ tools/json-field.mjs reads one field of an envelope by dotted path (used by the shell suites; `sed` cannot see into a nested object)
75
- ├─ tools/verify-bundle.mjs verifies the loader's resolution contract (10 cases)
76
- ├─ tools/verify-plugin-load.mjs runs the real `apply()` path (13 cases)
80
+ ├─ tools/verify-bundle.mjs verifies the loader's resolution contract (16 cases)
81
+ ├─ tools/verify-plugin-load.mjs runs the real `apply()` path, including the workflow state machine (30 cases)
82
+ ├─ tools/verify-package.mjs packs the package and asserts every runtime-resolved file ships (21 cases)
77
83
  ├─ tools/verify-editions.mjs proves both PowerShell artefacts are reachable (14 cases)
78
- ├─ tests/ golden (Windows/POSIX/desktop injection), table conformance, encoding, plugin, permission suites
84
+ ├─ tools/probe-pipeline.mjs drives the interception end to end against a stubbed verb layer (23 cases)
85
+ ├─ tests/ golden (Windows/POSIX/desktop injection), table conformance, encoding, plugin, permission, takeover, workflow suites
79
86
  │ ├─ conformance.ps1 command.txt and spec/verbs.tsv must describe the same surface (5 cases)
87
+ │ ├─ takeover.test.mjs byte-level format preservation, read windowing, eligibility (41 cases)
88
+ │ ├─ workflow.test.mjs phase gates, the user's choice, and the KV-cache invariant (30 cases)
80
89
  │ ├─ smoke-desktop-sh.sh what the POSIX desktop verbs actually inject (52 cases)
81
90
  │ └─ fake-xdotool, fake-wmctrl recording stand-ins, so the desktop verbs can be verified with no X server
82
91
  └─ permission.json authorization data (an empty file in the repository; that is the initial state)
@@ -115,8 +124,13 @@ precisely the class of encoding fault this project exists to remove.
115
124
 
116
125
  ## Getting started
117
126
 
118
- Not yet published to npm, so use it from source:
127
+ Published on npm, so a DSH profile can install it as a bundle:
119
128
 
129
+ ```console
130
+ dsh plugin install deepseek-coordination
131
+ ```
132
+
133
+ Or work from source:
120
134
  ```console
121
135
  git clone https://github.com/eloyhere/deepseek-coordination.git
122
136
  cd deepseek-coordination
@@ -145,11 +159,25 @@ because batch-file argument handling is itself a source of quoting bugs.
145
159
  ### Using it from the harness
146
160
 
147
161
  `package.json` declares `dsh.bundle.patch`, so the package installs into a DSH profile as a bundle and
148
- registers one model-facing tool:
162
+ inserts five rows:
163
+
164
+ | Row | Entry | What it does |
165
+ |---|---|---|
166
+ | `dsh-os` | `.` | registers the `dsh_os` tool |
167
+ | `dsh-os-settings` | `./settings` | the settings the panel edits, plus its two HTTP routes |
168
+ | `dsh-os-takeover` | `./takeover` | answers the built-in `read`/`write`/`edit`/`pwsh` through dsh-os while enabled |
169
+ | `dsh-os-permission` | `./permission` | the consent delegate, the only writer of `permission.json` |
170
+ | `dsh-os-workflow` | `./workflow` | the intent-first workflow tool and its system-prompt section |
171
+
172
+ The rows must name **different module specifiers**: `name` is resolved as a package specifier, and giving
173
+ two rows the bare package name loads the same module twice, which throws `tool "dsh_os" is already
174
+ registered` and aborts the whole entry. That is why the subpath exports exist.
149
175
 
150
176
  | Tool | Purpose |
151
177
  |---|---|
152
178
  | `dsh_os` | Run one verb of the surface and return its JSON envelope verbatim |
179
+ | `os_permission_*` | Read, grant, revoke and audit durable authorization decisions |
180
+ | `workflow_plan` | Run one phase of the intent-first workflow (see [The workflow](#the-workflow)) |
153
181
 
154
182
  One tool rather than one per verb, for three reasons: the verb table is the contract and lives in
155
183
  `src/host/spec/verbs.tsv`, so no schema needs regenerating when a verb is added; an agent discovers the
@@ -165,6 +193,48 @@ shape, so there is no second contract to drift from the script's.
165
193
  The implementation is spawned by absolute path with an argv array, never through a shell string, so a
166
194
  value containing spaces stays one value.
167
195
 
196
+ ### Taking over the built-in operations
197
+
198
+ `dsh_os` is a tool an agent has to be *told* to use, and it never is: the built-in `read`/`write`/`edit`/
199
+ `pwsh` tools are right there, they overlap almost completely, and nothing in the prompt prefers one over
200
+ the other. The guarantees this project exists for therefore stay unexercised — the plugin is installed
201
+ and imperceptible.
202
+
203
+ **Takeover** removes the choice. Enable it in **Settings → DSH OS** and the built-in calls are answered
204
+ by the same machinery, whether or not the agent cooperates. Per-tool switches let you redirect only
205
+ `write`, or only `edit`, and leave the rest alone.
206
+
207
+ It works through the `tools/execute` around-dispatch waterfall: a handler returns `{ value }` without
208
+ calling `next()`, and that value is then validated against the **built-in** tool's own `output.schema`
209
+ and rendered by the **built-in** `output.render`. The model-facing text, the diff cards and the
210
+ presentation metadata are therefore unchanged; only the byte-level source of the value is.
211
+
212
+ **What it does not take over, and why:**
213
+
214
+ * `glob` and `grep`. This surface has no content-search verb at all — `file search` matches *names* — so
215
+ `grep`'s `lineNumber`/`line` cannot be produced, and glob's ignore rules and mtime ordering have no
216
+ equivalent. Redirecting them would silently return a different result set.
217
+ * Background commands and sandbox escalations. A background call's result is a harness job id, which
218
+ this surface cannot mint, and `sandbox_permissions` is the harness's own approval seam.
219
+ * Relative paths. The built-in tools resolve them against the session workspace through `ctx.fs`; this
220
+ plugin has no equivalent resolution, and guessing the process working directory would read a different
221
+ file than the one named.
222
+
223
+ **Two properties are deliberately given up, and you should know them before enabling it:**
224
+
225
+ 1. **The filesystem sandbox does not bound a redirected call.** The built-in write path goes through
226
+ `ctx.fs`, which is where the sandbox policy and the read-before-write observation guard live. A
227
+ redirected write runs the verb directly, so dsh-os's own protected-path check is what remains. This
228
+ is a real reduction in defence, not a detail.
229
+ 2. **The reported text and the bytes on disk can differ.** With the default *follow the target* format, a
230
+ CRLF file stays CRLF and a BOM stays a BOM, while `value.after` is normalised LF text — which is what
231
+ the built-in `read` would show and what the diff card compares. Choose *same as built-in* if you would
232
+ rather the takeover change nothing about the bytes.
233
+
234
+ Every redirected call falls back to the built-in implementation on any failure, so takeover can change
235
+ how a result is produced but never whether the caller gets one. The panel's activity feed shows what was
236
+ actually taken over and why anything fell back.
237
+
168
238
  **Choosing the PowerShell edition.** The default remains the 5.1 artefact (`bin/dsh-os.ps1`),
169
239
  **deliberately unchanged**: a deployment that already works should not change behaviour underneath
170
240
  itself because a plugin was upgraded. To select the 7.6 artefact (`bin/dsh-os7.ps1`):
@@ -179,13 +249,177 @@ install locations, because `pwsh` is not always on `PATH` — it is not on this
179
249
  environment, while 7.6.6 is plainly installed in the standard location.
180
250
 
181
251
  > [!IMPORTANT]
182
- > **The bundle now loads in a live desktop session.** The plugin has since been installed into the
183
- > active profile, and the six tools it registers — `dsh_os` plus the five `os_permission_*` tools —
252
+ > **The bundle now loads in a live desktop session.** The plugin is installed into the active profile as
253
+ > a published npm package, and the tools it registers — `dsh_os` plus the five `os_permission_*` tools —
184
254
  > are present and callable in a running session. What is still **not** proven is the part that
185
255
  > `tools/verify-bundle.mjs` cannot cover: that the *harness's own loader* composes the bundle by the
186
256
  > same contract the verifier checks, rather than by a shortcut that happens to work here. The
187
257
  > verifier remains the evidence; the live session is corroboration.
188
258
 
259
+ ### The workflow
260
+
261
+ Every other row in this package makes an OS call **correct**. This one makes a *request* decided before it
262
+ is executed. The failure mode it removes is a request that was never a specification: the model starts
263
+ editing files, and the divergence is discovered at review — after the tokens have been spent.
264
+
265
+ `dsh-os-workflow` registers one tool, `workflow_plan`, and one system-prompt section.
266
+
267
+ ```
268
+ intake -> plan -> choose -> run -> done
269
+ ```
270
+
271
+ | Phase | Action | What it establishes |
272
+ |---|---|---|
273
+ | intake | `start` | the prompt **verbatim**, plus 1–3 questions about intent *or* the assumptions being made |
274
+ | plan | `plan` | the approach and process, **at least two** alternatives with pros and cons, the criteria, and a recommendation |
275
+ | choose | `choose` | **the user's decision**, taken through `ctx.userQuestions` inside the call |
276
+ | run | `run` | execution, its steps, and — in the same call — the outcome that closes the workflow |
277
+ | — | `note` | a remark or a decision made during execution |
278
+ | — | `checkpoint` | folds the record into one file and reports a summary |
279
+ | — | `status`, `reset` | read the state; discard it and start again |
280
+
281
+ > [!IMPORTANT]
282
+ > **The tool is named `workflow_plan`, not `workflow`, and the reason is a real collision.** A host
283
+ > composition already exposes a built-in `workflow` tool — the JavaScript multi-agent orchestrator from
284
+ > `@deepseek-ai/dsh-tool-workflow`. Two tools sharing one name in one scope collide: the second
285
+ > registration aborts, or both resolve and the model is offered two unrelated tools under one name. Since
286
+ > this row's tool lives in the host plane while `workflow` is mounted per agent preset, no test in this
287
+ > repository would have caught it; it was caught by reading the live tool catalog. `_plan` also says what
288
+ > this tool is: the planning half of a workflow.
289
+
290
+ **The gates are real.** A phase whose entry has not been recorded is refused, and the refusal names the
291
+ phase that must run first. `choose` cannot be answered by the model: the question goes to the user through
292
+ the same seam `ask_user_question` uses, and nothing else can move the workflow into `run`.
293
+
294
+ **Four calls, one question.** The unavoidable path is `start` → `plan` → `choose` → `run`. It was
295
+ originally eight calls and two questions, and the difference is three deliberate fusions — each of which
296
+ removes a round-trip without removing a decision:
297
+
298
+ * **design and options are one call**, because they are one decision. This is the QOC shape (Questions,
299
+ Options, Criteria) and the ADR shape (Context, Decision, Consequences): a decision record holds the
300
+ alternatives it was chosen between and the criteria it was judged against. Splitting them made the
301
+ second call re-derive the first and made the user read the same architecture twice.
302
+ * **a clear request costs zero questions.** `start` accepts `assumptions` instead of `questions`, so an
303
+ unambiguous prompt is not interrupted — and the assumption is on record where the user can correct it,
304
+ which is cheaper than the question it replaced. The evidence for *minimal, relevant* questions over
305
+ interrogation is the Ask-to-Act line of work ([arXiv:2504.00907](https://arxiv.org/abs/2504.00907)).
306
+ * **a single-step execution closes in the same call it opens.** Passing `outcome` to `run` finishes the
307
+ workflow; only multi-step work needs a second call.
308
+
309
+ **The compression is the other half.** A plan narrated into the conversation is re-sent with every later
310
+ step, which is what makes a short task expensive. So it never enters the conversation: each phase writes a
311
+ real document and returns a line — phase, summary, file path, one next call. `tests/workflow.test.mjs`
312
+ measures it and holds every phase envelope under 2000 characters; in the current build the four calls of a
313
+ full workflow total about 1.8 kB, roughly 500 tokens, for a plan document measured in kilobytes. This shape
314
+ is measured, not stylistic: pruning plus summarisation improves completion while cutting tokens and runtime
315
+ ([Less Context, Better Agents](https://export.arxiv.org/pdf/2608.16370)), and a planner/executor split
316
+ matches interleaved reasoning at a fraction of the token cost
317
+ ([Plan Once, Ground Locally](https://zenodo.org/records/22904595)).
318
+
319
+ **Where the cost goes**, stated as complexity because that is what it is:
320
+
321
+ | Path | Cost |
322
+ |---|---|
323
+ | reading state | once per workflow call, cached on the file's mtime — and **never** on a model step, because the prompt section reads no state at all |
324
+ | a phase transition | one atomic state write; the first version wrote it three times per action |
325
+ | a note during execution | one `appendFileSync` append, O(1), bounded by compaction — not the read-push-rewrite that is O(n²) over a workflow |
326
+ | the prompt section | a **constant**: no read, no lookup, no formatting, identical at every phase of every workflow |
327
+
328
+ ### The KV cache effect, which is why the prompt section is a constant
329
+
330
+ The section this row registers into the system prompt is a **constant string**. That is not tidiness; it is
331
+ the design, and the reason is that the prompt is the worst possible place for anything that moves.
332
+
333
+ **What this package measured.** `tools/measure-cache.mjs` drives the real endpoint
334
+ (`deepseek-flash`, `max_tokens: 1`, one request per variant after one warm request on the same lineage) and
335
+ reports `cache_read_input_tokens` for the shapes this plugin actually produces. Measured twice, with
336
+ identical results:
337
+
338
+ | Shape | Cache read | Total input | Hit |
339
+ |---|---:|---:|---:|
340
+ | an unchanged request (control) | 5,632 | 5,780 | 97.4% |
341
+ | appending a user message | 5,632 | 5,784 | 97.4% |
342
+ | appending a tool result | 5,632 | 5,853 | 96.2% |
343
+ | **one workflow: four appended calls** | 5,632 → 5,888 | ~5,900 each | **96.6% mean** |
344
+
345
+ So the row's own shape is measured warm: four calls, 23,040 cached tokens, every call above 96%.
346
+
347
+ **What this package did NOT manage to measure, and will not claim.** The experiment also tries the
348
+ counterfactual — rewrite the leading system prompt, edit a tool description, reorder two tools — and those
349
+ readings are **not reproducible**: the first run reported 0.0% for all of them, the second reported 97%. The
350
+ provider documents the reason and it is worth knowing: it persists a **common prefix detected across
351
+ requests**, so by the second run the repeated identical prefixes had been learned and a mutation still
352
+ matched. Per-variant A/B isolation on a shared account cannot settle the mutation side, so
353
+ `tools/measure-cache.mjs` asserts only the append side, which is stable, and prints the rest as
354
+ observations rather than claims.
355
+
356
+ **The counterfactual rests on independent evidence instead**, and it is strong:
357
+
358
+ * the provider's own rule is that a cache hit requires a request to **fully match a persisted cache prefix
359
+ unit**, and the system prompt sits before the tool catalog and the entire history
360
+ ([Context Caching](https://api-docs.deepseek.com/guides/kv_cache/));
361
+ * a ten-variant experiment on the same endpoint found appending a message kept a **98.7%** hit while adding
362
+ one line at the **end** of the system prompt produced **0.0%**
363
+ ([prefix-stability chapter](https://github.com/diguike/book-deepseek-harness/blob/main/book/11-prefix-stability.md));
364
+ * the harness itself paid for this lesson: the sandbox policy used to live in the system prompt, and one
365
+ permission switch dropped cache reads from **~14,700 tokens to 256**. The fix was to move the changing
366
+ fact out of the prompt into the message sequence
367
+ ([DSH agent note](https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md)).
368
+
369
+ **The first version of this row rendered the current phase into that section**, so a four-call workflow
370
+ re-wrote the system prompt three times. Whether or not the provider happens to forgive that on a given day,
371
+ the row has no business depending on it — so the fix is structural, not cosmetic:
372
+
373
+ * the section is a constant, so this row's **cache effect is `prefix-stable`**: zero prompt changes per
374
+ workflow, asserted by `tests/workflow.test.mjs` against the registered text at every phase, and again
375
+ after deleting `state.json` outright;
376
+ * the phase and the **one next call** travel in the `workflow_plan` tool's own result instead — appended
377
+ history, measured above at 96–97%;
378
+ * a step taken out of order is refused, and the refusal names the phase to record first, so the model can
379
+ recover from a stale plan without the prompt having told it anything;
380
+ * the tool **description** carries the discovery text. It lives in the tool catalog: inside the cached
381
+ prefix, sent once, and unchanged for the life of the session.
382
+
383
+ > [!IMPORTANT]
384
+ > **The consequence a reader should draw for their own plugins.** Anything that changes per step — the
385
+ > time, the current directory, a branch, a permission, a phase — must not be written into a prompt section.
386
+ > This harness gives that content its own path: register it with `ctx.systemPrompt.context()` and it is
387
+ > delivered as an appended user message, re-projected only when it changes. A section is for text that is
388
+ > stable for the session.
389
+
390
+ **Where the state lives.** `$DSH_HOME/dsh-os/workflows/<session>/`, beside the settings document and for
391
+ the same reason: the package directory is replaced on upgrade, and the workspace belongs to the user.
392
+ `state.json` is the compact record; every other file is the detail. A finished workflow is archived rather
393
+ than overwritten, so the next one starts clean and the last one stays readable.
394
+
395
+ ```
396
+ state.json the compact record: phase, intent, decisions, steps, artifact paths
397
+ journal.jsonl append-only: notes and steps, bounded, folded in by a checkpoint
398
+ 01-intent.md the prompt verbatim, then the questions and answers (or the assumptions)
399
+ 02-plan.md approach and process, alternatives with pros and cons, criteria, the decision
400
+ 03-execution.md chosen option, goal, steps, files in scope, constraints
401
+ 04-outcome.md what was delivered, and any step left open
402
+ 05-checkpoint.md the fold: everything decided so far, written when a compaction is wanted
403
+ ```
404
+
405
+ **What is configured.** `workflowRoot` moves the directory; `autoCompact` (default `true`) controls
406
+ whether a checkpoint reports the compaction the engine may perform, and `maxCheckpoints` bounds it. The
407
+ structural half — detail to disk, one line back — is **not** configurable, because it is the reason the
408
+ feature saves anything.
409
+
410
+ > [!WARNING]
411
+ > **This row asks the user questions, so the deployment must be able to answer them.** It injects
412
+ > `userQuestions`; a host without it registers nothing rather than registering a tool that would fail on
413
+ > its second call. Two calls block on a human: `start` when questions are given, and `choose` always.
414
+
415
+ > [!NOTE]
416
+ > **What this row does not do: it does not invoke the engine's compaction service.** `ctx.compaction`
417
+ > exists, and calling it from here would mean constructing a session-shaped context whose contract is not
418
+ > published — a guess dressed up as a feature. What the plugin does instead is deterministic and
419
+ > inspectable: the detail leaves the conversation for a file, and `checkpoint` records the boundary where a
420
+ > compaction is safe. That is why the compression is measurable by `tests/workflow.test.mjs` rather than
421
+ > merely asserted.
422
+
189
423
  ---
190
424
 
191
425
  ## JSON contract
@@ -546,7 +780,7 @@ been closed by three verifiers, and they found one real defect:
546
780
  | Verifier | What it proves | Cases |
547
781
  |---|---|---|
548
782
  | `tools/verify-bundle.mjs` | the loader's **resolution** contract: the manifest header, the patch file, every row resolving from a profile-style `node_modules`, every resolved module exporting the Cordis shape the loader calls (`apply`/`inject`/`name`), the two rows resolving to **different** modules (a shared specifier registers the same tool twice and aborts the whole entry), and the version gate | 10 |
549
- | `tools/verify-plugin-load.mjs` | the real `apply()` path: the host plugin registers `dsh_os` with a parameters schema whose **root type is `object`**, the permission plugin registers its five tools, and **the registered handler really does spawn the script** and return a real envelope (`selfcheck` → ok, `verbCount` 86); a mutating verb without `allow` is refused with a typed `E_PERMISSION` rather than throwing | 13 |
783
+ | `tools/verify-plugin-load.mjs` | the real `apply()` path: the host plugin registers `dsh_os` with a parameters schema whose **root type is `object`**, the permission plugin registers its five tools, and **the registered handler really does spawn the script** and return a real envelope (`selfcheck` → ok, `verbCount` 86); a mutating verb without `allow` is refused with a typed `E_PERMISSION` rather than throwing. It also covers the workflow row, whose failure would otherwise be silent: the tool and its prompt section **are** registered, and `start → plan → choose → run` runs through the real handler with its gates intact | 30 |
550
784
  | `tools/verify-editions.mjs` | edition selection: the default is still PowerShell 5.1; `powershellEdition: '7.6'` and `DSH_OS_POWERSHELL_EDITION` select the 7.6 artefact; `'auto'` uses 7.6 only when the artefact and a working `pwsh` are present **together**; configuration beats the environment; an explicit `scriptPath` disables the choice. `-ExecutionPolicy` is passed only to the 5.1 executable | 14 |
551
785
 
552
786
  The bundle verifier links the package into a throwaway profile sandbox by junction, so resolution
@@ -572,7 +806,7 @@ by a **later** case.
572
806
 
573
807
  That defect is **fixed**: the parser case in `tests/golden.ps1` now creates its own fixture
574
808
  (`$fixtureDir = Join-Path $sandbox 'parser\verify'`, then `New-Item` and `Set-Content`) and asserts
575
- against it. The current measurement is **49 passed, 0 failed**.
809
+ against it. The current measurement is **50 passed, 0 failed**.
576
810
 
577
811
  ### Two real POSIX defects found by `tests/smoke-desktop-sh.sh`
578
812
 
@@ -638,10 +872,19 @@ stand-in tools exposed both:
638
872
  no acceptance step depends on it. (The plugin's *installation* path does: the package manager
639
873
  resolves the repository with `git ls-remote`, so a machine where the harness cannot find `git` on
640
874
  `PATH` cannot install the bundle.)
875
+ * **PowerShell 7.6 is exercised, but not across the whole surface.** The 7.6 artefact definitely runs:
876
+ driving the plugin's real `resolveInvocation` path with `DSH_OS_POWERSHELL_EDITION=7.6` selects
877
+ `bin/dsh-os7.ps1` and the runtime that answers is **PowerShell 7.6.6**, while the default stays on
878
+ 5.1.19041.3031 — that is what `tools/verify-editions.mjs` asserts and what the 14 cases measured here
879
+ confirm. What has *not* been done is a run of every verb against `dsh-os7.ps1`; its fidelity as a port
880
+ rests on the identical 82-verb dispatch sets, a per-module function-name comparison and the window
881
+ group's golden cases. That is a real gap in the evidence, not a claim of full parity.
641
882
 
642
883
  ### What is not proven
643
884
 
644
- * That the DSH loader composes this bundle and makes `dsh_os` appear in an active session.
885
+ * That the loader composes the bundle **by the contract `tools/verify-bundle.mjs` checks**, rather than
886
+ by a path that happens to work on this machine. The weaker claim — that `dsh_os` and the five
887
+ `os_permission_*` tools appear and are callable in an active session — has since been observed.
645
888
  * Complete verb parity between the two platforms — in neither direction.
646
889
  * The enforcement effect of layer A on a machine where the harness and the delegate share one identity
647
890
  — by construction that case degrades to layer B.
@@ -665,15 +908,29 @@ npm run test:all # runs everything below, in order
665
908
  |---|---|---|---|
666
909
  | Table conformance | `powershell -File tests/conformance.ps1` | 5 | **5 passed, 0 failed** |
667
910
  | Encoding contracts | `powershell -File tests/encoding.ps1` | 32 build modules | **all contracts hold** |
668
- | Windows golden | `powershell -File tests/golden.ps1` | 49 | **49 passed, 0 failed** |
911
+ | Windows golden | `powershell -File tests/golden.ps1` | 50 | **50 passed, 0 failed** |
669
912
  | POSIX golden | `bash tests/golden-sh.sh` | 116 | **116 passed, 0 failed** |
670
913
  | POSIX desktop injection | `bash tests/smoke-desktop-sh.sh` | 52 | **52 passed, 0 failed** |
671
914
  | Plugin | `node tests/plugin.test.mjs` | 13 | **13 passed, 0 failed** |
915
+ | Takeover interceptions | `node tests/takeover.test.mjs` | 41 | **41 passed, 0 failed** |
672
916
  | Permission delegate | `node tests/permission.test.mjs` | 18 | **18 passed, 0 failed** |
673
- | Bundle resolution | `node tools/verify-bundle.mjs` | 10 | **10 passed, 0 failed** |
674
- | Plugin load | `node tools/verify-plugin-load.mjs` | 13 | **13 passed, 0 failed** |
917
+ | Workflow | `node tests/workflow.test.mjs` | 30 | **30 passed, 0 failed** |
918
+ | Bundle resolution | `node tools/verify-bundle.mjs` | 16 | **16 passed, 0 failed** |
919
+ | Package completeness | `node tools/verify-package.mjs` | 21 | **21 passed, 0 failed** |
920
+ | Plugin load | `node tools/verify-plugin-load.mjs` | 30 | **30 passed, 0 failed** |
675
921
  | Edition selection | `node tools/verify-editions.mjs` | 14 | **14 passed, 0 failed** |
676
922
  | Build drift | `powershell -File tools/build.ps1 -Check` | 3 artefacts | **no drift** |
923
+ | Prefix cache (`npm run measure:cache`) | `node tools/measure-cache.mjs` | 2 claims | **PASS — 96.6% mean hit over four workflow calls** |
924
+
925
+ `measure:cache` is **not** part of `test:all`: it calls the real provider, so it needs
926
+ `DEEPSEEK_API_KEY` (read from `$DSH_HOME/.credentials.yaml`) and it costs tokens. It exits non-zero only
927
+ when a measured claim does not hold.
928
+
929
+ > [!NOTE]
930
+ > **`test:desktop` needs `bash`, and this machine has none.** `npm run test:all` therefore ends with
931
+ > `'bash' is not recognized` on a Windows host without Git Bash or WSL on `PATH`. That is an environment
932
+ > gap in the runner, not a failing assertion: every suite above it ran and passed in the same invocation.
933
+ > The POSIX suites remain unverified here for the same reason they always were — they need a POSIX host.
677
934
 
678
935
  ### Conformance of the two tables
679
936
 
@@ -812,7 +1069,7 @@ Copyright (c) 2026 Eloy Kim
812
1069
 
813
1070
  The licence file is pure ASCII, LF-terminated and BOM-free, consistent with the rest of the
814
1071
  repository's encoding contracts. One earlier description needs correcting: it is **not** listed in
815
- `package.json`'s `files` whitelist (which has 14 entries; it includes `src/host/bin/dsh-os7.ps1` but
1072
+ `package.json`'s `files` whitelist (which has 20 entries; it includes `src/host/bin/dsh-os7.ps1` but
816
1073
  neither `LICENSE` nor `README.zh-CN.md`), so the file stays in a checkout but is not distributed with
817
1074
  the published package.
818
1075