deepseek-coordination 0.1.9 → 0.10.0

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
@@ -1,1083 +1,349 @@
1
- # deepseek-coordination
2
-
3
- Cross-platform **atomic OS-interaction surface** for the DeepSeek Harness.
4
-
5
- An agent that talks to the operating system through *strings* gets the wrong answer whenever a path
6
- contains a space, a file carries a UTF-8 BOM, a line ending is CRLF, a document is not UTF-8, or a
7
- command is assembled by concatenation. This project replaces string assembly with an **argument-stable
8
- verb set** and makes encoding, byte order marks and line endings explicit and byte-exact.
9
-
10
- The same verb surface is implemented natively on Windows and on POSIX, and can be driven either by an
11
- agent through a single harness tool or by a person directly in a terminal.
12
-
13
- ```console
14
- $ dsh-os selfcheck
15
- {"ok":true,"verb":"selfcheck","result":{"script":"bin/dsh-os.ps1","capabilities":{...}},"meta":{...}}
16
-
17
- $ dsh-os file charset "C:\path with spaces\notes.txt"
18
- {"ok":true,"verb":"file.charset","result":{"charset":"utf-8-bom","bom":true,"eol":"crlf"},"meta":{...}}
19
- ```
20
-
21
- * **Fully open source**, MIT licensed. No telemetry, no account, no network request.
22
- * **Zero runtime dependencies.** On Windows it is `Add-Type` P/Invoke; on POSIX it uses system tools
23
- such as coreutils. Nothing is fetched at run time.
24
- * **One self-contained file per platform**, assembled from modules by a deterministic build.
25
-
26
- > [!WARNING]
27
- > This layer can **read your screen, capture screenshots, read and write the clipboard, synthesise
28
- > mouse and keyboard input, start and kill processes, delete files and modify environment variables**.
29
- > Before granting it any permission, read [Dangerous permissions](#dangerous-permissions).
30
-
31
- ---
32
-
33
- ## Why it is needed
34
-
35
- The failure mode is always the same shape: a value becomes part of a string and is then reinterpreted
36
- downstream.
37
-
38
- | Fault | What goes wrong | What this project does |
39
- |---|---|---|
40
- | `C:\Program Files\a b.exe` | a space splits one path into two arguments | values are always **separate argv elements**, never spliced into a shell string |
41
- | a UTF-8 BOM in the middle of text | every consumer sees `\uFEFF` | the BOM is stripped on read and **never** leaks into a result; it is restored on write |
42
- | a CRLF file rewritten as LF | diff noise, broken scripts | reads report `eol`; writes restore the original convention |
43
- | a GBK document read as UTF-8 | silent mojibake (or a silent fall back to `latin1`) | the encoding is declared explicitly; strict UTF-8 is tried first, then a fall back |
44
- | a command built by concatenation | injection, quoting bugs | `execute.command` takes a real **argv array** and never goes through a shell |
45
- | a byte offset landing inside a character | text is cut to pieces | offsets are validated against the encoding's character boundaries |
46
-
47
- Every call returns **exactly one JSON envelope**, and a caller branches on `error.code` rather than
48
- parsing prose — and a **failing** call emits a parseable envelope too, because a bare exit code tells
49
- an agent nothing.
50
-
51
- ---
52
-
53
- ## Repository layout
54
-
55
- ```
56
- deepseek-coordination/
57
- ├─ src/host/command.txt authoritative verb contract, one verb per line
58
- ├─ src/host/spec/verbs.tsv machine-readable verb table (single source of truth), 86 verbs
59
- ├─ src/host/spec/charsets.tsv IANA name -> .NET code page -> iconv name -> BOM bytes, 15 charsets
60
- ├─ src/host/lib/powershell5.1/*.ps1 source: Windows PowerShell 5.1 side, the 16 modules build.ps1 lists plus the standalone git.ps1
61
- ├─ src/host/lib/powershell7.6/*.ps1 source: PowerShell 7.6 side, the same 16 modules plus the standalone git.ps1
62
- ├─ src/host/lib/*.sh source: POSIX side, 16 shell modules plus the standalone git.sh
63
- ├─ src/host/lib/powershell.md the 5.1 -> 7.6 API-change reference (referenced by no code, test or package.json; not published)
64
- ├─ src/host/bin/dsh-os.ps1 [generated] Windows PowerShell 5.1 implementation
65
- ├─ src/host/bin/dsh-os7.ps1 [generated] PowerShell 7.6 implementation
66
- ├─ src/host/bin/dsh-os.sh [generated] POSIX implementation
67
- ├─ src/host/bin/dsh-os.bat ASCII-only cmd.exe shim (hand-written, not generated)
68
- ├─ src/host/docs/os-spec.md full specification: contracts, error codes, golden matrix
69
- ├─ src/host/docs/permission-model.md the authorization model and its residual risk
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
76
- ├─ src/permission/index.js consent delegate: the only writer of permission.json, registers five `os_permission_*` tools
77
- ├─ tools/build.ps1 assembles the three generated scripts; `-Check` fails on drift
78
- ├─ tools/parse-check.ps1 parse-checks each module under the runtime that will execute it, and reports non-ASCII bytes and a BOM
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)
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)
83
- ├─ tools/verify-editions.mjs proves both PowerShell artefacts are reachable (14 cases)
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
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 (31 cases)
89
- │ ├─ smoke-desktop-sh.sh what the POSIX desktop verbs actually inject (52 cases)
90
- │ └─ fake-xdotool, fake-wmctrl recording stand-ins, so the desktop verbs can be verified with no X server
91
- └─ permission.json authorization data (an empty file in the repository; that is the initial state)
92
- ```
93
-
94
- The two PowerShell runtimes live in separate directories because the differences are not cosmetic:
95
- .NET Framework 4.8 has no `ProcessStartInfo.ArgumentList` and needs an ANSI-aware encoding path, while
96
- .NET 10 has both. Keeping them apart means neither side has to carry the other's workarounds.
97
-
98
- `src/host/lib/` holds one module per concern, mirrored across the three targets:
99
-
100
- | Module | Concern |
101
- |---|---|
102
- | `preamble` | strict mode, encoding pins, repository-root discovery |
103
- | `json` | error model, JSON serialisation, the envelope writer |
104
- | `command` | argument parsing (positional and `name="value"`), assertions, coercion |
105
- | `charset` | charset table, BOM policy, byte-level text model |
106
- | `native` | P/Invoke declarations and atomic replace (Windows only; empty on POSIX) |
107
- | `paths` | path resolution and the protected-path boundary |
108
- | `file` | `file.*`, `file verify.*`, `file search` |
109
- | `system` | `system.*` queries |
110
- | `execute` | `execute.command` / `.synchronous` / `.asynchronous` |
111
- | `task` | `task.*` |
112
- | `environment` | `environment.*` and `environment attribute.*` |
113
- | `service` | `service.*` |
114
- | `permission` | `permission.*` (three read-only verbs and three delegate-only writes) and `selfcheck` |
115
- | `window` | `window.*`, `mouse.*`, `keyboard.*`, `clipboard.*`, `wait.*`, the observation verbs |
116
- | `dispatch` | verb resolution and the dispatch table |
117
- | `entry` | run, emit, exit |
118
-
119
- The shipped script is **one self-contained file** rather than dot-sourced modules at run time. On
120
- Windows PowerShell 5.1 a sourced file without a BOM is decoded in the ANSI code page — which is
121
- precisely the class of encoding fault this project exists to remove.
122
-
123
- ---
124
-
125
- ## Getting started
126
-
127
- Published on npm, so a DSH profile can install it as a bundle:
128
-
129
- ```console
130
- dsh plugin install deepseek-coordination
131
- ```
132
-
133
- Or work from source:
134
- ```console
135
- git clone https://github.com/eloyhere/deepseek-coordination.git
136
- cd deepseek-coordination
137
- ```
138
-
139
- The scripts need no build or installation; run them directly:
140
-
141
- ```powershell
142
- # Windows
143
- src\host\bin\dsh-os.ps1 selfcheck
144
- src\host\bin\dsh-os.ps1 file charset "C:\path with spaces\file.txt"
145
- src\host\bin\dsh-os.ps1 file verify charset .\file.txt utf-8
146
- src\host\bin\dsh-os.ps1 environment attribute get PATH --scope process
147
- ```
148
-
149
- ```sh
150
- # POSIX
151
- src/host/bin/dsh-os.sh selfcheck
152
- src/host/bin/dsh-os.sh file charset "./a file.txt"
153
- ```
154
-
155
- `dsh-os.bat` is for a person typing in `cmd.exe`. Callers that can pass an argv array — the harness,
156
- Node's `execFile`, CI — should call the `.ps1` or `.sh` **directly** and bypass `cmd.exe` entirely,
157
- because batch-file argument handling is itself a source of quoting bugs.
158
-
159
- ### Using it from the harness
160
-
161
- `package.json` declares `dsh.bundle.patch`, so the package installs into a DSH profile as a bundle and
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.
175
-
176
- | Tool | Purpose |
177
- |---|---|
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)) |
181
-
182
- One tool rather than one per verb, for three reasons: the verb table is the contract and lives in
183
- `src/host/spec/verbs.tsv`, so no schema needs regenerating when a verb is added; an agent discovers the
184
- whole surface with a single `verb: "selfcheck"` call; and the envelope is already the stable result
185
- shape, so there is no second contract to drift from the script's.
186
-
187
- ```jsonc
188
- // call shape
189
- { "verb": "file charset", "arguments": ["path=\"./a.txt\""], "allow": false }
190
- { "verb": "environment attribute get", "arguments": ["PATH"], "flags": ["--scope", "process"] }
191
- ```
192
-
193
- The implementation is spawned by absolute path with an argv array, never through a shell string, so a
194
- value containing spaces stays one value.
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
-
238
- **Choosing the PowerShell edition.** The default remains the 5.1 artefact (`bin/dsh-os.ps1`),
239
- **deliberately unchanged**: a deployment that already works should not change behaviour underneath
240
- itself because a plugin was upgraded. To select the 7.6 artefact (`bin/dsh-os7.ps1`):
241
-
242
- * `powershellEdition: '7.6'` — plugin configuration;
243
- * `DSH_OS_POWERSHELL_EDITION=7.6` — for a deployment that cannot edit its profile;
244
- * `'auto'` — uses 7.6 only when both the artefact and a working `pwsh` are present.
245
-
246
- Configuration beats the environment variable, and an explicit `scriptPath` disables the choice
247
- entirely. `pwsh` is looked up as an explicit `DSH_OS_POWERSHELL`, then on `PATH`, then in the standard
248
- install locations, because `pwsh` is not always on `PATH` — it is not on this repository's own
249
- environment, while 7.6.6 is plainly installed in the standard location.
250
-
251
- > [!IMPORTANT]
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 —
254
- > are present and callable in a running session. What is still **not** proven is the part that
255
- > `tools/verify-bundle.mjs` cannot cover: that the *harness's own loader* composes the bundle by the
256
- > same contract the verifier checks, rather than by a shortcut that happens to work here. The
257
- > verifier remains the evidence; the live session is corroboration.
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
-
423
- ---
424
-
425
- ## JSON contract
426
-
427
- Every call writes **exactly one** JSON envelope to stdout, UTF-8 **without** a BOM, terminated by a
428
- single LF.
429
-
430
- ```jsonc
431
- // success
432
- { "ok": true, "verb": "file.charset", "result": { "charset": "utf-8", "bom": false, "eol": "crlf" },
433
- "meta": { "durationMs": 3, "platform": "win32", "shell": "pwsh" } }
434
-
435
- // failure — still exactly one envelope, still on stdout
436
- { "ok": false, "verb": "file.charset",
437
- "error": { "code": "E_NOT_A_FILE", "message": "…", "hint": "…" },
438
- "meta": { "durationMs": 2, "platform": "win32", "shell": "pwsh" } }
439
- ```
440
-
441
- * `error.hint` appears only when there really is an actionable suggestion.
442
- * `--pretty` indents the output; `--quiet` emits only `result` (or only `error`).
443
- * **stderr carries human-readable diagnostics only and never takes part in parsing.** On Windows,
444
- setting `DSH_OS_TRACE=1` puts a failure trace on stderr.
445
-
446
- ### `error.code` is authoritative
447
-
448
- Exit codes are a coarse class, so that a shell can branch cheaply. **Never parse `error.message`.**
449
-
450
- | Exit code | Class | Representative codes |
451
- |---|---|---|
452
- | 0 | success | — |
453
- | 1 | usage | `E_USAGE` |
454
- | 2 | type / value | `E_TYPE`, `E_CHARSET_UNKNOWN`, `E_BOUNDARY`, `E_RANGE`, `E_WINDOW_AMBIGUOUS` |
455
- | 3 | authorization | `E_PERMISSION`, `E_ELEVATION_REQUIRED`, `E_PERMISSION_PROTECTED_PATH`, `E_PERMISSION_DELEGATE_ONLY`, `E_PERMISSION_FILE_UNTRUSTED`, `E_PERMISSION_DENIED_BY_RULE`, `E_PERMISSION_NO_ANSWERER` |
456
- | 4 | platform unsupported | `E_NOT_SUPPORTED`, `E_NO_DISPLAY`, `E_ACCESSIBILITY_DENIED`, `E_ACCESSIBILITY_UNAVAILABLE` |
457
- | 5 | target / encoding / IO | `E_NOT_FOUND`, `E_NOT_A_FILE`, `E_EXISTS`, `E_BINARY_CONTENT`, `E_CHARSET_MISMATCH`, `E_TRUNCATED`, `E_PARTIAL_WRITE`, `E_WINDOW_NOT_FOUND`, `E_IO` |
458
- | 6 | timeout | `E_TIMEOUT` |
459
- | 7 | internal / aborted | `E_INTERNAL`, `E_ABORTED` |
460
-
461
- The Windows implementation registers **36** error codes; the POSIX implementation groups them with
462
- `case`. The two agree on the class of every **named** code, but not on unnamed ones: Windows returns
463
- 7, POSIX returns 5.
464
-
465
- ### Calling convention
466
-
467
- Values are always separate argv elements and are **never** spliced into a shell string. The two
468
- spellings below are equivalent, but the same parameter must not be given twice in different forms:
469
-
470
- ```sh
471
- # positional form
472
- dsh-os file create "./a.txt" "123" "utf-8-bom"
473
-
474
- # named form — order is free; the effective order comes from the verb table's parameter list
475
- dsh-os file create content="123" charset="utf-8-bom" path="./a.txt"
476
- ```
477
-
478
- `E_USAGE` rejects: an unknown flag, an empty `--`, a value-taking flag with no value, a repeated named
479
- argument, an unknown name, the same parameter given both positionally and by name, and surplus
480
- positional arguments. Read operations are never asked for `--allow`; write operations always are.
481
-
482
- A verb is one to three tokens deep — `selfcheck`, `file charset`, `file verify charset` — and the
483
- parser consumes the next token only when extending the verb is genuinely known to be possible, so a
484
- path argument that happens to begin with a word is not swallowed into the verb.
485
-
486
- ### Encoding, BOM and line endings
487
-
488
- `src/host/spec/charsets.tsv` defines 15 canonical encoding names, each with aliases, a .NET code page,
489
- an `iconv` name and BOM bytes:
490
-
491
- `utf-8`, `utf-8-bom`, `utf-16le`, `utf-16be`, `utf-32le`, `utf-32be`, `gbk`, `gb18030`, `big5`,
492
- `shift_jis`, `euc-jp`, `euc-kr`, `latin1`, `windows-1252`, `ascii`.
493
-
494
- Detection sniffs the BOM first, then tries **strict** UTF-8, then GBK, then latin1. A read strips the
495
- BOM and normalises `\r\n` to `\n` for output; **every write restores the file's original BOM presence
496
- and line-ending convention.** Offsets are byte offsets in the half-open range `[start, end)` and must
497
- fall on a character boundary, otherwise the call fails with `E_BOUNDARY` rather than cutting a
498
- character in half.
499
-
500
- A file is judged binary when a NUL byte appears in the first 8 KiB of the payload, which reports
501
- `E_BINARY_CONTENT` instead of returning mojibake.
502
-
503
- ---
504
-
505
- ## The verb surface
506
-
507
- `src/host/spec/verbs.tsv` **declares 86 verbs** and is the single source of truth. Some are declared
508
- but not implemented; before relying on one, read
509
- [What is actually available](#what-is-actually-available).
510
-
511
- | Group | Verbs |
512
- |---|---|
513
- | `file` | `create` `charset` `encode` `length` `information` `list` `delete` `move` `copy` `mkdir` `append` `insert` `view` `search` |
514
- | `file verify` | `charset` `encoding` `completion` |
515
- | `execute` | `command` `synchronous` `asynchronous` `query` `kill` |
516
- | `system` | `brand` `cpu` `gpu` `version` `info` |
517
- | `task` | `list` `query` `show` `kill` `launch` |
518
- | `service` | `list` `query` `start` `stop` `restart` |
519
- | `environment` | `list` `get` `add` `set` `delete` |
520
- | `environment attribute` | `list` `get` `add` `set` `delete` |
521
- | `permission` | `query` `list` `audit` `grant` `revoke` `acl init` |
522
- | `window` | `find` `list` `active` `close` `top` `bottom` `move` `resize` `minimize` `maximize` `restore` |
523
- | `mouse` | `click` `context` `over` `wheel` `down` `up` `cursor` |
524
- | `keyboard` | `down` `up` `press` `combination` `type` `codes` |
525
- | `clipboard` | `read` `write` `paste` |
526
- | `wait` | `window` `ms` |
527
- | observation | `screenshot` `text` `structure` |
528
- | self-check | `selfcheck` |
529
-
530
- A few design points worth knowing before you start:
531
-
532
- * **`file verify.*` asserts; it does not throw.** `file verify charset` returns `match: false`
533
- alongside the observed value, so a caller can tell "the file is wrong" from "I asked wrongly".
534
- `file verify completion` returns `complete: null` when no expectation was given — deliberately not
535
- `false`.
536
- * **`file insert`'s `--charset` is a flag, not a positional.** Inserted between `content` and `offset`
537
- it would **silently displace `offset`**.
538
- * **`file.delete` goes to the recycle bin on Windows by default.** On POSIX it needs `trash-put`,
539
- otherwise it reports `E_NOT_SUPPORTED` — it does **not** silently delete for real.
540
- * **`environment` has two levels.** A variable holds one string; an **attribute** (PATH, PATHEXT, LIB,
541
- INCLUDE) holds an ordered list. A write returns `visibleToRunningProcesses`, because a `user` or
542
- `machine` level write is invisible to processes that are already running.
543
- * **`keyboard type` is Unicode-first.** Non-ASCII text goes through a clipboard paste, because
544
- `KEYEVENTF_UNICODE` is unreliable when an input method is active.
545
-
546
- ---
547
-
548
- ## Dangerous permissions
549
-
550
- This chapter is required reading before granting any permission. The layer is deliberately capable,
551
- and the guarantees it offers are narrower than the feature list suggests.
552
-
553
- ### What a fully authorized agent can do
554
-
555
- | Capability | Verbs involved | Authorization |
556
- |---|---|---|
557
- | **Read the contents of your screen** | `screenshot`, `text`, `structure` | **read-only, never prompts** |
558
- | **Read and write your clipboard** | `clipboard.read`, `clipboard.write` | `read` never prompts; `write` needs `--allow` |
559
- | **Synthesise input** — clicks, drags, typing, chords | `mouse.*`, `keyboard.*` | `--allow` |
560
- | **Manipulate your windows** — move, resize, close, raise | `window.*` | `--allow` |
561
- | **Start and kill processes** | `task.kill`, `execute.*` | `--allow` |
562
- | **Run an arbitrary command line** | `execute.synchronous`, `execute.asynchronous` | `--allow` |
563
- | **Delete, move and overwrite files** | `file.delete`, `file.move`, `file.copy` | `--allow` |
564
- | **Persistently modify your environment variables** | `environment.set`, `.add`, `.delete`, `attribute.*` | `--allow` |
565
- | **Control system services** (declared, not implemented) | `service.start`, `.stop`, `.restart` | `--allow` + elevation |
566
- | **Read process and system inventories** | `task.*`, `system.*` | **read-only, never prompts** |
567
-
568
- Put plainly: `mouse.*` and `keyboard.*` mean an authorized agent can operate **any** program you can
569
- operate, with your rights. And because it can read the screen, it can also watch what it has just
570
- done. Inside the desktop there is no sandbox.
571
-
572
- ### How authorization actually works
573
-
574
- **Layer B — structural enforcement (always on, cross-platform).**
575
-
576
- * Every mutating verb requires `--allow`. Without it the call returns `E_PERMISSION` and **nothing
577
- happens**.
578
- * **`permission.json` has three write verbs, but they go through the delegate and not through the
579
- script's own authorization.** The table declares `permission grant`, `permission revoke` and
580
- `permission acl.init` (all class `W`), while `permission query`, `permission list` and
581
- `permission audit` are read-only. The flags of `grant` and `revoke` accept only `delegate` (besides
582
- `allow`), so they can be called **only by the consent delegate**; the script itself cannot create a
583
- persistent authorization with `--allow`, and reports `E_PERMISSION_DELEGATE_ONLY` when no delegate
584
- is present.
585
- * A hard **protected-path boundary** rejects any modification of the files that decide what this layer
586
- may do: `permission.json` (and `.bak`), `src/host/command.txt`, the **four** `src/host/bin/dsh-os.*`
587
- entry points (`.ps1`, `dsh-os7.ps1`, `.sh`, `.bat`), `src/host/spec/verbs.tsv` and
588
- `src/host/spec/charsets.tsv`, plus any same-named temporary staged beside one of them. A violation
589
- reports `E_PERMISSION_PROTECTED_PATH` with no side effect. Reads remain allowed so that you can
590
- review the rules. The POSIX list has eight entries — the same list **without `dsh-os7.ps1`**.
591
- * The check runs on the **resolved and normalised** path, compared segment by segment rather than by
592
- regular expression, because `..`, an 8.3 short name or a trailing dot all walk around a regex.
593
- `file.move` and `file.copy` check **both ends**, since moving a protected file is also a way of
594
- destroying it.
595
-
596
- **Layer A — kernel ACL (Windows, explicitly enabled, requires elevation).**
597
-
598
- `permission acl init` appends an explicit **DENY** for `Everyone` (`S-1-1-0`) to the DACL of
599
- `permission.json`, covering `FILE_GENERIC_WRITE | DELETE | WRITE_DAC | WRITE_OWNER`. It is idempotent,
600
- and reports `E_ELEVATION_REQUIRED` when it cannot act rather than failing vaguely.
601
-
602
- `selfcheck` reports `aclEnforced` by **reading the file's real security descriptor** and walking the
603
- DACL. An earlier version read a flag from a state file, which claimed an enforcement that was not in
604
- effect; that implementation has been deleted. `aclReason` explains a `false`, so the result can be
605
- acted on rather than merely wondered at.
606
-
607
- ### Four limitations that must be stated honestly
608
-
609
- These are documented in `src/host/docs/permission-model.md` and repeated here because they change what
610
- the guarantees above mean.
611
-
612
- 1. **Persistent rules are recorded but not enforced at the verb layer.** The rules in `permission.json`
613
- are matched by the consent delegate and its `os_permission_query` tool; the script reads the file
614
- for **display only** (`permission list/query/audit`). No verb consults a rule before acting: the
615
- real gates are `--allow`, the protected-path check, and (if enabled) layer A. A `deny-always` rule
616
- does **not** currently stop a verb that arrives with `--allow`.
617
- 2. **`E_PERMISSION_DENIED_BY_RULE` is registered but never thrown.** It is in the error table and maps
618
- to exit code 3, but no code path produces it — consistent with point 1.
619
- 3. **The in-script rule reader is not deny-first.** The script's `permission query` takes the **first**
620
- matching rule regardless of whether it allows or denies. The delegate implements deny-first
621
- correctly and has test coverage, so the two readers can disagree, and the script side is the weaker
622
- one.
623
- 4. **`execute` is a way around the protected-path boundary.** That boundary guards `file.*` and
624
- `environment.*`. It cannot constrain a child process: `execute.synchronous` hands the command line
625
- to `cmd.exe /d /s /c` on Windows and to `sh -c` on POSIX, so the program it starts can write
626
- anything its own rights permit, `permission.json` included. Only layer A prevents that. When
627
- `aclEnforced: false`, layer B cannot.
628
-
629
- The consent delegate has one more **design gap** worth stating plainly: it registers five tools
630
- (`os_permission_query`, `os_permission_grant`, `os_permission_revoke`, `os_permission_list`,
631
- `os_permission_audit`), but it does **not** register an `approval/request` answerer. Its own design
632
- document says it should, and that `permission grant` **must** go through a user-confirmed prompt. As
633
- the code stands, the enforcement is a **tool description** telling the model to call
634
- `os_permission_grant` only after the user has chosen; the code does not verify that a prompt ever
635
- happened. Treat the delegate as trusted infrastructure, not as a check on the agent.
636
-
637
- Two further points about layer A: it requires the harness and the delegate to run as **different
638
- identities** (otherwise it degrades to layer B), and its POSIX equivalent is weaker — a mode change
639
- that the same user can undo. `permission acl init` says as much in its own result.
640
-
641
- ### Reviewing what has been granted
642
-
643
- ```sh
644
- dsh-os permission list # rules, defaults, and the MEASURED aclEnforced
645
- dsh-os permission query file.delete "R:\project\**"
646
- dsh-os permission audit --limit 50 # the append-only tail; the delegate trims to 200 entries
647
- ```
648
-
649
- If `permission.json`'s `updatedBy` is not `consent-delegate`, the file is treated as **untrusted**: its
650
- rules are ignored, everything falls back to the defaults, and the reason is reported. A malicious rule
651
- hand-edited into that file therefore does **not** take effect — it is ignored, and you are told.
652
-
653
- To **create** a persistent decision you must use the delegate's tools. A wildcard target
654
- (`target.value === "*"`) is refused unless `acknowledgeBroad: true` is passed explicitly, because
655
- "agree to everything, permanently" has to be a separate decision and not the side effect of one click.
656
-
657
- ---
658
-
659
- ## What is actually available
660
-
661
- Method: the platform dispatch tables are extracted and compared against the declared verb table, entry
662
- by entry. An unimplemented verb **never fails silently**: it returns a named `E_NOT_SUPPORTED`
663
- envelope.
664
-
665
- | Platform | Declared | Implemented |
666
- |---|---|---|
667
- | Windows PowerShell 5.1 (`src/host/bin/dsh-os.ps1`) | 86 | **82** |
668
- | PowerShell 7.6 (`src/host/bin/dsh-os7.ps1`) | 86 | **82** |
669
- | POSIX (`src/host/bin/dsh-os.sh`) | 86 | **61** |
670
-
671
- The two PowerShell runtimes have **identical** implemented sets, established by comparing the two
672
- dispatch tables entry by entry rather than by eye.
673
-
674
- ### Windows (5.1 and 7.6 alike)
675
-
676
- **Implemented (82):** every `file.*` (including `file.encode`); `file verify.*`; `file.search`; all
677
- five `execute` verbs (`command`, `synchronous`, `asynchronous`, `query`, `kill`); `system.*`; all of
678
- `task.*` (including `task.launch`); `service.list/query`; the whole `environment` surface (including
679
- `attribute.*`); the whole `permission` surface (including `grant`, `revoke`, `acl.init`); the whole
680
- `window` surface; `screenshot`; `clipboard.read/write`; `wait.window/ms`; the whole `mouse` and
681
- `keyboard` surfaces; `text`; `structure`; `selfcheck`.
682
-
683
- **Not implemented (4):** `clipboard.paste`, `service.start`, `service.stop`, `service.restart`.
684
-
685
- ### POSIX
686
-
687
- **Implemented (61):** the whole `file` surface except `file.encode`; `file verify.*`; `file.search`;
688
- all five `execute` verbs (including `execute.command`, which the Windows side also has); `system.*`;
689
- `task.list`/`query`/`show`/`kill` (**not `task.launch`**); the whole `environment` variable level
690
- (including persistent writes) and all five `attribute.*` verbs; the whole `permission` surface;
691
- `wait.ms`; the whole `mouse` surface (7 verbs) and `keyboard` surface (6 verbs); `selfcheck`.
692
-
693
- Of those 61, **13 need an X display** (the 7 `mouse.*` and 6 `keyboard.*`); the other 48 do not.
694
-
695
- **Not implemented (25):** `file.encode`; all eleven `window.*`; `screenshot`, `text`, `structure`;
696
- `clipboard.read`, `clipboard.write`, `clipboard.paste`; `wait.window`; `task.launch`; and all five
697
- `service.*` (`list`, `query`, `start`, `stop`, `restart`). These fall through the dispatch table's
698
- default branch to a **typed** `E_NOT_SUPPORTED`; they are not silently absent.
699
-
700
- > [!NOTE]
701
- > POSIX has no kernel API for persisting a variable, so `environment add/set/delete --scope user`
702
- > edits `${HOME}/.profile` through a **managed block** delimited by
703
- > `# >>> dsh-os managed environment >>>` and `# <<< dsh-os managed environment <<<`. Every change
704
- > regenerates everything between the markers, so these operations are idempotent and a user can undo
705
- > the lot by deleting the block. Writing bare assignments into the file directly would append
706
- > duplicates on the second call and could not be undone. `--scope machine` is unavailable and reports
707
- > `E_NOT_SUPPORTED`.
708
- >
709
- > `environment attribute add/set/delete` are **process scope only**; a persistent scope is refused
710
- > with `E_NOT_SUPPORTED`. That is deliberate: POSIX has no kernel API for a list-valued variable, so
711
- > persisting one means rewriting a shell startup file, and a wrongly reconstructed `PATH` there can
712
- > make a machine's shells unusable.
713
-
714
- > [!IMPORTANT]
715
- > **On POSIX only `mouse.*` (7) and `keyboard.*` (6) need an X display.** With no `DISPLAY` they return
716
- > a **typed** `E_NO_DISPLAY`; with a display but no `xdotool` they return `E_NOT_SUPPORTED` naming the
717
- > missing tool.
718
- >
719
- > **`wait.ms` does not need one** — it is a sleep, so it can be accepted in full on this machine. The
720
- > five `environment attribute.*` verbs do not either (they act on environment variables, not on the
721
- > desktop).
722
- >
723
- > On this machine (no `DISPLAY`, no `xdotool`) the golden suite therefore asserts them as
724
- > `E_NO_DISPLAY`: **that asserts the absence path, not that they work.** The injection path is covered
725
- > by `tests/smoke-desktop-sh.sh` with stand-in tools; see [Tests](#tests).
726
-
727
- > [!NOTE]
728
- > What remains **structurally absent** on POSIX is `window.*` (11), `screenshot`, `text`, `structure`,
729
- > `clipboard.*`, `wait.window` and all of `service.*`. `native.sh` (515 bytes) and `service.sh`
730
- > (333 bytes) are deliberate empty stubs whose headers explain why: their POSIX counterparts are
731
- > external programs (`wmctrl`, `import`, `xclip`, `scrot`) rather than linkable libraries.
732
- >
733
- > **`window.sh` is no longer a stub** (26 KB): it now implements `wait.ms`, all of `mouse.*` and all of
734
- > `keyboard.*`, driving X11 through `xdotool` and checking for the display and the tools before
735
- > touching anything. The remaining desktop verbs still return `E_NOT_SUPPORTED` from the dispatch
736
- > table's default branch.
737
-
738
- ### The two implementations are not equivalent
739
-
740
- Same verb names and the same calling convention, but behaviour differs in several places. The
741
- important ones:
742
-
743
- | Aspect | Windows | POSIX |
744
- |---|---|---|
745
- | `execute.command` | implemented | implemented (real argv, no shell) |
746
- | `file.encode` | implemented | not implemented |
747
- | `file.delete` default | recycle bin | fails without `trash-put` |
748
- | `file.search` literal match | case-insensitive | case-sensitive |
749
- | `file.view` character boundaries | enforced (`E_BOUNDARY`) | **not checked at all** |
750
- | `file.view --max-bytes` | honoured | ignored (`truncated` is always `false`) |
751
- | files over 32 MiB | `E_TRUNCATED` | no limit |
752
- | `--eol crlf` on create/append | exact bytes | adds one extra trailing CRLF |
753
- | appending to a mixed-EOL file | existing endings untouched | the whole file is renormalised |
754
- | a character that cannot be represented | silently written as `?` | `E_ENCODING_INVALID` |
755
- | `--pretty` / `--quiet` | honoured | accepted and ignored |
756
- | `--` | `E_USAGE` | end-of-options marker |
757
- | protected-path comparison | case-insensitive, normalised | case-sensitive, as given |
758
- | unregistered error code | exit code 7 | exit code 5 |
759
- | `meta.durationMs` on failure | the real duration | `0` |
760
-
761
- Measured here, because prose is not evidence: on POSIX,
762
- `file view <utf-8 file> utf-8 0 2` returns `ok` with `"text":"\ufffd"` — an offset of 2 lands inside
763
- the first three-byte character and the character is split, where Windows fails the same call with
764
- `E_BOUNDARY`.
765
-
766
- `file.search`'s `--type` is a latent defect on both implementations: the code reads it, but it is not
767
- registered as a known flag, so behaviour depends on the parser's fallback path.
768
-
769
- ---
770
-
771
- ## Installation status and known defects
772
-
773
- Stated plainly, so that nobody is caught out.
774
-
775
- ### The bundle's load path is now covered by three verifiers
776
-
777
- This section previously recorded that the loader's end-to-end path was **unverified**. That gap has
778
- been closed by three verifiers, and they found one real defect:
779
-
780
- | Verifier | What it proves | Cases |
781
- |---|---|---|
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 |
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 |
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 |
785
-
786
- The bundle verifier links the package into a throwaway profile sandbox by junction, so resolution
787
- happens exactly as it would in a profile, and the **active profile is never written to**.
788
-
789
- The defect it found: `bin/dsh-os7.ps1` was previously **unreachable from the plugin** — it existed,
790
- built and tested, but `resolveInvocation` only ever looked for `dsh-os.ps1`, so all the 7.6 work was
791
- dead code from the plugin's point of view. It is now selectable, and **the default is unchanged**
792
- (still 5.1), because a deployment that already works should not change behaviour merely because a
793
- plugin was upgraded.
794
-
795
- **Now observed live, though not through this verifier:** the package has since been installed into an
796
- active desktop profile, and the six registered tools (`dsh_os` and the five `os_permission_*` tools)
797
- are present and callable in a running session. That corroborates the verifier; it does not replace it,
798
- because it does not establish that the loader composed the bundle by the same contract the verifier
799
- checks.
800
-
801
- ### The Windows golden suite's order dependency is fixed
802
-
803
- This was recorded as `passed: 47 failed: 1` on a clean checkout, the failing case being
804
- `parser/one-two-three-token-verbs`, which asserted `file length` against a fixture directory created
805
- by a **later** case.
806
-
807
- That defect is **fixed**: the parser case in `tests/golden.ps1` now creates its own fixture
808
- (`$fixtureDir = Join-Path $sandbox 'parser\verify'`, then `New-Item` and `Set-Content`) and asserts
809
- against it. The current measurement is **50 passed, 0 failed**.
810
-
811
- ### Two real POSIX defects found by `tests/smoke-desktop-sh.sh`
812
-
813
- Neither was visible after writing, and not by review either — because this machine has no `DISPLAY`,
814
- they normally showed up only as "an `E_NO_DISPLAY` either way". Opening the injection path with
815
- stand-in tools exposed both:
816
-
817
- 1. **Validation ran after injection.** `mouse click 10 20 --count abc` **returned an error, but the
818
- pointer had already been moved** — a call that reports failure had changed the state of the
819
- machine. The Windows side validates every argument *before* injecting; the POSIX side now matches
820
- it. (The same held for `mouse over abc 20`.)
821
-
822
- 2. **A `fail` inside `$(...)` did not stop the verb.** The coordinate validator called `fail`, but the
823
- caller wrote `x="$(window_int_arg ...)"`, so the `exit` ended only the subshell: the verb continued
824
- with an empty value and **injected anyway** — `mouse over abc 20` **moved the pointer and returned
825
- `ok`**. The three validators that reject input now **assign a variable** (`WINDOW_INT`,
826
- `WINDOW_BUTTON`, `WINDOW_KEY`) which is read in the **caller's own shell**.
827
-
828
- **This trap was already documented in this repository's `environment` module** (the comment on
829
- `env_resolve_scope` explains precisely why it assigns rather than prints), and it was still
830
- repeated here. Both defects are now pinned by three assertions each: invalid input must be
831
- **rejected with nothing injected**.
832
-
833
- ### Other gaps
834
-
835
- * **`tests/conformance.ps1` did not exist**, although the header of `spec/verbs.tsv` claimed it did and
836
- that it checked consistency with `command.txt`. **The file now exists** (see
837
- [Conformance of the two tables](#conformance-of-the-two-tables)), so the header's claim is now a
838
- fact. A comment naming a guard that is not there is worse than no comment, because it is read as a
839
- guarantee.
840
- * **The consent delegate never registers an approval answerer**, as described above.
841
- * **The delegate reports `aclEnforced: false` unconditionally** in its `query` result instead of
842
- measuring the ACL as the script does, so it **under-reports** protection when layer A is on.
843
- * **This section previously recorded that `--shell`, `--env`, `--stdin` and `--background` were
844
- accepted but never read, and that `--encoding` on Windows built a value that was never applied. Both
845
- records were out of date, and both had been real defects; they are now fixed.**
846
- * **Windows:** `--shell`, `--env`, `--stdin` and `--background` really are read (there are branches
847
- for them in `execute.ps1`). `--encoding` genuinely **was inert** — it built `$enc` and never used
848
- it, and `StandardOutputEncoding` appeared nowhere in the module set, so child output was always
849
- decoded with the runtime default. It is now wired to the output and error streams. **Measured:**
850
- with a child emitting the GBK bytes `D6 D0 CE C4`, the default decoding gives `����` (four
851
- replacement characters) and `--encoding gbk` gives `中文` (two characters).
852
- * **POSIX:** these flags genuinely were never read. They are now **explicitly refused** —
853
- `--shell`, `--env`, `--stdin`, `--background` and `--encoding` all return `E_NOT_SUPPORTED` (exit
854
- code 4) instead of being silently ignored. That is what this project's own contract requires: **an
855
- unavailable capability must be an explicit error, never an empty success**, and a caller that
856
- passed `--stdin`, had nothing fed in, and received a success envelope has been misled.
857
- * **Some declared error codes really are unreachable.** A full-text search of both implementations
858
- confirms **zero occurrences** of `E_ENCODING_INVALID` (for `file.encode`), `E_IS_A_DIR` (for
859
- `file.delete`) and `E_ABORTED`. By contrast `E_CHARSET_MISMATCH` (3 sites), `E_BINARY_CONTENT` and
860
- `E_TRUNCATED` all have real throw sites — so the table is not uniformly unreliable, but those three
861
- should be read as unimplemented intentions rather than dependable return codes.
862
- * **This section previously recorded that "`E_TIMEOUT` is never thrown by either side" — that record
863
- was out of date and wrong.** It is thrown in `execute.ps1` (an `execute command` timeout) and in
864
- `window.ps1` (a `wait window` timeout). Measured:
865
- `execute command cmd.exe /c ping -n 10 127.0.0.1 --timeout 300 --allow` returns
866
- `{"ok":false,...,"code":"E_TIMEOUT",...,"hint":"captured so far: stdout=86 bytes, stderr=0 bytes"}`
867
- with exit code **6**. POSIX has `E_TIMEOUT` too (`json.sh` maps it to exit code 6).
868
- * **`src/client/` is an untouched Vite scaffold.** It is not wired to the host: `index.html` refers to
869
- `./src/main.ts` while the real file is `./src/client/main.ts`, and there is no `node_modules`, so
870
- `npm run build` and `npm run dev` are currently meaningless.
871
- * **`git` is not a required dependency, but it is not present in every target environment either**, so
872
- no acceptance step depends on it. (The plugin's *installation* path does: the package manager
873
- resolves the repository with `git ls-remote`, so a machine where the harness cannot find `git` on
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.
882
-
883
- ### What is not proven
884
-
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.
888
- * Complete verb parity between the two platforms — in neither direction.
889
- * The enforcement effect of layer A on a machine where the harness and the delegate share one identity
890
- — by construction that case degrades to layer B.
891
- * Anything about macOS or non-Linux POSIX beyond code review: the POSIX suites are written for a GNU
892
- userland and were not run anywhere else.
893
-
894
- ---
895
-
896
- ## Tests
897
-
898
- ```console
899
- npm run test:all # runs everything below, in order
900
- ```
901
-
902
- `test:all` chains `test:conformance`, `test:encoding`, `test` (the Windows golden suite),
903
- `test:plugin`, `test:permission`, `test:bundle` (`tools/verify-bundle.mjs`), `test:load`
904
- (`tools/verify-plugin-load.mjs` then `tools/verify-editions.mjs`) and `test:desktop`
905
- (`bash tests/smoke-desktop-sh.sh`).
906
-
907
- | Suite | Command | Cases | Measured result |
908
- |---|---|---|---|
909
- | Table conformance | `powershell -File tests/conformance.ps1` | 5 | **5 passed, 0 failed** |
910
- | Encoding contracts | `powershell -File tests/encoding.ps1` | 32 build modules | **all contracts hold** |
911
- | Windows golden | `powershell -File tests/golden.ps1` | 50 | **50 passed, 0 failed** |
912
- | POSIX golden | `bash tests/golden-sh.sh` | 116 | **116 passed, 0 failed** |
913
- | POSIX desktop injection | `bash tests/smoke-desktop-sh.sh` | 52 | **52 passed, 0 failed** |
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** |
916
- | Permission delegate | `node tests/permission.test.mjs` | 18 | **18 passed, 0 failed** |
917
- | Workflow | `node tests/workflow.test.mjs` | 31 | **31 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** |
921
- | Edition selection | `node tools/verify-editions.mjs` | 14 | **14 passed, 0 failed** |
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.
934
-
935
- ### Conformance of the two tables
936
-
937
- The header of `spec/verbs.tsv` had always claimed that "`tests/conformance.ps1` asserts the two
938
- agree", while that file **did not exist** — it has now been **added**, so the claim is a fact. The
939
- guard resolves each line of `command.txt` to a verb by the **longest word-prefix that names a known
940
- verb** (so `file verify charset path charset` resolves to `file.verify.charset` and not to a
941
- nonexistent `file.verify`), then checks that the two tables hold each other's verbs, that the counts
942
- are equal, that the R/W class agrees, and that the positional names agree in order.
943
-
944
- **Writing it found five real inconsistencies**, and all but one of them were in `command.txt` (which is
945
- the authoritative contract):
946
-
947
- | Verb | Was declared | What the implementation actually does |
948
- |---|---|---|
949
- | `file.append` | `path content charset` | charset is an **optional** third positional; the old form implied it was required |
950
- | `file.insert` | `path content charset offset` | charset is `--charset` and **deliberately not** a positional — as a positional, omitting it would silently shift `offset` |
951
- | `wait.ms` | no positionals | takes `ms` |
952
- | `keyboard.combination` | two lines, `... down a b c d` / `... up a b c d` | it is **one** verb with a `phase` positional; the two-line form read as two verbs |
953
- | `execute.command` | tsv wrote `command\|arguments` | the entire positional list is argv; there is no separate `command` |
954
-
955
- Four were corrected in `command.txt`, and `verbs.tsv`'s `execute.command` row became `arguments...`.
956
- **Nothing about the running surface changed** — only what the two documents say — which is exactly
957
- what such a conformance guard is for.
958
-
959
- `tests/encoding.ps1` guards the contracts that make everything else meaningful: the `.ps1` artefacts
960
- carry a BOM, the `.sh` artefacts do not, line endings follow `.gitattributes`, `command.txt` and the
961
- `.tsv` tables stay pure ASCII + LF, and **stdout never carries a BOM**.
962
-
963
- **The "four encoding violations" recorded earlier are gone, and this is not a relaxation of the
964
- guard.** The rule itself never moved; its **scope** did. It used to require **every** `*.ps1` under
965
- `src/host/lib/` to be BOM-free and ASCII-only, and two of those files are the standalone `git.ps1`
966
- scripts — never concatenated into anything, and carrying a BOM because that is what makes their
967
- Chinese prompt literals parse under Windows PowerShell 5.1. Applying that rule to files the build
968
- never reads was a **false positive**. The guard now checks only the **32 modules `tools/build.ps1`
969
- actually concatenates**, and **prints which files it skipped**, so the narrowing is visible rather
970
- than silent.
971
-
972
- > [!NOTE]
973
- > **Neither the "116 passed" nor the "52 passed" for the POSIX desktop verbs is evidence that they
974
- > drive a real window manager.** The two suites cover the same verbs from two directions, and this
975
- > machine has no X server:
976
- > * `tests/golden-sh.sh` asserts the **absence** path — with no `DISPLAY`, every desktop verb must
977
- > return a typed `E_NO_DISPLAY` rather than an empty success.
978
- > * `tests/smoke-desktop-sh.sh` asserts the **injection** path — it puts the recording
979
- > `tests/fake-xdotool` on `PATH` and sets `DISPLAY`, so the verbs run their real code and the
980
- > assertions are about the **exact argument vector** handed to the tool: whether `--button right`
981
- > became button `3`, whether `Enter` became the keysym `Return`, whether a name outside the table
982
- > (`XF86AudioMute`) is passed through unchanged, whether `--clearmodifiers` and `--repeat` land in
983
- > the right place, and whether an `xdotool` failure is reported as `E_IO` rather than a false
984
- > success.
985
- >
986
- > **That harness is what found the two real defects in the POSIX implementation** (see
987
- > [What is actually available](#what-is-actually-available) and
988
- > [Installation status](#installation-status-and-known-defects)). Neither suite proves that they
989
- > operate a real desktop.
990
-
991
- > [!WARNING]
992
- > **The two golden suites must be run serially.** Both implementations stage a failure envelope in a
993
- > **fixed-path** temporary file — `.temporary/dsh-os.err`, with no PID and no lock — so concurrent
994
- > calls overwrite each other's pending result. Running `golden.ps1` and `golden-sh.sh` in parallel was
995
- > measured to report 2 **false** failures, in which the exit code contradicted the envelope's own
996
- > `error.code`; run serially, both disappear. Treat this as a defect, not as flakiness: that staging
997
- > path is racy by construction.
998
-
999
- ---
1000
-
1001
- ## Contributing
1002
-
1003
- `src/host/lib/powershell5.1/*`, `src/host/lib/powershell7.6/*` and `src/host/lib/*.sh` are the source.
1004
- **Never hand-edit `src/host/bin/dsh-os.ps1`, `dsh-os7.ps1` or `.sh`** — they are generated. Edit the
1005
- modules, then:
1006
-
1007
- ```console
1008
- powershell -File tools/build.ps1 # regenerate all three scripts
1009
- powershell -File tools/build.ps1 -Check # fails as soon as a committed script drifts
1010
- ```
1011
-
1012
- There are three separate artefacts rather than "one script with a runtime branch" because the two
1013
- runtimes differ in ways a branch cannot paper over cleanly: .NET Framework has no
1014
- `ProcessStartInfo.ArgumentList`, and .NET 10 has no reason to keep carrying the hand-rolled Windows
1015
- command-line escaping this project needed in order to work around its absence.
1016
-
1017
- `git.ps1` / `git.sh` are **not** in `tools/build.ps1`'s module lists, so they are never concatenated
1018
- into any artefact; they are separate, self-contained interactive tools — see
1019
- [Standalone tools](#standalone-tools).
1020
-
1021
- `.gitattributes` is part of the contract rather than a preference: `command.txt` and the `.tsv` tables
1022
- must be LF so that both implementations parse byte-identical input; `dsh-os.bat` must be CRLF and
1023
- ASCII-only, or `cmd.exe` mis-parses it; and `dsh-os.ps1` must be CRLF and UTF-8 **with** a BOM, or
1024
- PowerShell 5.1 reads its non-ASCII literals in the ANSI code page.
1025
-
1026
- Two lessons learned the hard way in this repository, worth inheriting:
1027
-
1028
- * **A security boundary derived from a path must fail closed.** A refactor once made the repository
1029
- root one level too high, so no protected path matched. The failure mode was not an error but
1030
- **protection silently ceasing to apply** — the protected files really were deleted.
1031
- * **Temporary files live under `.temporary/`**, golden-test fixtures included.
1032
-
1033
- Module headers still refer to a `tools/split-once.ps1` that no longer exists; the current assembler is
1034
- `tools/build.ps1`.
1035
-
1036
- ---
1037
-
1038
- ## Standalone tools
1039
-
1040
- `src/host/lib/git.sh`, `src/host/lib/powershell5.1/git.ps1` and `src/host/lib/powershell7.6/git.ps1`
1041
- are interactive git runners. They are deliberately **not** part of the verb surface: they are absent
1042
- from `$modulesPs1` and `$modulesSh` in `tools/build.ps1`, so they are never concatenated into
1043
- `bin/dsh-os.ps1`, `bin/dsh-os7.ps1` or `bin/dsh-os.sh`, and they emit no JSON envelope. Each is a
1044
- single self-contained file, byte-independent of the others, kept in step by review rather than by a
1045
- generator.
1046
-
1047
- They locate git in three phases: `GIT_EXE`/`GIT_HOME`/`PATH`, then the known install locations
1048
- (including the Git-for-Windows registry keys), then a **depth- and time-bounded** walk of every
1049
- mounted file system. When nothing is found they list what was searched and exit 127. Arguments reach
1050
- git as separate argv elements; no shell string is ever assembled. `git add` with an empty answer
1051
- (directly pressing Enter) is equivalent to `git add .`. Each of the three obeys its own runtime's
1052
- encoding contract — which is precisely why `git.sh` is plain ASCII (English prompts) and the two
1053
- `git.ps1` files are not.
1054
-
1055
- ---
1056
-
1057
- ## Licence
1058
-
1059
- **MIT**, declared in `package.json` (`"license": "MIT"`, author Eloy Kim) and distributed with the
1060
- repository as a separate [`LICENSE`](LICENSE) file. Fully open source: the whole verb surface, the
1061
- specification documents and the test suites are all in the repository. No telemetry, no account, no
1062
- network access at run time.
1063
-
1064
- ```
1065
- MIT License
1066
-
1067
- Copyright (c) 2026 Eloy Kim
1068
- ```
1069
-
1070
- The licence file is pure ASCII, LF-terminated and BOM-free, consistent with the rest of the
1071
- repository's encoding contracts. An earlier description here claimed it was **not** distributed because it
1072
- is not listed in `package.json`'s `files` whitelist. That was wrong, and the published tarball proves it:
1073
- `npm pack deepseek-coordination@0.1.5` contains `package/LICENSE` **and** `package/README.zh-CN.md`, because
1074
- npm always includes those two names regardless of `files`. The whitelist has 20 entries and includes
1075
- `src/host/bin/dsh-os7.ps1`; the two files simply do not need to be in it.
1076
-
1077
- The verb contract and the specification documents are part of the source: a behaviour change should be
1078
- reflected in `src/host/spec/verbs.tsv`, `src/host/command.txt` and `src/host/docs/os-spec.md`, not only
1079
- in code.
1080
-
1081
- One further note: the specification documents (`src/host/docs/os-spec.md`,
1082
- `src/host/docs/permission-model.md`, `src/host/command.txt`) are written in **Chinese**, while this
1083
- README and the verb table are in English.
1
+ # deepseek-coordination
2
+
3
+ **English** · [简体中文](README.zh-CN.md) · [Changelog](CHANGELOG.md) · [中文更新日志](CHANGELOG.zh-CN.md)
4
+
5
+ A dual-face Cordis plugin for DeepSeek Harness: the host half owns a coordination SQLite database, seven model-facing tools and an HTTP surface; the web half mounts five composer dropdowns, a settings catalog and the service-level takeover dialog.
6
+
7
+ **Status: v0.10.0 (2026-10-18).** This release follows the v0.9.2 tooltip fix and lands the settings catalog, the durable file layer, the turn pipeline, phase freezes and the data-structure adoption pass. The package version, changelog and release tarball are the same tree; claims below were checked against source, not against the design documents alone.
8
+
9
+ | Status | Meaning in this README |
10
+ |---|---|
11
+ | **Wired** | Registered by `src/index.ts` or called by the composed service, routes or tools. |
12
+ | **Library-only** | Implemented and tested as a module, but not driven by the current turn lifecycle or exposed through a route/tool. |
13
+ | **Design/planned** | Documented or declared in config/schema, with no runtime consumer yet. |
14
+
15
+ ## What it is
16
+
17
+ | Piece | Location | Role |
18
+ |---|---|---|
19
+ | Bundle | `cordis.patch.yml`, `dsh.bundle` in `package.json` | Inserts one `deepseek-coordination` row into a DSH profile roster. |
20
+ | Host half | `lib/index.mjs` (ESM, Node) | `CoordinationService`, the `coordination` SQLite database, 7 tools, 14 route paths and the agent hooks. |
21
+ | Client half | `lib/client.js` (CJS, browser, `__ModuleLoader__` wrapper) | React 18 + Material UI 9: composer dropdowns, settings page, takeover dialog. |
22
+ | Database | `%DSH_HOME%\coordination\coordination.db` by default | Plugin-owned SQLite file; 63 tables, 7 triggers, 63 indexes, schema version 24. Never DSH's `harness` database. |
23
+
24
+ The plugin does not replace DSH's agent loop. It records coordination state, locks one profession per task, exposes the composer selection and file metadata, asks DSH to compact when context pressure crosses a threshold, and relays approvals/questions to a human through its own dialog.
25
+
26
+ The five composer controls are **权限 (permission), 职能 (profession), 交互 (interaction), 情景 (scene), 验证 (validation)**. The command face (`/goal`, `/plan`, `/feedback`, `/compact`, `/permission`) is defined as a message prefix, not a sixth dropdown.
27
+
28
+ ## Install and build
29
+
30
+ | Requirement | Detail |
31
+ |---|---|
32
+ | Node.js | `package.json` declares `>=18`, but the host imports `node:sqlite`; use a Node build that ships it. Verified on Node 24.21.0. |
33
+ | npm | npm 11 (bundled with Node 24) installs from `package-lock.json` and drives the scripts and the release workflow. |
34
+ | DSH | A DSH installation with the `dsh` CLI and a web-capable profile (`@deepseek-ai/dsh-base` + `@deepseek-ai/dsh-web-app`). |
35
+ | Peer packages | See the table below; they come from the DSH installation, not from this package. |
36
+
37
+ ```powershell
38
+ cd deepseek-coordination
39
+ npm ci # or `npm install` in a fresh checkout
40
+ npm run typecheck # tsc -p tsconfig.json --noEmit
41
+ npm test # tsdown test build + node --test
42
+ npm run build # lib/index.mjs, lib/index.d.mts, lib/client.js
43
+ npm pack # prepack builds first; produces the installable tarball
44
+ ```
45
+
46
+ `npm run test:tsc` type-checks the test tree. `npm run pack:check` validates the packed runtime files and syntax-checks the host entry. `package-lock.json` is committed; `npm ci` reproduces it exactly.
47
+
48
+ | Peer package | Range | Side |
49
+ |---|---|---|
50
+ | `@deepseek-ai/cordis` | `^4.0.1` | Host runtime |
51
+ | `@deepseek-ai/dsh-host-webserver` | `^0.1.0-rc.7 \|\| ^0.2.0-rc.0` | Host route registration |
52
+ | `@deepseek-ai/dsh-client-runtime`, `-locale`, `-ui-slots`, `-ui-settings` | `^0.1.0-rc.7 \|\| ^0.2.0-rc.0` | Browser module table |
53
+ | `react` | `^18.2.0` | Browser module table; never bundled |
54
+
55
+ ### Install into a profile
56
+
57
+ Install the packed tarball, not the GitHub source:
58
+
59
+ ```powershell
60
+ npm pack
61
+ dsh plugin --profile <name> add .\deepseek-coordination-0.10.0.tgz
62
+ dsh --profile <name> --dump-config # expect a deepseek-coordination layer
63
+ ```
64
+
65
+ **Raw `github:eloyhere/deepseek-coordination` installs are unsupported.** `lib/` is gitignored, and the bundle is only built by `prepack` (or an explicit `npm run build`), which npm does not run for a git dependency; the installed package then fails at activation with `ERR_MODULE_NOT_FOUND` for `lib/index.mjs`. The tag-triggered GitHub Actions workflow (`.github/workflows/release.yml`) builds and attaches the verified tarball to the release; install that artifact.
66
+
67
+ **npm latest is still the old `0.1.9` dsh-os line.** Do not install by package name yet. A local checkout can be linked with `dsh plugin --profile <name> add .` after `npm run build`.
68
+
69
+ `dsh plugin add` writes the dependency and the bundle row. If the profile still records a different package spec, replace it rather than assuming the new code loaded; check `dsh --profile <name> --dump-config` and the installed `package.json` version.
70
+
71
+ | Artifact | Format | Size in this build |
72
+ |---|---|---|
73
+ | `lib/index.mjs` | Host ESM, `platform: node` | 940,726 B |
74
+ | `lib/index.d.mts` | Host declarations | 9,095 B |
75
+ | `lib/client.js` | Browser CJS wrapped in `__ModuleLoader__.load()` | 1,170,887 B |
76
+
77
+ Material UI, emotion and `react-dom` are bundled into `lib/client.js`; React itself comes from the host module table. The inlined `react-dom` is the 18.3.1 line that matches the host's React 18 — npm refuses to auto-install a React 19 `react-dom` against a React 18 `react`. The client bundle is large because MUI and `react-dom` are inlined, not because it ships a second React.
78
+
79
+ ## Architecture
80
+
81
+ Host half (`src/index.ts`):
82
+
83
+ - exports `name`, `inject = ['webServer', 'tools']` and `Config`; `apply()` resolves config, creates the service, recovers interrupted turns and installs every effect through `ctx.effect`.
84
+ - `CoordinationService` (`src/host/coordination.ts`) is the Cordis `coordination` service. It owns the database, module host and the composed modules: session, selection, turn, profession, plan, goal, compact, directive, permission, takeover/gate, provider, cache and command.
85
+ - SQLite is opened with `node:sqlite` (`DatabaseSync`), foreign keys, a 5 s busy timeout and WAL outside `:memory:`. Triggers refuse changing a locked profession and make `orchestration_trace` append-only.
86
+ - The `/deepseek-coordination` prefix route answers JSON `{ ok, value | error }`; request bodies are capped at 1 MiB and unknown paths return 404 with the route envelope.
87
+ - Seven tools are registered through `ctx.tools`; the three composer tools are re-registered after `POST /locale`, because their descriptions are part of the model-facing prompt.
88
+ - Hooks: `agent/pre-step` opens the coordination turn and runs the compaction governor; `agent/turn-stopping` closes it as `done`; `agent/error` closes it as failed; `approval/request` and `user-questions/request` feed the takeover gate. A 60 s timer deletes expired lock rows.
89
+
90
+ Client half (`src/client/index.ts`):
91
+
92
+ - `inject = ['slots', 'locale']`; `package.json`'s `dsh.client.inject` separately lists the four module-table packages.
93
+ - `settings.section` (order 50) mounts `CoordinationSettings`; `conversation.input.left` (order 40) mounts the composer controls and the takeover dialog as siblings.
94
+ - Language follows `ctx.locale.getLocale().active`, normalises `zh*`/`en*` onto `zh-CN`/`en-US`, and reports changes to `POST /locale`. The host rebuilds localized tool descriptions from that report.
95
+ - The client bridge uses same-origin `fetch` with a 3 s selection timeout and 2 s takeover/locale timeouts. Unreachable hosts fall back to a per-session `localStorage` copy for the five choices; a permission preset is never written offline.
96
+ - The composer components are memoised on `sessionId` only, because the slot also passes the composer input on every keystroke; re-rendering this row would make emotion touch `<head>` and close DSH's popover.
97
+
98
+ ```text
99
+ deepseek-coordination/
100
+ ├─ src/
101
+ │ ├─ index.ts # host composition root: service, routes, tools, hooks
102
+ │ ├─ host/
103
+ │ │ ├─ coordination.ts # CoordinationService (Cordis service "coordination")
104
+ │ │ ├─ config.ts # Schemastery schema + resolveConfig()
105
+ │ │ ├─ database.ts # SQLite schema (24), 63 tables, triggers
106
+ │ │ ├─ dsh.ts # narrow DSH ports + turn lifecycle
107
+ │ │ ├─ tools.ts # 7 model-facing tools
108
+ │ │ ├─ chain.ts # bounded Chain projection engine
109
+ │ │ ├─ node-registry.ts # NodeCard registry and describe()
110
+ │ │ ├─ parse-service.ts # workspace-bound parse + read service
111
+ │ │ ├─ token.ts # TokenGovernor and pure policy
112
+ │ │ ├─ takeover-gate.ts # approval/question waterfall gate
113
+ │ │ ├─ selection.ts turn.ts profession.ts … # selection, lock, lifecycle
114
+ │ │ ├─ bayesian.ts comprehension.ts organization.ts … # library engines
115
+ │ │ └─ text/ # P0 parsers: JS/TS, CSS/HTML/MD, TeX, formulas, graph
116
+ │ ├─ client/
117
+ │ │ ├─ index.ts # slots, locale store, nav icon, takeover CSS
118
+ │ │ ├─ controls.ts # five composer dropdowns, tooltips, offline replay
119
+ │ │ ├─ settings.ts # 13-group, 151-control settings catalog
120
+ │ │ ├─ takeover.ts # approval/question dialog
121
+ │ │ ├─ bridge.ts store.ts i18n.ts theme.ts selection-cache.ts
122
+ │ └─ shared/ # entity/enum/selection/i18n/error + data structures
123
+ ├─ test/ # 24 *.test.ts + helpers.ts
124
+ ├─ lib/ # build output; gitignored, never edited by hand
125
+ ├─ cordis.patch.yml # bundle row inserted into a profile
126
+ ├─ scripts/check-pack.mjs # tarball runtime-file verification
127
+ └─ .github/workflows/release.yml
128
+ ```
129
+
130
+ ## Composer controls
131
+
132
+ | Control | Options | What it does |
133
+ |---|---|---|
134
+ | 权限 Permission | DSH preset catalog, or a neutral “follow DSH” state | Writes the chosen preset to DSH immediately through `ctx.permissionPresets`; never persists it in the coordination DB. Only shown while the local 权限接管 switch is on. |
135
+ | 职能 Profession | decision / outcome / resource / personnel / qualification / execution | Locked for the active turn. Selecting another value is refused with `PROFESSION_LOCKED` and opens the rebuild/abandon dialog. |
136
+ | 交互 Interaction | emotion / think / heuristic / converge / orchestrate | Stored for the next turn; frozen while the active turn has a non-cleared goal. |
137
+ | 情景 Scene | dialogue / writing / office / research / engineering / iterate | Stored for the next turn. `writing` sits between `dialogue` and `office`; the scene policy is read by the turn pipeline. |
138
+ | 验证 Validation | low / medium / high | Stored for the next turn; frozen while the active turn has a draft/scored/active plan. All three levels search. |
139
+
140
+ - Every control and every option has a tooltip. A single-owner `tooltipVisibility` store ensures only one composer tooltip is visible; placement is left/right, exit is a 160 ms fade, and the fade popper is pointer-transparent.
141
+ - Clicking a dropdown opens its menu on the first click; there is no state change on pointerdown/mousedown that would swallow the Select click.
142
+ - A locked profession trigger is disabled and carries an accessible name with the lock explanation; option `MenuItem`s carry the label plus hint for keyboard and screen-reader users.
143
+ - Phase freezes are re-derived from the host on every poll: turn → profession, plan → validation, goal → interaction. The select is disabled but stays focusable, its aria-label and tooltip name when the freeze lifts, and the server refuses a changed value with `INVALID_STATE` (replaying the stored value is allowed).
144
+ - Tooltips obey the host's `ui.showTooltip` gate; the local “show hints” preference can only narrow them, never resurrect a host-disabled tooltip.
145
+ - Saves are optimistic and debounced by 120 ms. A rejected save reverts to the last host-confirmed state; offline edits are replayed when the host returns. A full-access preset needs an explicit second confirmation.
146
+ - While the host state is loading, selects stay disabled and show a loading line. A missing preset catalog is shown as a missing catalog, not invented.
147
+
148
+ Validation budgets are policy, not a claim that the whole pipeline runs today:
149
+
150
+ | Level | Queries × results | Evidence scope | Interaction scope |
151
+ |---|---|---|---|
152
+ | low | 1 × 3 | Current turn only | Selected interaction |
153
+ | medium | 2 × 5 | Current session | Selected interaction |
154
+ | high | 4 × 8 | All local content | All interactions |
155
+
156
+ The budgets live in `src/host/comprehension.ts` and the shared `ValidationBudgetByLevel` table. `ComprehensionModule` is library-only and is not driven by the current `CoordinationService` turn lifecycle, so the composer persists the level without executing these searches yet.
157
+
158
+ ### Scenes and the writing profile
159
+
160
+ | Scene | Thinking / effort | Plan | Outline | Revision | Score | Bash | Web | Max iterations |
161
+ |---|---|---|---|---|---|---|---|---|
162
+ | dialogue | disabled / medium | – | – | – | – | no | yes | 1 |
163
+ | writing | enabled / high | yes | yes | yes | yes | no | yes | 3 |
164
+ | office | disabled / medium | yes | – | – | – | no | yes | 1 |
165
+ | research | enabled / high | yes | – | – | – | no | yes | 3 |
166
+ | engineering | enabled / high | yes | – | – | – | yes | yes | 3 |
167
+ | iterate | enabled / high | – | – | – | yes | no | yes | 3 |
168
+
169
+ `writing` requires an outline and at least one revision round, requires a quality score, disallows bash, allows web search and caps the loop at three iterations. The pass threshold is `0.6`; `plan.ts` uses that writing threshold. The turn pipeline reads reasoning effort, the require-* flags, citation/evidence, `allowWeb` and `sandboxOnly`; `thinking`, `allowBash`, `maxIteration` and non-writing thresholds remain deferred diagnostics because no DSH port or consumer exists yet.
170
+
171
+ ## Profession lock and turn lifecycle
172
+
173
+ - One task has exactly one profession. `turn.create` writes `profession NOT NULL`, `locked = 1` and one profession record plus one `lock` trace event.
174
+ - While a non-terminal turn exists, `SelectionModule.set` refuses a different profession with `ProfessionLockedError`; the HTTP layer answers 409 `PROFESSION_LOCKED`. The selection is not mutated.
175
+ - `POST /profession/rebuild` ends the active task as `abandoned`, creates a replacement turn with a new ordinal and profession, copies the prompt/command, and emits `rebuild` + `lock` trace events. It requires an active turn.
176
+ - `POST /profession/abandon` closes the active task without a replacement; the next task may choose again. Both reselect flags are forced true by `resolveConfig`.
177
+ - SQLite repeats the rule: `trg_turn_profession_lock`, `trg_profession_record_lock`, `trg_subtask_profession_lock` abort a profession change while locked; `trg_turn_profession_unlock` and `trg_subtask_profession_unlock` refuse clearing the flag.
178
+
179
+ | Hook | Effect |
180
+ |---|---|
181
+ | `agent/pre-step` | Ensure the coordination session, open (or reuse) one Turn for the DSH turn; also runs the token governor before `next()`. |
182
+ | `agent/turn-stopping` | Close the Turn as `done`; the profession unlocks for the next turn. |
183
+ | `agent/error` | Close the Turn as `failed`. |
184
+ | Host start | `recoverInterrupted()` marks every non-terminal Turn failed with reason `turn was interrupted by a host restart`. |
185
+ | Every 60 s | Delete ended locks older than `defaultLease`. |
186
+
187
+ Terminal turn states (`done`, `failed`, `cancelled`, `abandoned`) are irreversible. The turn hook records turn metadata and an empty prompt row; it does not mirror the DSH conversation into the coordination database.
188
+
189
+ ## Settings and configuration
190
+
191
+ The settings page is driven by `SETTINGS_CONTROLS` in `src/shared/selection.ts`: **151 controls in 13 groups, 134 writable host rows, covering 155 non-secret config leaves**. The 18 top-level config keys include `database` and `databasePath`, which are deliberately not projected or writable.
192
+
193
+ | Group | Defaults / scope | Write path |
194
+ |---|---|---|
195
+ | `session` | `maxTurnPerSession=1000`, `defaultLease=30000 ms`, `clarifyTimeout=30000 ms` | Host rows through `/config` |
196
+ | `profession` | enabled/lock/rebuild/abandon forced `true`; default mirrors `defaults.profession`; audit/errorHint `true` | Forced flags read-only; audit/errorHint host rows (audit has no consumer) |
197
+ | `plan` | threshold `0.5`, maxRebuild `3`, 8 weights summing to 1, 8 dimension thresholds | Host rows; weights re-normalise on save |
198
+ | `orchestration` | maxReject `2`, maxSubTask `32`, groundTruth `true`, timeout `5000 ms`, shell ground truth `false`, modelRouting `true` | Host rows; `groundTruthTimeout` has no consumer |
199
+ | `cache` | enabled `true`, ttl `60000 ms`, fingerprintWindow `50`, effortWarning `true` | Host rows; `fingerprintWindow` has no consumer |
200
+ | `bayesian` | prior `0.5`, stopEntropy `0.05`, infoGain `0.01`, maxRoutes `12`, calibrationFloor `0.1` | Host rows; `calibrationFloor` has no consumer |
201
+ | `comprehension` | stageTimeout `30000 ms`, searchEnabled `true`, maxEvidence `8`, highHistoryWindow `50` | Host rows |
202
+ | `compaction` | auto `true`, soft `0.70`, hard `0.88`, reserve `2048`, recentTurns `6`, cooldown `30000 ms`, maxPerSession `20` | auto/soft/hard host rows; structural fields read-only |
203
+ | `ui` | toolbar `true` (deprecated alias), showTooltip `true`, takeover `false`, refreshInterval `5000 ms`, compact `true`, locale `auto` | Host rows plus the two local switches; toolbar/locale read-only |
204
+ | `scenes` | six scene profiles, 13 fields each; writing threshold `0.6` | Host rows; only the writing score threshold is read by `plan.ts` |
205
+ | `parser` | enabled `true`, 1 MiB, 200k tokens, depth `256`, 2 s, cache `256`, graph 20k/100k | enabled + four limits host rows; depth and graph caps planned |
206
+ | `chain` | depth `8`, links `50`, tokenBudget `1500` | Host rows |
207
+ | `tools` | maxResultBytes `0` | Planned/read-only; not consumed |
208
+ | `defaults` | profession `execution`, interaction `think`, scene `dialogue`, validation `medium`, command `none` | Host rows |
209
+
210
+ `GET /deepseek-coordination/config` returns `{ values, defaults }`; `defaults` is `resolveConfig(undefined)`, so the page never duplicates a default literal. `POST /config` accepts a flat dotted patch such as `{ "plan.threshold": 0.7 }`, validates it against the catalog and the real Schemastery schema, applies it to the live config, re-resolves invariants (weight normalisation, alias mirror, forced flags, compaction clamps), rebuilds `TokenGovernor` when compaction changes, and best-effort persists through `ctx.settings.update`.
211
+
212
+ Persistence is explicit: `persisted: true` only when DSH accepted the settings write; otherwise the change is live for this process and the page says so. Unknown keys, `databasePath` and `database` are rejected with `INVALID_ARGUMENT`. Known but non-editable keys are reported in `ignored` and never written. `POST /config/compaction` remains as a small compatibility route for the old auto/soft/hard patch.
213
+
214
+ The page is Material UI end to end: one default-expanded `Accordion` per group, a `List` of rows, six scene tables (no scene dropdown), `Checkbox` for booleans, `RadioGroup` for enums, `Slider` for ratio-like numbers, `TextField type="number"` for counts/bytes/milliseconds, and `Chip` only for the `主机 / 本机 / 强制 / 只读 / 计划中` status plus connection state. There is no save button; controls apply on change/blur. A forced row draws a disabled control, a read-only row shows a value, and a planned row says the runtime does not read it yet. The settings nav icon is a host-shell workaround: the shell accepts only `{ id, order, label }` for a section, so the plugin decorates its nav button with a CSS mask of the Material `sync` glyph.
215
+
216
+ ## Service-level takeover
217
+
218
+ - With `ui.takeover` on, `TakeoverGate` listens to DSH's `approval/request` and `user-questions/request` waterfalls. It records a durable pending row, emits `takeover/pending`, and waits for a human answer delivered through the client routes.
219
+ - A timeout, an aborted request signal, an explicit cancel, or plugin unload calls `next()`; the gate never turns “nobody answered” into `allowed-once` or a fabricated answer. `unavailable` is the host's fail-closed outcome and is rejected as a user decision.
220
+ - The dialog polls `/takeover/pending` every 1 s only while the local 权限接管 switch is on, the session id is real and the document is visible. The first pending request is rendered; a `1 / N` badge appears for a queue.
221
+ - Approval offers 仅本次允许 / 拒绝 / 取消; only `allowed-once` and `rejected` are submitted, while cancel/Esc/backdrop use the cancel route. Questions render radios for single-select, checkboxes for multi-select and optional free text; submit stays disabled until every question is answered.
222
+ - **Both switches are needed**: host `ui.takeover` (default `false`) opens the gate; the local 权限接管 switch makes the dialog poll. With the host switch off, the gate delegates immediately and there is nothing to show.
223
+
224
+ ## Auto-compaction and the token governor
225
+
226
+ `TokenGovernor` estimates context (CJK-aware O(n) estimator with a bounded 256-entry memo), plans a budget against `contextWindow - reserve`, and asks DSH's `compactIfNeeded` at `agent/pre-step` when utilisation crosses a threshold. Defaults: soft `0.70`, hard `0.88`, reserve `max(2048, 8% of window)` capped at 25%, recentTurns `6`, cooldown `30 s`, `maxPerSession 20`.
227
+
228
+ The soft threshold is editable as a 0.30–0.90 ratio; the hard threshold as 0.30–0.98 and strictly above soft. The old 60/70/80/88% values remain valid choices (`0.60`, `0.70`, `0.80`, `0.88`) even though the catalog now uses a slider rather than a four-item picker. Auto compaction is disabled by `compaction.auto=false`, but an explicit compact command is not governed by that switch.
229
+
230
+ Fail-safe rules are deliberate: the hook never blocks the step; a missing compaction port, `null`/`undefined` result, timeout, throw, spent per-session allowance or unelapsed cooldown all become a no-op, and the agent proceeds to `next()`. The governor snaps the policy at construction, so a live settings change also rebuilds it. DSH owns the actual safe range and replacement; this plugin only decides when it is worth asking.
231
+
232
+ ## File tools, parser and Chain
233
+
234
+ The file layer is wired as four model-facing tools, alongside the three composer tools:
235
+
236
+ | Tool | Purpose |
237
+ |---|---|
238
+ | `coordination_parse` | Parse a workspace file or supplied text. Exactly one of `path`/`text`; returns references, diagnostics, charset, truncation and node cards, never the file body. |
239
+ | `coordination_describe` | Read one `NodeCard` and optionally `summary`/`description`/`usage`/`purpose` by handle; never the body. |
240
+ | `coordination_chain` | Query one bounded relation chain: dependencies, dependents, satisfies, satisfied_by, baseline, prerequisites, unmet, path or cycle. Depth ≤ 8, links ≤ 50, ~1,500 tokens; never an AST or graph object. |
241
+ | `coordination_read` | The only tool that returns file content. Reads a byte range (end exclusive) or a line range (1-based inclusive), capped at 256 KiB / 2,000 lines / 16k tokens per call. |
242
+
243
+ P0 parsers are lexical and deterministic: JS/TS/JSX (imports, exports, `require`, dynamic import, symbols, comments), CSS/HTML/Markdown (`@import`/`url()`, script/link/img, links/images/headings/front matter/fenced divs/formulas), TeX (theorem-like environments, proofs, labels/refs, math, `\input`/`\include`), generic formulas (Markdown/TeX/spreadsheet/MathML/OMML/unknown; no evaluation), and a lexical dependency graph (external filtering, topological order, cycles, levels, isolated vertices). Malformed input produces diagnostics and `truncated`, not a crash.
244
+
245
+ Every parsed node is **blueprint-only**: `path`, UTF-8 `byteStart`/`byteEnd`, 1-based `lineStart`/`lineEnd`, `sha256:<hex>` fingerprint and `charset`; no `text`, `body`, `content` or AST. The same holds for `NodeCard` and `ChainLink`. `coordination_read` is the single content path; it rejects absolute paths, UNC paths, traversal and symlink escapes, refuses NUL/binary/undecodable content, and hashes what it returns.
246
+
247
+ | Limit | Value |
248
+ |---|---|
249
+ | Source text per parse | 1 MiB; prefix parsed and marked `truncated` |
250
+ | Tokens per parse | 200,000 |
251
+ | AST depth | 256 |
252
+ | Parse wall clock | 2,000 ms |
253
+ | Diagnostics retained | 100 |
254
+ | Node metadata per parse | 20,000 |
255
+ | Dependency graph | 20,000 vertices / 100,000 edges, fail-closed |
256
+ | Formulas | 5,000 per scan / 512 references |
257
+ | Parse cache | 256 files, LRU keyed `path+mtime+size+parserId` |
258
+ | Read range | 256 KiB / 2,000 lines / 16,000 estimated tokens |
259
+ | Chain projection | depth ≤ 8 / links ≤ 50 / ~1,500 tokens; 50k nodes / 200k edges fail-closed |
260
+
261
+ `parser.enabled` is read when the file tools are registered; parser limits and `chain.depth`/`links`/`tokenBudget` are captured when the lazy tool runtime creates its `ParseService`/`ChainEngine`. A live change through `/config` therefore does not retune an already-created runtime; it takes effect on the next registration (today a locale change or restart). The workspace root falls back to `process.cwd()` because the plugin entry does not receive DSH's workspace root; a host whose cwd is not the workspace must pass a root through tool options.
262
+
263
+ **No write or refresh tool exists.** The v24 write contract needs `expectedFingerprint` conflict detection, transactional reparse and range recomputation, explicit charset/BOM changes and a watcher/rename policy; none is implemented, so a writer would risk overwriting concurrent edits or returning stale ranges. `coordination_write` and `coordination_refresh` are design/planned.
264
+
265
+ ## Data structures
266
+
267
+ The shared library in `src/shared/` is pure ES2022: hash/LRU, nodes, binary/B+ and red-black trees, heaps, stack/deque/queue, ordered maps/sets/skip lists, AST/token stream, directed graph, trie and sparse grid. The current runtime uses:
268
+
269
+ - `LruList` for the token-estimator memo, parse cache, node registry and chain projection cache.
270
+ - `BinaryHeap` for `selectContext`, preserving the old total order (value desc → created asc → id byte-asc → input index).
271
+ - `RedBlackTree` predecessor/successor for numeric agreement; the exact argmax proof is in the adoption document.
272
+ - `OrderedSet` for first-seen dedupe in comprehension/emotion/permission; `Queue` for the permission-command token stream.
273
+ - `DirectedGraph` for the dependency graph and one graph per chain relation.
274
+
275
+ The second adoption pass is measured and randomized-tested. Its honest caveat: on the 20k-chunk **wide** synthetic case the heap is not faster than V8 sort (17.7 ms vs 15.9 ms); it wins when the budget fills early and is never worse in result. `SparseGrid` and `BPlusTree` are regression-tested in the shared library but **not wired into a runtime path**; do not infer they are used.
276
+
277
+ ## Tools and routes
278
+
279
+ | Tool | What it returns |
280
+ |---|---|
281
+ | `coordination_selection` | Read or patch the five selections for a session (including `command` and DSH `preset`); returns `SelectionState` with lock, presets and config projection. |
282
+ | `coordination_profession` | Read the current profession record; set a profession only before/without an active task. A locked task returns `PROFESSION_LOCKED` and leaves the selection unchanged. |
283
+ | `coordination_professionlock` | Read the lock state/profession of the active turn, or of a given turn ordinal. |
284
+ | `coordination_parse` | Body-free parse projection (references, diagnostics, charset, truncation, cards). |
285
+ | `coordination_describe` | Body-free `NodeCard` and requested metadata fields. |
286
+ | `coordination_chain` | Bounded `ChainProjection` only. |
287
+ | `coordination_read` | The only content-returning tool; capped byte/line range. |
288
+
289
+ All routes live under `/deepseek-coordination`; there are **14 paths / 16 method handlers**:
290
+
291
+ | Method | Path | Purpose |
292
+ |---|---|---|
293
+ | GET | `/ping` | Liveness; returns plugin name, `pong`, current locale. |
294
+ | POST | `/echo` | Echo a JSON body (diagnostic). |
295
+ | GET, POST | `/selection` | Read `SelectionState`; patch the five choices. |
296
+ | GET | `/presets` | DSH permission-preset catalog projected for the dropdown. |
297
+ | POST | `/permission` | Apply one DSH preset immediately. |
298
+ | GET, POST | `/config` | Read `{ values, defaults }`; apply a flat config patch. |
299
+ | POST | `/config/compaction` | Compatibility patch for `auto`/`soft`/`hard`. |
300
+ | GET | `/takeover/pending` | Pending approval/question windows for a session. |
301
+ | POST | `/takeover/answer` | Deliver a validated human answer; invalid answers keep the window open. |
302
+ | POST | `/takeover/cancel` | Cancel a window and delegate to the host default. |
303
+ | POST | `/profession/rebuild` | End the active task and create a replacement with a new profession. |
304
+ | POST | `/profession/abandon` | End the active task without a replacement. |
305
+ | POST | `/locale` | Report the client language; re-registers localized tool descriptions. |
306
+ | GET | `/state` | Selection plus coordination metrics (`session`, `turn`, `plan`, `subtask`, `trace`, `takeover`, `schema`). |
307
+
308
+ There is no `coordination_write`, `coordination_refresh`, `outline`, `findSymbol`, `searchSummary` or `commentsOf` tool. The four file tools carry inline EN/zh-CN copy because the shared `ToolName` table has no keys for them yet; the three composer tools use the shared copy table.
309
+
310
+ ## Data, privacy and safety
311
+
312
+ - The plugin owns an independent SQLite database. It never opens DSH's `harness` database. The default path is `%DSH_HOME%\coordination\coordination.db`; `database`/`databasePath` are deployment-only and are neither projected nor writable from `/config`.
313
+ - Parser nodes, `NodeCard`s, `ChainLink`s and their tool results never carry file bodies, attachments or ASTs. Only `coordination_read` returns text, and only for the requested range.
314
+ - File paths are workspace-relative; absolute paths, drive-rooted paths, UNC paths, `..` escapes and symlink escapes are rejected before a read. Binary/NUL/undecodable content is refused rather than guessed.
315
+ - The turn hook records turn metadata and an empty prompt row; it does not mirror the DSH transcript. No file content is copied into the coordination database.
316
+ - Permission presets are written to DSH immediately (full access needs a second confirmation) and are never stored in the offline localStorage copy. Offline replay covers only the five stored selection fields.
317
+ - Takeover never auto-approves. Timeout, abort, cancel and unload delegate with `next()`; only a human decision delivered through the answer route settles a window.
318
+ - Fail-closed behaviour is explicit: invalid takeover answers leave the window open; `PROFESSION_LOCKED` leaves the selection intact; parser uncertainty is `unverifiable`; graph/parse overflow is `truncated`; compaction failure is a no-op; chain `unknown` never auto-passes.
319
+ - The coordination DB stores task/audit rows (including `prompt` and trace rows), not credentials or provider secrets. The config projection contains no DSH credential, token or provider key.
320
+
321
+ ## Testing, quality and limitations
322
+
323
+ | Check | Result |
324
+ |---|---|
325
+ | `npm test` | **473 tests, 472 pass, 1 skipped (GC-only), 0 fail**, ~3.3 s on Node 24.21.0 |
326
+ | `npm run typecheck` / `npm run test:tsc` | Clean |
327
+ | `npm run build` | Host + client bundles green; MUI `"use client"` directive warnings are non-fatal |
328
+ | `npm run pack:check` | OK: `deepseek-coordination-0.10.0.tgz` (538,358 B); four runtime files present |
329
+ | Release CI | `.github/workflows/release.yml` runs typecheck, test:tsc, tests, pack:check and attaches the tarball to a `v*` GitHub Release |
330
+
331
+ Parser and structure numbers are guards, not promises: a 100k-character parse test stays under 3 s; the data-structure report records B+ tree 10k inserts at 8.3/9.8 ms, skip-list 10k inserts at 6.4 ms, red-black 10k random set/delete at 13.5 ms, sparse grid 10k cells at 5.1 ms, and `hashString` 100k calls at 4.3 ms on the audit machine. The complexity audit records the ordinal `COUNT(*)` → `MAX(ordinal)` change (≈65–69 ms → ≈12–14 ms for 2,000 statements) and route ranking (2,400 → 200 posterior statements for 200 × 12 routes).
332
+
333
+ Known limitations, stated rather than hidden:
334
+
335
+ 1. **Durable graph is bounded.** `node`/`edge`/`snapshot` are now schema 24 and survive a new runtime; rehydration is bounded to the newest 10k nodes / 50k edges, removed symbols are not pruned, and a handle older than the rehydration window is `NOT_FOUND`.
336
+ 2. **Domain extraction is parser-level; the chain projection is partial.** Book/Article/Part/Chapter/Section/Paragraph, Person/Venue, Condition, Sheet/Cell/Range, TeX proof status and CSV/TSV sheets are emitted and projected onto node cards. `partOf`/`condition` edges are not yet queryable through `coordination_chain`; xlsx/docx/pptx, Sign promotion, Lean/Coq/Isabelle status and cross-file Book composition remain deferred.
337
+ 3. **The core turn pipeline is wired, the wider engines are not.** Comprehension → Organization → Plan/score and required Predict rows now run for `command=none` turns, with the scene policy applied where ports exist. Standalone Emotion/Bayesian/Cognition/Orchestration/Learning/Resource/Score, plan rebuild loops and decompose/dispatch/verify/synthesize remain library-only. `thinking`, `allowBash`, `maxIteration` and non-writing score thresholds are recorded as deferred diagnostics, not faked.
338
+ 4. **Configured caps are live.** `parser.maxAstDepth`, both dependency-graph caps and `tools.maxResultBytes` are consumed; parser/chain limits retune an existing runtime, and a config change no longer leaves a stale tool runtime.
339
+ 5. **Workspace root is resolved, not hard-coded.** Tool option → service root → host root → sandbox-policy root → `process.cwd()` only as the last fallback.
340
+ 6. **`parser.enabled` still needs registration.** The other file-layer limits retune live; turning the parser off fails closed immediately, but adding/removing the four tools requires the next registration (restart).
341
+ 7. **No browser E2E test.** The UI is covered by types, build, the settings-catalog tests and the takeover/bridge/integration tests; visual verification remains manual/screenshot-based.
342
+ 8. **Node engine is `>=22.13.0`.** `node:sqlite` is unflagged from 22.13.0/23.4.0; CI and this verification use 24.21.0.
343
+ 9. **A profile can still record an older package spec.** Verify the installed version (`dsh --profile <name> --dump-config` or the profile's `package.json`); install the 0.10.0 tarball or link the built checkout rather than assuming a previous install was upgraded.
344
+
345
+ ## Repository history and license
346
+
347
+ `0.1.x` is **dsh-os**, a different OS-interaction project, no longer on the branch tip; `0.2.0` became the DSH plugin bundle; `0.3`–`0.6` moved the UI from Angular to MDUI to plain React to Material UI; `0.7`–`0.9` added the five dropdowns, i18n, profession lock, takeover, compaction, the P0 parser, the domain model and the shared data structures; `0.10.0` adds the settings catalog, the four file tools, the second data-structure pass and the verified tarball. Earlier states remain in git history and tags.
348
+
349
+ MIT © 2026 Eloy Kim. See [LICENSE](LICENSE). Source: [github.com/eloyhere/deepseek-coordination](https://github.com/eloyhere/deepseek-coordination).