dsh-plugin-wsl-env 0.7.4 → 0.8.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/CONTRIBUTING.md +129 -39
  3. package/README.md +20 -20
  4. package/docs/ARCHITECTURE.md +6 -3
  5. package/docs/LIMITATIONS.md +13 -0
  6. package/docs/README.zh.md +20 -20
  7. package/lib/agent-confined.d.ts +42 -0
  8. package/lib/agent-confined.js +25 -41
  9. package/lib/agent-errors.d.ts +31 -0
  10. package/lib/agent-errors.js +19 -16
  11. package/lib/agent-exec.d.ts +159 -0
  12. package/lib/agent-exec.js +151 -165
  13. package/lib/agent-protocol.d.ts +192 -0
  14. package/lib/agent-protocol.js +76 -100
  15. package/lib/agent-shared.d.ts +27 -0
  16. package/lib/agent-shared.js +12 -12
  17. package/lib/agent.d.ts +353 -0
  18. package/lib/agent.js +536 -552
  19. package/lib/auto-preset.d.ts +110 -0
  20. package/lib/auto-preset.js +157 -141
  21. package/lib/bwrap.d.ts +90 -0
  22. package/lib/bwrap.js +18 -18
  23. package/lib/file-reference-wsl.d.ts +160 -0
  24. package/lib/file-reference-wsl.js +192 -173
  25. package/lib/fs-decisions.d.ts +107 -0
  26. package/lib/fs-decisions.js +64 -60
  27. package/lib/fs-routing.d.ts +182 -0
  28. package/lib/fs-routing.js +173 -116
  29. package/lib/fs-substrate.d.ts +258 -0
  30. package/lib/fs-substrate.js +248 -248
  31. package/lib/fs.d.ts +10 -0
  32. package/lib/fs.js +5 -0
  33. package/lib/fsio-agent.d.ts +307 -0
  34. package/lib/fsio-agent.js +414 -410
  35. package/lib/fsio-text.d.ts +115 -0
  36. package/lib/fsio-text.js +104 -84
  37. package/lib/git-route.d.ts +100 -0
  38. package/lib/git-route.js +46 -47
  39. package/lib/index.d.ts +1003 -0
  40. package/lib/index.js +1101 -997
  41. package/lib/listing.d.ts +114 -0
  42. package/lib/listing.js +68 -55
  43. package/lib/paths.d.ts +182 -0
  44. package/lib/paths.js +136 -105
  45. package/lib/picker.d.ts +266 -0
  46. package/lib/picker.js +371 -351
  47. package/lib/ports.d.ts +44 -0
  48. package/lib/ports.js +39 -29
  49. package/lib/preset-choice.d.ts +33 -0
  50. package/lib/preset-choice.js +15 -7
  51. package/lib/probe-cache.d.ts +40 -0
  52. package/lib/probe-cache.js +35 -29
  53. package/lib/sandbox-core.d.ts +79 -0
  54. package/lib/sandbox-core.js +71 -88
  55. package/lib/sandbox.d.ts +48 -0
  56. package/lib/sandbox.js +4 -4
  57. package/lib/search-exec.d.ts +96 -0
  58. package/lib/search-exec.js +52 -79
  59. package/lib/search-route.d.ts +75 -0
  60. package/lib/search-route.js +30 -31
  61. package/lib/shell-env.d.ts +88 -0
  62. package/lib/shell-env.js +78 -70
  63. package/lib/shell.d.ts +10 -0
  64. package/lib/shell.js +5 -0
  65. package/lib/subprocess.d.ts +313 -0
  66. package/lib/subprocess.js +267 -262
  67. package/lib/terminal-activity.d.ts +98 -0
  68. package/lib/terminal-activity.js +29 -39
  69. package/lib/terminal-route.d.ts +27 -0
  70. package/lib/terminal-route.js +16 -8
  71. package/lib/watcher.d.ts +77 -0
  72. package/lib/watcher.js +120 -126
  73. package/lib/workspace-files-route.d.ts +132 -0
  74. package/lib/workspace-files-route.js +73 -81
  75. package/lib/workspace-files-wsl.d.ts +257 -0
  76. package/lib/workspace-files-wsl.js +311 -251
  77. package/lib/wsl.d.ts +266 -0
  78. package/lib/wsl.js +144 -151
  79. package/package.json +68 -20
package/CHANGELOG.md CHANGED
@@ -12,6 +12,304 @@ reference below points at that record's numbering.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.8.0] - 2026-10-06
16
+
17
+ The toolchain's TypeScript story completes (pnpm, a checkJs gate, and 36 of 36
18
+ modules with their declarations shipped), and with it three behaviour fixes
19
+ land: the byte-window cap is enforced, the agent path's EXEC capture budget is
20
+ honoured, and the filesystem's host-path mapping speaks its synchronous
21
+ contract.
22
+
23
+ One theme: the toolchain moves from npm to pnpm. `pnpm-lock.yaml` replaces
24
+ `package-lock.json`, CI installs through corepack with
25
+ `pnpm install --frozen-lockfile` (the `packageManager` field pins pnpm 11.7.0,
26
+ matching the harness repository), and the three dependencies with install
27
+ scripts (`@deepseek-ai/dsh-subprocess-local`, `koffi`, `node-pty`) are
28
+ allow-listed in `pnpm-workspace.yaml` — pnpm 11 blocks dependency build
29
+ scripts by default. With it: `engines` and the CI matrix align with the harness
30
+ host's own floor (`^22.19.0 || >=24.0.0`), so the Node 20 legs are gone, and
31
+ the per-target lock test drops its unused `withResolvers` gate, a Node 22+ API
32
+ that hung those legs on every push.
33
+
34
+ One more theme: the whole repository comes under a TypeScript check gate.
35
+ `tsc --noEmit` under `strict`/`checkJs` now runs as part of `pnpm test` over
36
+ `lib/**`, `test/**` and the probe scripts, with the types taken from the peer
37
+ packages' own declarations and the service augmentations loaded through
38
+ `types/dsh-services.d.ts`. The gate's first pass surfaced the seam work this
39
+ package already carries: the private `spawnSpec` the WSL spawn directory needs
40
+ (`docs/UPSTREAM-SPAWN-SEAM.md` bridges it) and a synchronous-contract violation
41
+ in `processPathFromHostPath`, pending a design decision.
42
+
43
+ Extending the gate to the tests found real defects the untyped corners had
44
+ been hiding: `distroReadBytes` accepted the byte-window cap (both call sites
45
+ pass it) but neither documented nor enforced it, so a ranged `readBytes`
46
+ asking past the page cap was served instead of refused — upstream refuses a
47
+ window the same way it refuses a page. Fixed against the upstream wording and
48
+ arithmetic (`lib/workspace-files-wsl.js`), with an absent length now
49
+ defaulting to the window cap rather than the whole-file cap, as upstream's
50
+ `resolveWindow` defaults it; the refusal is pinned in
51
+ `test/workspace-files-wsl.test.mjs`. Two hand-written JSDoc contracts wider
52
+ than their code (`gitSpawnRewrite`'s `argv`/`cwd`, `wslErrorCode`'s
53
+ `stdout`/`stderr` — both tolerate absence at runtime) and one narrower than
54
+ its caller (`uncToPosix`'s path) were corrected to what the functions actually
55
+ take. Test doubles are typed as the slice of the seam they script, reaching
56
+ the seam's declared type through one documented cast at the injection point —
57
+ no `any` and no new `@ts-expect-error` anywhere in the suite.
58
+
59
+ And the migration's third step opens: `src/agent-protocol.ts` is the first
60
+ module whose source is TypeScript. `pnpm run build` (`tsconfig.build.json`)
61
+ emits `lib/agent-protocol.js` plus `lib/agent-protocol.d.ts` under the same
62
+ export paths, and the artifacts stay committed so a bare checkout remains
63
+ loadable and the tarball needs no build step; the new `lint:build` gate
64
+ (`test/build-freshness.mjs`) makes `pnpm test` fail the moment a committed
65
+ artifact drifts from its source. Generated files are excluded from the
66
+ `checkJs` gate — their TypeScript sources are type-checked with
67
+ `noEmitOnError` instead, which is strictly stronger. The protocol message
68
+ union now has one home: `lib/agent.js` and the script-driven tests import
69
+ `AgentMessage` and friends from the protocol module's declarations instead of
70
+ restating them.
71
+
72
+ And the declarations land for every module. A second, declaration-only pass
73
+ (`tsconfig.dts.json`, driven by `scripts/build.mjs`) emits `.d.ts` for the
74
+ hand-written, JSDoc-typed JavaScript, and every export subpath now carries its
75
+ `types` condition — downstream finally sees this plugin's types, verified end
76
+ to end (a consumer's wrong call is rejected with the precise signature). The
77
+ declaration pass must run against a clean tree: a stale `.d.ts` is resolved as
78
+ an input exactly where `./x.js` would be, and re-emitting through it silently
79
+ degrades the result — a precise type in `index.d.ts` collapsed to `any` that
80
+ way during the migration — so the build deletes the declarations its first
81
+ pass does not own before re-emitting, and `lint:build` snapshots the
82
+ artifacts, runs the real build, byte-compares and restores the tree.
83
+ `lint:style` gained the invariant that keeps the surface from rotting: a
84
+ JavaScript export without its `types` sibling fails the gate.
85
+
86
+ `lib/paths.js` follows as the second migrated module — the path algebra every
87
+ other module imports, plus `DistroPath`/`WorldPathOptions`, which consumers
88
+ reference through `import("./paths.js").DistroPath`; the TypeScript source
89
+ keeps those names, so nothing downstream changes except that they now come
90
+ from a checked source. CONTRIBUTING records the per-module checklist this
91
+ settled into: write `src/<name>.ts`, add the generated JavaScript to the two
92
+ `exclude` lists, build, and let the suite verify the artifact is
93
+ behaviourally identical.
94
+
95
+ Three leaf modules then follow in one pass — `agent-errors` (the two error
96
+ classes the executor switches on), `probe-cache` (the verdict policy that
97
+ remembers successes and re-probes failures) and `listing` (the picker's pure
98
+ helpers, `boundedInsert`'s generic included). Each was verified export-for-
99
+ export against its predecessor, and the whitespace-normalized code diff is
100
+ tsc's statement reflow and nothing else; every source keeps its module doc
101
+ plus a note naming the generated artifact.
102
+
103
+ The next four carry the decision layer: `fsio-text` (the byte-for-byte
104
+ replication of the peer's text mechanics — `FsCodedError` gained a declared
105
+ `code` field, and the stream decoder is typed through `node:util`, since
106
+ Node's global `TextDecoder` is a value, not a type), `fs-decisions` (the
107
+ refusal descriptors, with `Refusal` as an exported interface and `isCoded` as
108
+ a real type predicate), and `git-route`/`search-route` (the two spawn
109
+ rewrites; their generated artifacts are byte-identical modulo whitespace).
110
+ Nine of the thirty-six modules now have TypeScript sources.
111
+
112
+ Six more follow — `terminal-route`, `preset-choice`, `ports`, `bwrap`,
113
+ `agent-confined` and `agent-shared` — and they cross a structural line: a
114
+ source may now import a module that is not migrated yet. `rootDirs` merges
115
+ `src/` and `lib/` for resolution, so `./agent.js` resolves through the
116
+ generated `lib/agent.d.ts` while the emitted specifier stays `./agent.js`;
117
+ because that merge also pulls declarations in as program inputs, pass 1 now
118
+ emits into a scratch directory the build script copies from, keeping inputs
119
+ and outputs disjoint (TS5055 otherwise). `agent-shared`'s generated artifact
120
+ is byte-identical even after whitespace normalization; the rest differ only
121
+ by tsc's statement reflow. Fifteen of the thirty-six modules are TypeScript.
122
+
123
+ The mid-size tier follows: `wsl` (the `wsl.exe` interop primitives — its four
124
+ option shapes are exported interfaces now), `agent-exec` (the agent-backed
125
+ execution handle, whose request/result/runner/handle typedefs the tests and
126
+ `search-exec` already imported by name) and `shell-env` (the `DSH_*` fact
127
+ contributor, typing the harness registry it contributes through structurally,
128
+ as its JSDoc always did). `agent-exec` surfaced one Node typing fact worth
129
+ knowing: `Buffer.alloc(0)` infers `Buffer<ArrayBuffer>` while an incoming
130
+ `Buffer` is `Buffer<ArrayBufferLike>`, so the accumulators are annotated
131
+ `Buffer`. Eighteen of the thirty-six modules are TypeScript now.
132
+
133
+ Seven small modules follow — `watcher` (the in-distro mtime watch loop and its
134
+ protocol, `ChildProcess` and options typed), `sandbox-core`/`sandbox` (the
135
+ bwrap probe-and-confine pair; `WslConfined`/`WslSandboxCoreInstance` are
136
+ exported interfaces and the two class fields are `declare` so the emitted
137
+ JavaScript stays byte-identical), `terminal-activity`, `search-exec`, and the
138
+ two entry re-exports `shell`/`fs` (whose artifacts are byte-identical). At
139
+ twenty-five of thirty-six, the remaining modules are the large ones: `agent`,
140
+ `fs-substrate`, `fsio-agent`, `picker`, `subprocess`, `auto-preset`,
141
+ `workspace-files-*`, `file-reference-wsl`, `fs-routing`, and finally `index`.
142
+
143
+ The route layer follows — `workspace-files-route` (the pure build/parse pairs
144
+ for the distro-routed file service), `file-reference-wsl` (the `@`-completion
145
+ traversal strategy, with `TraversalDirent`/`FileReferenceTraversal` as
146
+ exported interfaces) and `fs-routing` (the root-plane coordinate router).
147
+ `fs-routing` is where the compiler re-met the documented upstream seam:
148
+ `WslFileSystem.processPathFromHostPath` is async against the `FileSystem`
149
+ interface's synchronous contract, so the router's lazy backend is typed as the
150
+ concrete class rather than casting the mismatch away — the seam keeps its one
151
+ visible `@ts-expect-error` in `index.js`. The port also dropped two dead
152
+ imports `fs-routing` had carried since a refactor (`windowsToLinuxMount`,
153
+ `linuxJoin`), which the emitted artifact no longer contains. The orphaned
154
+ `linuxJoin` doc block above the class — a leftover of that same refactor — is
155
+ gone too. Twenty-eight of the thirty-six modules are TypeScript.
156
+
157
+ `fs-substrate` and `auto-preset` follow — the agent-backed filesystem face
158
+ (`ResolvedTarget` exported as an interface, its six instance fields declared
159
+ with `declare` so the emitted JavaScript stays byte-identical) and the
160
+ preset-adoption plugin (whose harness service slices stay spelled
161
+ structurally, and whose `ensureSession` patch carries the `this: unknown`
162
+ parameter the JSDoc already documented). Thirty of the thirty-six modules
163
+ are TypeScript; the remaining six are `agent`, `fsio-agent`, `picker`,
164
+ `subprocess`, `workspace-files-wsl` and `index`.
165
+
166
+ `picker` follows — the biggest single module migrated so far after the
167
+ protocol file itself (494 lines). Its config, the cached distro listing and
168
+ the per-distro home map are `declare`d so the emitted JavaScript stays
169
+ byte-identical, and the browse capability object stays an inline class field,
170
+ which is how the original script wrote it. Thirty-one of the thirty-six
171
+ modules are TypeScript.
172
+
173
+ `subprocess` follows — the composition-level provider behind the GUI's
174
+ terminal window, and the last module that overrides a shipped peer class for
175
+ the terminal/search routing. Its `spawnTerminal`/`spawn` overrides are checked
176
+ against the peer's declared signatures (they pass without a new assertion),
177
+ and the two JSDoc casts the script carried (`env`, the stdout collect budget)
178
+ are absorbed into the types. Thirty-two of the thirty-six modules are
179
+ TypeScript; the last four are `agent`, `fsio-agent`, `workspace-files-wsl`
180
+ and `index`.
181
+
182
+ `agent` follows — the resident in-distro agent's whole lifecycle (756 lines).
183
+ Its seven internal methods stay `private`, which surfaced the migration's one
184
+ structural seam: a class with private members is nominal, so the declaration
185
+ generated for the still-un-migrated `fsio-agent` and the one this source emits
186
+ are not interchangeable even though the runtime object is the same class. The
187
+ one boundary (`fs-substrate` handing agents to `DistroFs`) crosses it through a
188
+ single documented assertion naming the lib-side type; it disappears when
189
+ `fsio-agent` migrates. Thirty-three of the thirty-six modules are TypeScript.
190
+
191
+ `fsio-agent` follows, and with it that seam closes: the lib-side declaration
192
+ it used to drag in is gone, so the boundary assertion in `fs-substrate` is
193
+ deleted in the same commit — the class is declared once now, and the private
194
+ members it keeps are nominal against nothing else. Thirty-four of the
195
+ thirty-six modules are TypeScript; `workspace-files-wsl` and `index` remain.
196
+
197
+ `workspace-files-wsl` follows — the distro-routed GUI file service, with
198
+ `WorkspaceFilesWslConfig`/`DistroListing` exported, the row-config read kept
199
+ as its one documented double cast (the base class keeps `config` private), and
200
+ the `readByteRange` expect-error left exactly where it was: that one is an
201
+ open design decision, not a migration artifact. Thirty-five of the thirty-six
202
+ modules are TypeScript; `index` is the last one.
203
+
204
+ `index` closes the migration — the integration layer's two providers, 1400
205
+ lines, with both documented upstream seams intact: the private `spawnSpec` a
206
+ `wsl.exe` spawn needs, and the `processPathFromHostPath` contract violation,
207
+ keep their `@ts-expect-error` carrying the reason. The generated artifact is
208
+ proven structurally identical to the hand-written module: an AST comparison
209
+ that treats the erased type wrappers (`as`, `@type` casts, parentheses) as
210
+ transparent reports only the imports the source no longer needs — a never-used
211
+ `fileURLToPath`, the dead `isUnderDistro`, and the type-only `WslAgent` (which
212
+ tsc elides; the module still loads through its other edges) — plus tsc's
213
+ reflow and one internal rename in `decorateResult`, whose result row now
214
+ derives from the handle type instead of a second type parameter. `resolve()`
215
+ returns the precise `WslExecSpec` where the JSDoc declared the wider
216
+ `ShellExecSpec`. Thirty-six of thirty-six modules are TypeScript.
217
+
218
+ The transitional scaffolding then retires, nothing left for it to do:
219
+ `rootDirs` and the scratch declaration emission existed for a mixed tree
220
+ (`src/` sources importing un-migrated `lib/*.js` neighbours), and the
221
+ declaration-only second pass (`tsconfig.dts.json`) existed to give the
222
+ hand-written JavaScript its `.d.ts`. `pnpm run build` is one `tsc` pass
223
+ emitting JavaScript and declarations straight into `lib/`, pruning
224
+ declarations no source owns; the `checkJs` gate drops its 36-entry exclude
225
+ list and states what is NOT generated — `test/**` and the ambient service
226
+ types, with `src/` checked by the build program (`noEmitOnError`); and
227
+ `lint:build` enforces the closed corpus (every `lib/*.js` has its
228
+ `src/<name>.ts`, every source has both artifacts), so "`lib/` is entirely
229
+ generated" is a checked property rather than a convention.
230
+
231
+ The real-machine probes then re-ran green on the migrated tree — 12 of 12, the
232
+ one verification face CI cannot cover (its self-hosted leg is skipped):
233
+ filesystem mutation through the throwaway wslfs profile, terminal, mode,
234
+ missing-wsl, bwrap, the shell confinement wiring, sandbox-off, picker, agent
235
+ protocol, watch, substrate and exec. The run surfaced two defects in the probe
236
+ harness itself. `ELECTRON_RUN_AS_NODE` — the flag that boots the harness CLI in
237
+ node mode — never crossed the WSL interop boundary: interop hands a Windows
238
+ process only the variables `WSLENV` lists, so the inline assignment was dropped
239
+ and the app booted as its GUI self, leaving the probe waiting on a process that
240
+ would never run; `test/probe/env.sh` lists the flag now, composed with any
241
+ `WSLENV` the caller had. And the kill-cleanup check counted every `sleep` in
242
+ the distro, which the resident agent's lease watchdog — introduced after the
243
+ probes last ran — makes non-zero by construction, alongside in-flight timeout
244
+ watchers; the check counts its own command line now, so a kill that really
245
+ leaked still fails it. And the suite's live log sits inside the checkout,
246
+ where the mirror sync's `tar` read it while it grew — `file changed as we read
247
+ it`, exit 1, `set -e` failing the probe before it ran; the sync excludes
248
+ `test/probe/.scratch` now, which is run scratch nothing at runtime reads.
249
+
250
+ One real defect fell out of the type gate's migration notes and closes here:
251
+ the agent execution path handed the EXEC frame the volatile wrapper object
252
+ where a byte budget belongs. `encodeExecFrame`'s `> 0` test was false on an
253
+ object, the frame carried 0, and 0 is the agent's "uncapped" — so the cut at
254
+ the caller's per-stream budget that `agent-exec` documents never ran, and a
255
+ chatty command cost unbounded distro memory. `src/index.ts` now reads
256
+ `config.maxOutputBytes.get()` like every other volatile field, and
257
+ `test/provider.test.mjs` pins the resolved number in the frame (the check
258
+ fails against the previous artifact). The one-shot path was always correct —
259
+ it reads the same field through the upstream executor's own `.get()`.
260
+
261
+ The second pending seam closes the same way — with an implementation instead
262
+ of a note. `WslFileSystem.processPathFromHostPath` was `async` against the
263
+ peer interface's synchronous contract, so the consumers that read it without
264
+ awaiting (spill-policy's attachment mapping, the session controller's and the
265
+ UI's path verification) received a Promise — always truthy, never the
266
+ `undefined` they test for — and mapped nothing. It is synchronous now, built
267
+ from what is already known: the distro once `distro()` has resolved it (the
268
+ pinned config, or the default `wsl.exe` answered by an earlier operation) and
269
+ the configured `cwd`, or the resolved home for a relative path. A call that
270
+ would have to ask `wsl.exe` — a cold default distro, a relative path before
271
+ the home is known — answers `undefined`, the same "no mapping" the ssh
272
+ provider gives, so the consumers take their documented refusal path instead of
273
+ receiving a bogus value. The value keeps the world spelling the backend keys
274
+ on, so their map → resolve → processPath round trip closes exactly as it does
275
+ for the host backend. `test/provider.test.mjs` pins the sync value, the round
276
+ trip, and both `undefined` cases (the checks fail against the previous
277
+ artifact). The `@ts-expect-error` count drops to three: the two remaining
278
+ design decisions and the upstream spawn seam.
279
+
280
+ The last in-code seam note closes by deletion rather than repair. The GUI file
281
+ service's `readByteRange` — a method older than the migration, which the type
282
+ gate flagged with an expect-error because its host-workspace branch called a
283
+ base method that does not exist — turned out to be unreachable: the remote
284
+ layer registers exactly the methods a decorated class declares (typert writes
285
+ its markers per instance, from the class that carries the decorators), the
286
+ base service has no such method, and nothing in the plugin or in the harness
287
+ calls it. Ranged reads already go through the wire op they always did —
288
+ `readBytes` with `options.range`, which the distro branch serves and the host
289
+ branch delegates. The method and its one test line are gone (the range case
290
+ stays covered through `readBytes`), and one more expect-error goes with it:
291
+ two remain, the exit-4 kind and the upstream spawn seam.
292
+
293
+ ## [0.7.5] - 2026-10-05
294
+
295
+ One theme: a failed environment probe retries instead of being trusted
296
+ forever - the shell placeholder that used to pin bash onto every later
297
+ command, and the hardcoded distro name that minted UNC identities.
298
+
299
+ ### Fixed
300
+
301
+ - **A failed shell probe no longer pins `bash` for the executor's
302
+ lifetime.** `defaultShell` reported the literal `"bash"` when both probes
303
+ failed - a cold distro, a flaky first `wsl.exe` call - and the executor
304
+ cached that placeholder permanently: every later command ran bash on a
305
+ zsh distro, and the shell tool introduced itself as bash. The probes now
306
+ answer `undefined`; the executor caches only a real resolution and falls
307
+ back to bash per call, so the next command retries. `shell-env` follows:
308
+ an unresolved shell simply omits the `DSH_WSL_SHELL` fact instead of
309
+ presenting a guess as the distro's login shell. Alongside, `toWorldPath`
310
+ no longer mints UNC identities under the hardcoded `"Ubuntu"` fallback -
311
+ the pinned distro is a required option now, and every caller passes it.
312
+
15
313
  ## [0.7.4] - 2026-10-05
16
314
 
17
315
  One theme: the 0.2.0 review's engineering debt closes out - the five
package/CONTRIBUTING.md CHANGED
@@ -10,26 +10,115 @@ the conventions this repository uses.
10
10
  | Windows 10 or 11 | the plugin targets Windows only |
11
11
  | WSL2 with at least one distro | `wsl.exe -l -v` lists the installed distros |
12
12
  | `bubblewrap` in the distro | `sudo apt install bubblewrap`. The sandbox fails closed without it. |
13
- | Node 20, 22, or 24 | the versions CI runs. `engines` requires `>=20`. |
13
+ | Node 22.19+, or 24 | the versions CI runs. `engines` requires `^22.19.0 \|\| >=24.0.0`, the harness host's own floor. |
14
14
 
15
15
  ## Toolchain
16
16
 
17
- The package has no dependencies, so there is no install step. Run the scripts
18
- straight from a checkout.
19
-
20
- `npm` runs the scripts in this repository, and CI uses npm. Treat
21
- `npm run <script>` as the reference toolchain.
22
-
23
- Do not add a runtime dependency. The package ships an empty `dependencies` object
24
- on purpose, because it is loaded into the DSH process and everything it needs
25
- arrives as a peer package.
26
-
27
- Style tooling is kept out for the same reason. There is no ESLint, no Prettier, and
28
- no `devDependencies` at all. The checks that would normally need them are built
29
- from Node builtins instead. `test/style.mjs` verifies the rules `.editorconfig`
30
- states, and it checks that every `files` entry still matches tracked content and
31
- every `exports` target still exists. The syntax pass inside `npm test` uses Node's
32
- own parser.
17
+ The runtime package ships an empty `dependencies` object on purpose: it is
18
+ loaded into the DSH process and everything it needs arrives as a peer package.
19
+ Running the plugin from a checkout has no install step.
20
+
21
+ Developing and running the gates do have dependencies. The 0.7.x tests import
22
+ pinned `@deepseek-ai/*` packages — the composition and boot tests run the
23
+ loader's real patch algorithm — so a fresh checkout needs one `pnpm install`
24
+ before the gates pass.
25
+
26
+ pnpm runs the scripts in this repository, pinned through the `packageManager`
27
+ field (corepack resolves it). Treat `pnpm run <script>` as the reference
28
+ toolchain.
29
+
30
+ Do not add a runtime dependency — see above. Style tooling is kept out for the
31
+ same reason: there is no ESLint and no Prettier. The checks that would normally
32
+ need them are built from Node builtins instead. `test/style.mjs` verifies the
33
+ rules `.editorconfig` states, and it checks that every `files` entry still
34
+ matches tracked content and every `exports` target still exists. The syntax pass
35
+ inside `pnpm test` uses Node's own parser.
36
+
37
+ Exactly three dependencies run install scripts (`@deepseek-ai/dsh-subprocess-local`,
38
+ `koffi`, `node-pty` — the native pieces); they are allow-listed in
39
+ `pnpm-workspace.yaml` and everything else builds nothing.
40
+
41
+ ## The type gate
42
+
43
+ `lint:types` runs `tsc --noEmit` under `strict` with `checkJs`: the JavaScript
44
+ in `lib/` is fully type-checked, and the types come from the peer packages'
45
+ own `.d.ts` — nothing is invented twice. `types/dsh-services.d.ts` pulls in the
46
+ `declare module '@deepseek-ai/cordis'` augmentations the service packages ship,
47
+ so `ctx.fs`, `ctx.shell`, `ctx.sandboxPolicy` and friends are typed; it declares
48
+ nothing itself and ships nowhere.
49
+
50
+ The gate's scope is what is NOT generated: `test/**` (the probe scripts
51
+ included) and the ambient service types the build shares. The sources under
52
+ `src/` are checked by the build program itself (`strict`, `noEmitOnError`),
53
+ which is strictly stronger than checking their emitted JavaScript. The shipped
54
+ artifact was checked first because it is where protocol drift with the pinned
55
+ peers hurts; the tests and probes came next, and the errors they surfaced were
56
+ real — a test that pinned a raw number into an argv, a couple of hand-written
57
+ JSDoc contracts wider than the code (`wslErrorCode`, `gitSpawnRewrite`) and
58
+ narrower than their seams, and one accepted-but-never-enforced byte-window cap
59
+ in the distro-routed file service, found because a fake hands the confinement
60
+ seam an `"full"` enforcement the core itself never reports.
61
+
62
+ Three patterns keep the check honest:
63
+
64
+ - A subclass's own config keys are declared as a type mirroring its
65
+ `static Config` schema and read through one explicit cast — the schema and
66
+ the type can then only drift apart visibly.
67
+ - A test double is typed as the slice of the seam it scripts (a duck
68
+ `@typedef`) and reaches the seam's declared type through one documented cast
69
+ at the injection point — the cast states which contract the double stands in
70
+ for, instead of silencing the mismatch with `any`.
71
+ - `@ts-expect-error` is allowed exactly when the suppressed error names a real
72
+ upstream seam gap, carries the reason inline, and the gap is written up in
73
+ `docs/UPSTREAM-*.md`. It is a published finding with a tracking doc, not a
74
+ silencer.
75
+
76
+ ## The build
77
+
78
+ The plugin must be loadable from a bare checkout: the harness has no
79
+ transpilation layer, the Windows runtime mirror is a copy of the tree, and the
80
+ tests exercise `lib/*.js` — the published artifact — rather than the sources.
81
+ Sources live in `src/`, built JavaScript and declarations in `lib/`, and the
82
+ artifacts stay COMMITTED. `pnpm run build` (`scripts/build.mjs`) runs one pass —
83
+ `tsc -p tsconfig.build.json` — emitting every `src/*.ts` as JavaScript plus its
84
+ declaration under the same export paths as the hand-written modules it
85
+ replaced. Imports name the artifact (`./x.js`); `nodenext` resolves that
86
+ through the TypeScript source. The build then prunes declarations no source
87
+ owns (a rename leftover), so `lib/` is exactly the artifacts of `src/`.
88
+
89
+ Rules that keep this honest:
90
+
91
+ - Never edit a generated file under `lib/` — its module doc names the source
92
+ to edit and `lint:build` fails the moment the two drift. Generated artifacts
93
+ keep tsc's canonical formatting (four-space, compacted); review `src/` for
94
+ every module.
95
+ - `lint:build` (`test/build-freshness.mjs`) enforces that the corpus is closed
96
+ — every `lib/*.js` has its `src/<name>.ts`, every source has both artifacts —
97
+ then snapshots every generated file, runs the real build, byte-compares and
98
+ RESTORES the snapshot: a check, not a rebuild; the remedy for drift is
99
+ `pnpm run build` plus a commit. It is part of the gate chain after
100
+ `test:syntax`.
101
+ - Generated files are not in the `checkJs` gate at all (`tsconfig.json` covers
102
+ `test/**` and the ambient types); their sources are type-checked by the build
103
+ program with `noEmitOnError`, so a broken build never overwrites `lib/`.
104
+ - Every JavaScript export carries its `types` condition (enforced by
105
+ `lint:style`), so downstream resolves these declarations; there is no
106
+ second, hand-written type surface.
107
+ - The protocol message union and friends are the single home now: consumers
108
+ import them (`@typedef {import("./agent-protocol.js").AgentMessage}
109
+ AgentMessage`) instead of restating them, which is how `lib/agent.js` and
110
+ the script-driven tests read them.
111
+
112
+ ### Adding a module
113
+
114
+ 1. Write `src/<name>.ts`; imports name the artifact (`./x.js`), which
115
+ `nodenext` resolves through the TypeScript source. Move every type into the
116
+ signature, keep the prose, and keep every exported NAME — types included,
117
+ since consumers import them (`import("./paths.js").DistroPath`). Close the
118
+ module doc with the note that names the generated artifact.
119
+ 2. `pnpm run build`, then `pnpm test`. There is no exclude list to maintain and
120
+ no registration step: the build and `lint:build` derive everything from
121
+ `src/`, and `lib/` is generated output — never edit it by hand.
33
122
 
34
123
  ## The development loop
35
124
 
@@ -50,7 +139,7 @@ The Windows copy under `default-workspace/dsh-plugin-wsl` is a runtime mirror. S
50
139
  it before launching the app:
51
140
 
52
141
  ```bash
53
- npm run sync:windows
142
+ pnpm run sync:windows
54
143
  ```
55
144
 
56
145
  The mirror destination sits outside a session workspace. An agent that runs the
@@ -62,28 +151,29 @@ terminal if you would rather not see the prompt.
62
151
 
63
152
  | Command | Covers | Needs a harness |
64
153
  |---|---|---|
65
- | `npm test` | style checks, syntax pass, and unit tests | no |
66
- | `npm run test:coverage` | unit tests with coverage | no |
67
- | `npm run lint:style` | style and lint rules | no |
68
- | `npm run probe` | filesystem behaviour against a real distro | yes |
69
- | `npm run probe:sandbox` | what bubblewrap confines and what it does not | no |
70
- | `npm run probe:sandbox-shell` | the confined executor through a real boot | yes |
71
- | `npm run probe:terminal` | the terminal provider | yes |
72
- | `npm run probe:substrate` | the agent filesystem substrate over a real `wsl.exe` transport | yes; inside the distro |
73
- | `npm run probe:watch` | the in-distro watcher over a real directory | yes; inside the distro |
74
- | `npm run probe:agent` | the resident-vs-one-shot fallback parity legs | yes; inside the distro |
75
- | `npm run probe:exec` | the agent-backed execution handle: timeout, kill, cwd failure | yes; inside the distro |
76
- | `npm run probe:missing-wsl` | how a `wslPath` that cannot start is reported | yes |
77
- | `npm run probe:picker` | the picker's root level, its refusals and its cap | yes |
78
- | `npm run probe:mode` | which POSIX-mode facts survive the share | no; Windows Node |
79
- | `npm run probe:sandbox-off` | the documented opt-out on both providers | yes |
80
-
81
- Every script above is wired in `package.json`. `npm test` runs `lint:style`,
82
- `test:syntax` and `test:unit` in that order. `prepublishOnly` runs `npm test` again
154
+ | `pnpm test` | style checks, syntax pass, type check, and unit tests | no |
155
+ | `pnpm run test:coverage` | unit tests with coverage | no |
156
+ | `pnpm run lint:style` | style and lint rules | no |
157
+ | `pnpm run lint:types` | the TypeScript check over `lib/` (`tsc --noEmit`) | no |
158
+ | `pnpm run probe` | filesystem behaviour against a real distro | yes |
159
+ | `pnpm run probe:sandbox` | what bubblewrap confines and what it does not | no |
160
+ | `pnpm run probe:sandbox-shell` | the confined executor through a real boot | yes |
161
+ | `pnpm run probe:terminal` | the terminal provider | yes |
162
+ | `pnpm run probe:substrate` | the agent filesystem substrate over a real `wsl.exe` transport | yes; inside the distro |
163
+ | `pnpm run probe:watch` | the in-distro watcher over a real directory | yes; inside the distro |
164
+ | `pnpm run probe:agent` | the resident-vs-one-shot fallback parity legs | yes; inside the distro |
165
+ | `pnpm run probe:exec` | the agent-backed execution handle: timeout, kill, cwd failure | yes; inside the distro |
166
+ | `pnpm run probe:missing-wsl` | how a `wslPath` that cannot start is reported | yes |
167
+ | `pnpm run probe:picker` | the picker's root level, its refusals and its cap | yes |
168
+ | `pnpm run probe:mode` | which POSIX-mode facts survive the share | no; Windows Node |
169
+ | `pnpm run probe:sandbox-off` | the documented opt-out on both providers | yes |
170
+
171
+ Every script above is wired in `package.json`. `pnpm test` runs `lint:style`,
172
+ `test:syntax`, `lint:types` and `test:unit` in that order. `prepublishOnly` runs `pnpm test` again
83
173
  at publish time, so a broken gate stops a release before the upload.
84
174
 
85
- CI runs `npm test` on Node 20, 22 and 24, on both `ubuntu-latest` and
86
- `windows-latest`, and `npm run test:coverage` on Node 24. The Windows runners are
175
+ CI runs `pnpm test` on Node 22 and 24, on both `ubuntu-latest` and
176
+ `windows-latest`, and `pnpm run test:coverage` on Node 24. The Windows runners are
87
177
  there because the plugin targets Windows; the coverage thresholds need Node 22.8 or
88
178
  newer, which is why that job runs on one version.
89
179
 
@@ -117,7 +207,7 @@ start, because a leftover file from an earlier run changes the result.
117
207
 
118
208
  ## What has been verified
119
209
 
120
- `npm test` covers the pure modules and runs a `--check` pass over every shipped
210
+ `pnpm test` covers the pure modules and runs a `--check` pass over every shipped
121
211
  module. It cannot import the service modules, because those need DSH peer packages
122
212
  that a bare checkout does not have. Only booting the harness closes that gap, which
123
213
  is why the probes exist; the archived record's §15.4 documents the five rounds of
package/README.md CHANGED
@@ -28,7 +28,7 @@ Step 3 should print a layer named `# == dsh-plugin-wsl-env`, and `- id: terminal
28
28
 
29
29
  Then open a folder under `\\wsl.localhost\<distro>\...` in the GUI. The picker lists every installed distro at its root level, and **New terminal** opens a shell in the distro.
30
30
 
31
- You also need **bubblewrap inside the distro**: run `npm run bootstrap -- <distro> --install` (drop `--install` for a read-only check), or paste `wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap`. Without it every command fails closed, see [Sandbox](#sandbox).
31
+ You also need **bubblewrap inside the distro**: run `pnpm run bootstrap -- <distro> --install` (drop `--install` for a read-only check), or paste `wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap`. Without it every command fails closed, see [Sandbox](#sandbox).
32
32
 
33
33
  Uninstall: `dsh plugin --profile wsl remove dsh-plugin-wsl-env`. Upgrade: run the same `add` command again.
34
34
 
@@ -73,7 +73,7 @@ Override a row by id in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. The keys w
73
73
  - **Paths and performance**: the model sees and operates on Linux paths
74
74
  (`/home/...`) on the distro's own ext4. `/mnt/c` reaches the Windows disk over
75
75
  9p — noticeably slow for many small files; keep heavy-IO projects on the
76
- distro filesystem. `npm run bootstrap -- <distro>` also reports whether
76
+ distro filesystem. `pnpm run bootstrap -- <distro>` also reports whether
77
77
  ripgrep, git and inotifywait (the search, snapshot and watch backends) are in place.
78
78
  - **WSLENV passthrough**: WSL imports only the variables listed in `WSLENV`.
79
79
  This plugin admits the managed `DSH_*` namespace by prefix, translating the
@@ -114,7 +114,7 @@ Commands are confined by `bubblewrap` inside the distro, and file writes are che
114
114
 
115
115
  **`bubblewrap` is required, and it fails closed.** Without it every confined command reports `SANDBOX_UNAVAILABLE` instead of running unconfined. Set `sandbox: false` on either provider to opt out; the tool layer then tells the model these operations have no sandbox.
116
116
 
117
- **The reported enforcement is `partial`, not `full`.** A process inside the distro can still run a Windows program through WSL interop, for example `/mnt/c/.../*.exe`, and bubblewrap does not govern it. `npm run probe:sandbox` demonstrates the boundary on your machine.
117
+ **The reported enforcement is `partial`, not `full`.** A process inside the distro can still run a Windows program through WSL interop, for example `/mnt/c/.../*.exe`, and bubblewrap does not govern it. `pnpm run probe:sandbox` demonstrates the boundary on your machine.
118
118
 
119
119
  See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for everything the plugin does not do.
120
120
 
@@ -122,37 +122,37 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#sandbox) for the design, and [do
122
122
 
123
123
  | Symptom | Cause | Fix |
124
124
  |---|---|---|
125
- | every command reports `SANDBOX_UNAVAILABLE` | `bubblewrap` is not installed in the distro | run `npm run bootstrap -- <distro> --install`, or set `sandbox: false` on both providers |
125
+ | every command reports `SANDBOX_UNAVAILABLE` | `bubblewrap` is not installed in the distro | run `pnpm run bootstrap -- <distro> --install`, or set `sandbox: false` on both providers |
126
126
  | a command or write is refused outside the session folder | expected behaviour of `workspace-write` | accept the wider-permission offer, or open a session on the folder you need |
127
127
  | writes are refused even inside the workspace | the session is in `read-only` mode | switch the Permissions selector |
128
128
  | `dsh plugin add` warns that no layer was activated | the dependency was already installed, so `add` had nothing to record | run `dsh plugin --profile wsl remove dsh-plugin-wsl-env`, then add it again |
129
129
  | the terminal still opens `cmd.exe` | the `terminal-controller` row from the layer did not apply | check that `dsh --profile wsl --dump-config` shows `shell: { path: wsl.exe, name: WSL }` |
130
130
  | changes to `lib/` have no effect | ES module cache | restart the app |
131
131
  | `link:\\wsl.localhost\...` leaves a broken symlink | pnpm cannot link a UNC path | link a Windows path instead; developing inside the distro needs the runtime mirror, see [Development](#development) |
132
- | `glob` and `grep` are slow | a distro search without rg inside the distro falls back to rg's own "command not found"; a Windows-folder search is native and unaffected | run `npm run bootstrap -- <distro> --install` (installs ripgrep); distro searches always run the distro's rg — they never walk the 9p share |
132
+ | `glob` and `grep` are slow | a distro search without rg inside the distro falls back to rg's own "command not found"; a Windows-folder search is native and unaffected | run `pnpm run bootstrap -- <distro> --install` (installs ripgrep); distro searches always run the distro's rg — they never walk the 9p share |
133
133
  | the terminal reports `unknown` activity | only while the resident agent is out — distro terminals are observed from inside the distro (a `DSH_TERMINAL_ID` marker scanned in `/proc`: a shell alone is `idle`, a shell running anything is `busy`) | check the distro is running; idle terminals are reclaimed automatically after the controller's unattended timeout (2 h by default), and `terminalIdleReclaim: false` restores the close-by-hand posture |
134
134
  | a result names an `FS_*` code | the code says what refused it, and what clears it | see the error-code table in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#error-codes) |
135
135
 
136
136
  ## Development
137
137
 
138
138
  ```bash
139
- npm test # style and packaging checks, syntax check, unit tests
140
- npm run test:coverage # the unit tests with coverage thresholds (Node 22.8+)
141
- npm run probe:sandbox # measure inside the distro what bubblewrap does and does not confine
142
- npm run probe # filesystem probe against a real distro (Windows + WSL only)
143
- npm run probe:sandbox-shell # boot a real harness and drive the confined executor
144
- npm run probe:terminal # open a PTY through the terminal provider
145
- npm run probe:substrate # drive the agent filesystem substrate over a real wsl.exe transport (from inside the distro)
146
- npm run probe:watch # arm the in-distro watcher over a real directory (from inside the distro)
147
- npm run probe:agent # the resident-vs-one-shot fallback parity legs (from inside the distro)
148
- npm run probe:exec # the agent-backed execution handle: timeout, kill, cwd failure (from inside the distro)
149
- npm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
150
- npm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
151
- npm run probe:mode # which POSIX-mode facts survive the share (Windows node only)
152
- npm run probe:sandbox-off # prove sandbox: false unconfines both providers (Windows + WSL only)
139
+ pnpm test # style and packaging checks, syntax check, unit tests
140
+ pnpm run test:coverage # the unit tests with coverage thresholds (Node 22.8+)
141
+ pnpm run probe:sandbox # measure inside the distro what bubblewrap does and does not confine
142
+ pnpm run probe # filesystem probe against a real distro (Windows + WSL only)
143
+ pnpm run probe:sandbox-shell # boot a real harness and drive the confined executor
144
+ pnpm run probe:terminal # open a PTY through the terminal provider
145
+ pnpm run probe:substrate # drive the agent filesystem substrate over a real wsl.exe transport (from inside the distro)
146
+ pnpm run probe:watch # arm the in-distro watcher over a real directory (from inside the distro)
147
+ pnpm run probe:agent # the resident-vs-one-shot fallback parity legs (from inside the distro)
148
+ pnpm run probe:exec # the agent-backed execution handle: timeout, kill, cwd failure (from inside the distro)
149
+ pnpm run probe:missing-wsl # boot a profile whose wslPath cannot start (Windows + WSL only)
150
+ pnpm run probe:picker # list the picker's root level, refusals and its cap (Windows + WSL only)
151
+ pnpm run probe:mode # which POSIX-mode facts survive the share (Windows node only)
152
+ pnpm run probe:sandbox-off # prove sandbox: false unconfines both providers (Windows + WSL only)
153
153
  ```
154
154
 
155
- There is nothing to install first: the package has no dependencies, and every module under test imports only Node builtins. CI runs `npm test` on Node 20, 22 and 24, on Linux and Windows, and `npm run test:coverage` on Node 24.
155
+ One `pnpm install` reproduces the pinned development dependencies before the first run: the 0.7.x tests import pinned `@deepseek-ai/*` packages (the composition and boot tests exercise the loader's real patch algorithm), while the runtime package itself still ships zero dependencies. pnpm is resolved through the `packageManager` field, so any corepack-enabled Node works. CI runs `pnpm test` on Node 22 and 24, on Linux and Windows, and `pnpm run test:coverage` on Node 24; `engines` matches the harness host's own floor (`^22.19.0 || >=24.0.0`).
156
156
 
157
157
  [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) has the file layout, the mounting, and the sandbox design. [CONTRIBUTING.md](CONTRIBUTING.md) has the development loop, the full gate list, and what has been verified.
158
158
 
@@ -185,7 +185,7 @@ selector read that value.
185
185
  distro can still run a Windows program through WSL interop, for example
186
186
  `/mnt/c/.../*.exe`. Bubblewrap does not govern that process.
187
187
 
188
- `npm run probe:sandbox` demonstrates the boundary on your machine and records it as
188
+ `pnpm run probe:sandbox` demonstrates the boundary on your machine and records it as
189
189
  `INFO`.
190
190
 
191
191
  ### Failure mode
@@ -250,7 +250,7 @@ distro checkout.
250
250
 
251
251
  The Windows copy at `default-workspace/dsh-plugin-wsl` is a runtime mirror.
252
252
  [`test/probe/sync-to-windows.sh`](../test/probe/sync-to-windows.sh) keeps it in
253
- step with the checkout, and `npm run sync:windows` runs that script.
253
+ step with the checkout, and `pnpm run sync:windows` runs that script.
254
254
 
255
255
  ## Test layers
256
256
 
@@ -306,12 +306,15 @@ docs/ this file, the configuration and limitation references, the
306
306
  ```
307
307
 
308
308
  `package.json` publishes a subset of this tree: the `files` list is the contract, and
309
- `npm run lint:style` fails if an entry stops matching tracked content.
309
+ `pnpm run lint:style` fails if an entry stops matching tracked content.
310
310
 
311
311
  ## Where the reasoning lives
312
312
 
313
313
  - [`CHANGELOG.md`](../CHANGELOG.md) records what changed in each release. It also
314
314
  names the documentation a reader would have acted on that is no longer true.
315
+ - [`REQUIREMENTS.zh.md`](REQUIREMENTS.zh.md) is the requirements register: every
316
+ documented behaviour commitment numbered, with where it is specified and what
317
+ verifies it, plus the open items (Chinese, maintainer-facing, not packaged).
315
318
  - [`archive/engineering-record.zh.md`](archive/engineering-record.zh.md) is the
316
319
  full engineering record, in Chinese: measured contracts, debugging rounds, and
317
320
  the conclusions as they stood then.