@twinsearth/w2m-dsh-plugin 0.0.0-stage → 0.4.6

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 (60) hide show
  1. package/CHANGELOG.md +1288 -0
  2. package/LICENSE +21 -0
  3. package/PROTOCOL-v0.1.2.md +165 -0
  4. package/PROTOCOL-v0.3.0.md +454 -0
  5. package/PROTOCOL-v0.3.9.md +207 -0
  6. package/PROTOCOL-v0.4.0.md +255 -0
  7. package/PROTOCOL.md +339 -0
  8. package/README.md +665 -2
  9. package/bin/w2m-localside.mjs +303 -0
  10. package/bin/w2m-rabbit.mjs +396 -0
  11. package/bin/w2m-stun.mjs +231 -0
  12. package/cordis.patch.yml +94 -0
  13. package/deploy/caddy/Caddyfile.example +118 -0
  14. package/deploy/docker/Dockerfile +144 -0
  15. package/deploy/docker/README.md +338 -0
  16. package/deploy/docker/docker-compose.yml +179 -0
  17. package/deploy/nginx/w2m.conf.example +196 -0
  18. package/deploy/shared-server/README.md +118 -0
  19. package/deploy/shared-server/install.sh +255 -0
  20. package/deploy/systemd/w2m-rabbit.service +150 -0
  21. package/deploy/systemd/w2m-stun.service +69 -0
  22. package/deploy/windows/README.md +115 -0
  23. package/deploy/windows/install-service.ps1 +195 -0
  24. package/deploy/windows/w2m-agent-supervisor.mjs +205 -0
  25. package/deploy/windows/winsw/w2m-localside.xml +50 -0
  26. package/docs/DEPLOY.md +902 -0
  27. package/docs/LINT-AUDIT.md +492 -0
  28. package/docs/PIPELINE-DESIGN.md +95 -0
  29. package/docs/TROUBLESHOOTING.md +560 -0
  30. package/docs/UI-CARDS.md +81 -0
  31. package/lib/index.js +13 -0
  32. package/lib/tools.js +11 -0
  33. package/package.json +133 -4
  34. package/src/agent/agent.mjs +3317 -0
  35. package/src/agent/allowed-commands.mjs +116 -0
  36. package/src/agent/caps.mjs +437 -0
  37. package/src/agent/exec.mjs +356 -0
  38. package/src/agent/git.mjs +616 -0
  39. package/src/agent/identity.mjs +253 -0
  40. package/src/agent/p2p-node.mjs +1243 -0
  41. package/src/agent/p2p.mjs +605 -0
  42. package/src/agent/resolve.mjs +148 -0
  43. package/src/agent/spool.mjs +252 -0
  44. package/src/agent/stun.mjs +397 -0
  45. package/src/agent/url.mjs +127 -0
  46. package/src/plugin/auto-update.mjs +574 -0
  47. package/src/plugin/schedule.mjs +452 -0
  48. package/src/plugin/shared-config.mjs +264 -0
  49. package/src/plugin/tools.mjs +3891 -0
  50. package/src/plugin/update-install.mjs +764 -0
  51. package/src/plugin/update-source.mjs +686 -0
  52. package/src/relay/metrics.mjs +284 -0
  53. package/src/relay/peers.mjs +180 -0
  54. package/src/relay/persistence.mjs +207 -0
  55. package/src/relay/report.mjs +591 -0
  56. package/src/relay/server.mjs +1187 -0
  57. package/src/relay/state.mjs +2004 -0
  58. package/src/relay/stun-server.mjs +733 -0
  59. package/src/signing.mjs +359 -0
  60. package/src/util/cli.mjs +70 -0
@@ -0,0 +1,492 @@
1
+ # Lint & format audit (task-23)
2
+
3
+ > Scope: adds ESLint + Prettier configuration and a CI lint job to a project that
4
+ > had neither. **TypeScript is deliberately deferred** — see §1.1.
5
+ >
6
+ > This document is the audit the change is based on: what the tools report on
7
+ > the tree **as it stands**, and what to do about each finding. Nothing in
8
+ > `src/`, `test/`, `bin/` or `scripts/` was modified to make lint pass.
9
+ >
10
+ > Measured 2026-10-08 against `eslint@10.12.0`, `prettier@3.9.9`,
11
+ > `globals@17.13.0`, on Node 24.21.0 (Windows).
12
+
13
+ ---
14
+
15
+ ## 0. TL;DR
16
+
17
+ | Question | Answer |
18
+ |---|---|
19
+ | Do the tools run? | Yes: `eslint .` lints 38 files; `prettier --check .` flags 44–45 (the repo is being edited concurrently, so this drifts by a file or two). |
20
+ | How many real defects? | **1** (`no-undef`: a function is called but never imported — §3.1). |
21
+ | Current ESLint state | 31 errors, 43 warnings across 19 files. |
22
+ | Would Prettier rewrite the tree? | **Yes — ~41–45 of 48 files, ~90% of all lines.** Recommendation: **do not run it** (§4). |
23
+ | Is `dependencies` still empty? | Yes, `{}` — unchanged. Only `devDependencies` gained four entries. |
24
+ | Is lint a blocking CI gate? | Yes. Prettier is reported, not blocking (§5). |
25
+
26
+ ---
27
+
28
+ ## 1. What was added
29
+
30
+ | File | Purpose |
31
+ |---|---|
32
+ | `eslint.config.mjs` | Flat config (ESLint 9/10 style), stock JS parser only |
33
+ | `.prettierrc` | Options derived from measuring the existing style (§4.1), not from defaults |
34
+ | `.prettierignore` | Keeps generated artifacts, deployment templates and licence text out of scope |
35
+ | `.github/workflows/ci.yml` | New `lint` job (additive; `test`, `live-api`, `pack` untouched) |
36
+ | `package.json` | `devDependencies` + four scripts; `dependencies` still `{}` |
37
+
38
+ ### 1.1 Why no TypeScript, and what that costs
39
+
40
+ The project is plain ESM JavaScript with no build step. Adding
41
+ typescript-eslint would mean adding TypeScript — which is the thing the user
42
+ asked to defer. The practical consequence is that the **type-aware rules are
43
+ unavailable**, and one of them matters here:
44
+
45
+ - `@typescript-eslint/no-floating-promises` — the canonical "you forgot
46
+ `await`" rule. It requires a TS program, so it cannot be enabled in this
47
+ configuration.
48
+
49
+ What was enabled instead, and how far it actually gets:
50
+
51
+ | Substitute | Coverage | Honest limitation |
52
+ |---|---|---|
53
+ | `no-undef` | An identifier that does not exist anywhere. **This is what caught the one real defect.** | Does not see a *wrong* identifier that does exist. |
54
+ | `no-async-promise-executor` | `new Promise(async …)` | n/a |
55
+ | `require-atomic-updates` | Shared state written after an `await` | ~6 of 7 findings in `src/` are false positives (§3.3) |
56
+ | `no-promise-executor-return` | Returning a value from a Promise executor | Fires on the idiomatic `new Promise((r) => setTimeout(r, ms))` |
57
+
58
+ **Conclusion:** a forgotten `await` remains invisible to this configuration.
59
+ Anyone wanting that class of check needs TypeScript, and this audit is the
60
+ evidence for the cost of not having it: one real defect out of the 31 errors,
61
+ and it happened to be a missing import rather than a missing `await`.
62
+
63
+ ### 1.2 Rule-set structure
64
+
65
+ - `js.configs.recommended` as the base (ESLint 10's recommended set).
66
+ - A general block for `**/*.mjs` with `globals.node`, then explicit rules
67
+ grouped as *unbound/unused names*, *async mistakes*, *suspicious-but-legal*,
68
+ *shadowing*, *hygiene*, and *output discipline*.
69
+ - `bin/**` turns `no-console` off: the CLI entry points print by design.
70
+ - `test/**` and `scripts/**` demote exactly two rules to warnings (§3.3).
71
+
72
+ Two deliberate rule decisions, both with the reason recorded in the config:
73
+
74
+ | Rule | Setting | Why |
75
+ |---|---|---|
76
+ | `no-useless-assignment` | **off** | Ships as an *error* in ESLint 10's recommended set, and fires 6 times on `let x = null; try { x = … } catch { x = null }`. ESLint treats try/catch as "assigned on every path" and cannot see the window before the first assignment. Removing those initialisers would be the wrong fix. |
77
+ | `no-control-regex` | **left as error** | Fires 3 times in `src/agent/exec.mjs` on regexes that match ANSI escape sequences (`\x1b`, `\x07`). Those characters are the point of the pattern, so the correct fix is an inline `eslint-disable-next-line` at those lines — in `src/`, i.e. **not this task's write scope**. Reported in §3.2. |
78
+
79
+ ---
80
+
81
+ ## 2. Baseline numbers (real output)
82
+
83
+ ```
84
+ $ eslint .
85
+ ✖ 74 problems (31 errors, 43 warnings)
86
+
87
+ $ eslint . --format json -> 38 files linted, 19 with findings
88
+ ```
89
+
90
+ By rule:
91
+
92
+ | Count | Rule | Where it comes from |
93
+ |---|---|---|
94
+ | 31 | `no-promise-executor-return` | my config, demoted to warn in `test/`+`scripts/` |
95
+ | 21 | `no-unused-vars` | my config |
96
+ | 18 | `require-atomic-updates` | my config, demoted to warn in `test/`+`scripts/` |
97
+ | 6 | `no-useless-assignment` | ESLint 10 recommended — now **off** |
98
+ | 3 | `no-control-regex` | ESLint 10 recommended |
99
+ | 1 | `no-undef` | my config — **the real defect** |
100
+
101
+ By directory (before demotion): `test/` 38, `src/` 21, `scripts/` 9, `bin/` 0,
102
+ `lib/` 0.
103
+
104
+ After the two demotions, CI's blocking count is **31 errors / 43 warnings**.
105
+
106
+ ---
107
+
108
+ ## 3. Findings, by disposition
109
+
110
+ ### 3.1 REAL DEFECT — 1 finding (fix in `src/`, outside this task's scope)
111
+
112
+ ```
113
+ src/plugin/tools.mjs:571:9 error 'existsSync' is not defined no-undef
114
+ ```
115
+
116
+ ```js
117
+ // src/plugin/tools.mjs
118
+ import { promises as fs } from 'node:fs'; // line 49 — only `fs`, no `existsSync`
119
+ …
120
+ function resolveProfileDir(config) { // line 559
121
+ …
122
+ for (let depth = 0; depth < 8; depth += 1) {
123
+ const parent = path.dirname(dir);
124
+ if (parent === dir) break;
125
+ dir = parent;
126
+ if (path.basename(dir) !== 'node_modules') continue;
127
+ const root = path.dirname(dir);
128
+ if (existsSync(path.join(root, 'package.json'))) return root; // line 571 ← ReferenceError
129
+ }
130
+ ```
131
+
132
+ **Why it is a defect, not a style complaint**
133
+
134
+ - `existsSync` is never imported anywhere in the file (all six imports checked).
135
+ - In an ESM module an unbound identifier is a `ReferenceError` at evaluation
136
+ time, not `undefined` — so this throws rather than silently taking the false
137
+ branch.
138
+ - It is reachable from normal plugin startup: `resolveProfileDir` is called at
139
+ `tools.mjs:416` while building the tool configuration, and the loop reaches
140
+ line 571 as soon as it walks up into a directory literally named
141
+ `node_modules`. That is the ordinary layout this project *ships* (`files`
142
+ includes `src/` and `lib/`, so the package always sits under a
143
+ `node_modules/`).
144
+ - The module-load walk probably starts from a path whose parent chain contains
145
+ `node_modules` in the published layout, which is exactly when a user would
146
+ hit it.
147
+ - `test/tools.test.mjs` has **no** `resolveProfileDir` coverage (grepped), which
148
+ is why 10 test files and 291 assertions pass while this line stays broken.
149
+
150
+ **Suggested fix** (one line, in `src/plugin/tools.mjs`, which is why this audit
151
+ stops here):
152
+
153
+ ```js
154
+ import { existsSync, promises as fs } from 'node:fs';
155
+ ```
156
+
157
+ **Suggested test**: a case that calls the profile-directory resolver from a
158
+ path under a `node_modules` directory and asserts it returns the package root.
159
+ That is the missing signal, not the import.
160
+
161
+ ### 3.2 PURE STYLE — 27 findings, worth unifying, no behaviour change
162
+
163
+ **Unused bindings — 21** (`no-unused-vars`). These are dead declarations: no
164
+ behaviour change either way, and the risk of removing them is that a reader
165
+ loses a hint. Grouped by how they should be handled:
166
+
167
+ *Never-used imports (9)* — safe to delete, and two are worth a second look
168
+ because the *name* suggests missing wiring:
169
+
170
+ | Location | Binding | Note |
171
+ |---|---|---|
172
+ | `src/plugin/auto-update.mjs:44` | `parseSha256Sums` | **looks like missing wiring** — a checksum parser imported and never called. Verify whether signature verification was meant to run (§3.3). |
173
+ | `src/plugin/schedule.mjs:198` | `secondOfDayIn` | helper imported, never called |
174
+ | `test/relay.test.mjs:33` | `Persistence` | test-side import left over |
175
+ | `scripts/pack.mjs:29` | `relative` | — |
176
+ | `test/auto-update.test.mjs:11` | `before` | hook imported but no `before()` in the file |
177
+ | `test/auto-update.test.mjs:14` | `mkdirSync` | — |
178
+ | `test/e2e.test.mjs:29` | `tmpdir` | — |
179
+ | `test/tools.test.mjs:23` | `before` | same as above |
180
+ | `test/update-wiring.test.mjs:12` | `fs` | — |
181
+
182
+ *Assigned but never read (12)* — `prefer-const`-adjacent tidy-ups:
183
+ `src/plugin/auto-update.mjs:216` (`intervalDays`), `src/plugin/tools.mjs:1501`
184
+ (`transportError`), `src/relay/report.mjs:23` (`FAILURE_OUTCOME`),
185
+ `src/agent/agent.mjs:1186` (`stderr`), `scripts/probe-restart.mjs:83` (`base`),
186
+ `test/relay.test.mjs:814/828/865/902/914` (five `a`), `test/schedule.test.mjs:127`
187
+ (`midnight`), `test/agent.test.mjs:1925` (`requests`).
188
+
189
+ `test/relay.test.mjs`'s five bare `a` bindings are the strongest candidates for
190
+ a quick cleanup: an unused single-letter binding is almost always a leftover
191
+ from a refactor.
192
+
193
+ **`no-control-regex` — 3**, in one file:
194
+
195
+ ```
196
+ src/agent/exec.mjs:311:18 / 312:18 / 313:20 Unexpected control character(s) in regex: \x1b, \x07
197
+ ```
198
+
199
+ Correct fix: inline `// eslint-disable-next-line no-control-regex` above each,
200
+ because matching ANSI escape sequences is the intent. **In `src/`, so not
201
+ changed here.**
202
+
203
+ **`require-atomic-updates` in `src/` — 7**, see §3.3.
204
+
205
+ ### 3.3 RECOMMEND LEAVING ALONE — 46 findings
206
+
207
+ **`no-promise-executor-return` — 31, all of them.**
208
+
209
+ Every occurrence is the same line shape:
210
+
211
+ ```js
212
+ await new Promise((r) => setTimeout(r, ms)); // test/agent.test.mjs:171 and 30 more
213
+ ```
214
+
215
+ The rule's message ("Return values from promise executor functions cannot be
216
+ read") is about a *returned value* from an executor. Here the arrow body is a
217
+ bare call expression, which is the idiomatic sleep and is correct. **Disposition:
218
+ demoted to `warn` in `test/` and `scripts/`** (config, not a code change), so a
219
+ genuine instance still surfaces in output. 0 of 31 are defects.
220
+
221
+ **`require-atomic-updates` in tests — 18, all of them.**
222
+
223
+ All are deliberate state assignment in sequential scenario scripts:
224
+ `globalThis.fetch = stub` in a `finally`, `process.env.W2M_OPERATOR_TOKEN = …`
225
+ around a case, `group.relay = await startRelay(...)`. Each test file is a
226
+ straight-line script with no interleaving. **Disposition: demoted to `warn` in
227
+ `test/`.**
228
+
229
+ **`require-atomic-updates` in `src/` — 7, of which ~6 are false positives.**
230
+ These stay as **errors** (production code deserves the scrutiny), but none of
231
+ them should be "fixed" by rewriting the code:
232
+
233
+ | Location | Shared state written after `await` | Assessment |
234
+ |---|---|---|
235
+ | `agent.mjs:1022` | `toolchainCache = {…}` | **False positive.** Written from `toolchainInfo()`, which has an early `if (toolchainCache) return`. Two concurrent callers (`:1334`, `:1516`) can each recompute and the last write wins — but the value is derived from immutable per-project facts, so the only cost is doing the git lookup twice. There is nothing to "fix" short of a promise-coalescing cache. |
236
+ | `agent.mjs:1120,1121` | `identity.device_token`, `identity.rabbit_url` | **False positive.** Single caller: `bin/w2m-localside.mjs:184`, once, before the loop starts. |
237
+ | `agent.mjs:1591,1602` | `state.connected` | **False positive.** `streamOnce()` is called from the single reconnect loop; every writer is in that one function. |
238
+ | `agent.mjs:1798` | `pumping = false` (in `finally`) | **False positive.** `pump()` has a single entry point guarded by `pumping`. |
239
+ | `test/agent.test.mjs:2507` | `child` | Test-side, already demoted. |
240
+
241
+ **`no-useless-assignment` — 6.** The rule is now off with the reasoning in
242
+ §1.2. These are the defensive `let x = null` initialisers at
243
+ `src/agent/agent.mjs:1016`, `:1055`, `src/agent/git.mjs:365`,
244
+ `src/plugin/tools.mjs:1826`, `src/agent/caps.mjs:280`, and
245
+ `test/agent.test.mjs:1043`. Removing them would shorten the code and make the
246
+ declaration's type less clear for no behavioural gain.
247
+
248
+ **`no-console` — 0 findings, and one clarification the task asked for.**
249
+
250
+ The task expected console usage to be confined to `bin/`. It is — *except* that
251
+ a grep for `console.` finds 13 hits in `test/` and `scripts/`, all of which are
252
+ **console calls inside string literals** passed to `node -e` as the command
253
+ under test:
254
+
255
+ ```js
256
+ const SMOKE_ARGV = [NODE, '-e', 'console.log("crossnetwork")']; // test/crossnetwork.test.mjs:54
257
+ command_argv: [NODE, '-e', 'console.log("dedupe")'], // test/e2e.test.mjs:471
258
+ ```
259
+
260
+ ESLint correctly does not flag string contents. **So there is no counterexample
261
+ to report: no `console` call exists outside `bin/`.**
262
+
263
+ ---
264
+
265
+ ## 4. Prettier: measured, and recommended against
266
+
267
+ ### 4.1 The configuration was derived, not guessed
268
+
269
+ Measured over all 37 tracked `.js`/`.mjs` files (25,768 lines):
270
+
271
+ | Property | Measurement | Config chosen |
272
+ |---|---|---|
273
+ | Quotes | 7,279 single vs 285 double | `singleQuote: true` |
274
+ | Indentation | 21,283 space-indented lines, **0** tab-indented | `tabWidth: 2`, `useTabs: false` |
275
+ | Semicolons | present on statement ends throughout | `semi: true` |
276
+ | Line endings | 0 CRLF files | `endOfLine: 'lf'` |
277
+ | Object spacing | 1,156 `{ a: 1 }` vs 98 `{a: 1}` | `bracketSpacing: true` |
278
+ | Arrow params | 501 parenthesised vs **0** bare | `arrowParens: 'always'` |
279
+ | **Line width** | p50 34, p90 83, p95 96, **p99 118**, max 611 | `printWidth: 110` (swept, see below) |
280
+
281
+ `printWidth` was chosen by sweeping, not by eye — files that would change:
282
+
283
+ | printWidth | 80 | 90 | 100 | **110** | 120 | 130 | 140 |
284
+ |---|---|---|---|---|---|---|---|
285
+ | files changed | 45 | 44 | 42 | **41** | 42 | 43 | 43 |
286
+
287
+ 110 is the minimum. At that width, `trailingComma: 'all'` (41) beats `es5` (44)
288
+ and `none` (44); `arrowParens: 'avoid'` (43) loses to `always` (41).
289
+
290
+ **So the configuration is optimal for this codebase — and it still rewrites 41
291
+ files.**
292
+
293
+ ### 4.2 What it would actually change
294
+
295
+ Prettier was run with `--write` on a **throwaway copy** of the tracked tree (the
296
+ repository was never written to). Result:
297
+
298
+ | Metric | Value |
299
+ |---|---|
300
+ | Files rewritten | **31 of 37 JS files** (44–45 of 48 including docs/CI once `.prettierignore` scopes the rest) |
301
+ | Lines changed | **23,715 of 26,383 — 89.9%** |
302
+ | Category of changed lines | 23,513 "other", 122 whitespace-only, 61 quote style, **19 re-wrap** |
303
+
304
+ The decisive number is the last row: only **19** of ~23,700 changed lines are
305
+ pure re-wrapping. The rest are structural. Concretely:
306
+
307
+ ```js
308
+ // before (one line, 96 columns — inside the 110 budget)
309
+ '-c', 'user.name=w2m-test', '-c', 'user.email=w2m@test.invalid', 'commit', '-m', 'base',
310
+
311
+ // after
312
+ '-c',
313
+ 'user.name=w2m-test',
314
+ '-c',
315
+ 'user.email=w2m@test.invalid',
316
+ 'commit',
317
+ '-m',
318
+ 'base',
319
+ ```
320
+
321
+ and, worse, argument lists that already fit get exploded anyway when an
322
+ *enclosing* call exceeds the budget:
323
+
324
+ ```js
325
+ // before: 6 lines
326
+ throw new W2MError('W2M_ABORTED', `…${reason instanceof Error && reason.message ? … }`, {
327
+ hint: '…',
328
+ cause: reason,
329
+ });
330
+
331
+ // after: 12+ lines, with the message split across lines and the options object
332
+ // re-indented as an argument block
333
+ ```
334
+
335
+ ### 4.3 Recommendation: do not reformat now
336
+
337
+ The measured facts point one way:
338
+
339
+ 1. **The magnitude defeats the purpose.** The task's stated worry was "a
340
+ several-thousand-line diff that buries real changes". The real number is
341
+ ~23,700 lines across 31 files — 90% of the codebase. As a single commit it
342
+ would also destroy `git blame` for every line of `src/` at once, which
343
+ matters more than the diff size.
344
+ 2. **The house style is a deliberate rule, not sloppiness.** The author packs
345
+ multiple values per line and breaks only near ~110 columns (p99 = 118). That
346
+ is a consistent, readable convention; Prettier simply cannot express it, so
347
+ the "fix" is the formatter imposing a different convention.
348
+ 3. **The mitigation the task offered — "narrow the config so it doesn't touch
349
+ existing code" — is not available.** The sweep in §4.1 shows 41 files is the
350
+ floor. No combination of supported options reaches zero.
351
+ 4. **The cost of waiting is near zero.** Prettier is configured and runnable
352
+ (`npm run format`), `.prettierignore` already excludes generated files,
353
+ deployment templates and licence text, and CI reports the count on every run.
354
+ Nothing degrades by deferring.
355
+
356
+ **What I would do instead**, in order of preference:
357
+
358
+ - **Keep Prettier as a tool for new files** (`npm run format <path>`), and let
359
+ CI keep reporting the count. If the count starts growing rather than shrinking,
360
+ that is the signal to revisit — it would mean new code is being added
361
+ unformatted.
362
+ - **If a formatting commit is wanted, do it on a dedicated branch with no other
363
+ changes**, add it to `.git-blame-ignore-revs`, and only after the current
364
+ work has landed. Both the `45`-vs-`41` question and the blame problem then
365
+ stop mattering.
366
+ - **Do not** add 41 paths to `.prettierignore` to force a green check: that
367
+ would make the check meaningless while looking green, which is worse than an
368
+ informational count.
369
+
370
+ ---
371
+
372
+ ## 5. CI integration
373
+
374
+ ### 5.1 What was added
375
+
376
+ A new `lint` job (`ubuntu-latest`, Node 22) inserted **before** `test`. `test`,
377
+ `live-api` and `pack` are untouched, as is `release.yml`.
378
+
379
+ ```
380
+ lint:
381
+ - actions/checkout@v4
382
+ - actions/setup-node@v4 (node 22)
383
+ - npm install --no-audit --no-fund
384
+ - npm run lint <- BLOCKING
385
+ - npm run format:check || true <- informational, plus a ::warning:: with the count
386
+ ```
387
+
388
+ ### 5.2 `npm install` vs `npx --yes`: why `npm install`
389
+
390
+ The task asked which is more stable. **`devDependencies` + `npm install`** wins
391
+ for three reasons:
392
+
393
+ 1. **The repo commits no lockfile on purpose.** That rules out `npm ci`, but it
394
+ also means `npx --yes eslint@9` and `eslint: ^9` resolve through the same
395
+ "latest matching" logic — the reproducibility argument for `npx` does not
396
+ apply here.
397
+ 2. **`npx --yes` is the riskier of the two, not the safer.** It installs
398
+ whatever is latest *at run time* into a cache the author never sees, decoupled
399
+ from what a developer has locally. `npm install` uses the ranges in
400
+ `package.json`, so CI and a developer run the same versions.
401
+ 3. **The manifest becomes the source of truth.** `eslint@^10.12.0` in
402
+ `devDependencies` is reviewable; `npx --yes eslint@9` buried in YAML is not.
403
+ (It also means the task's `eslint@9` could not be honoured — 10.12.0 is
404
+ current, `@eslint/js` only exists from 9.9 onward, and ESLint 10's
405
+ `recommended` set includes `no-useless-assignment` and `no-control-regex`,
406
+ both discussed above.)
407
+
408
+ `dependencies` remains `{}`. All four additions are dev-only; the published
409
+ tarball is unaffected.
410
+
411
+ ### 5.3 Why lint is blocking but Prettier is not
412
+
413
+ - **Lint gates** because its errors are findings, not preferences: an unbound
414
+ identifier (§3.1), dead code, unused bindings. Cheap (seconds, one OS) and it
415
+ fails for a different reason than a test failure, so it is its own job rather
416
+ than a step inside `test`.
417
+ - **Prettier reports** because the only way to make it pass today is the
418
+ ~23,700-line rewrite of §4.3 or an ignore list that would make the check
419
+ vacuous. It prints the count and raises a `::warning::` so drift is visible.
420
+
421
+ Flipping Prettier to blocking later is a two-character change: delete the
422
+ `|| true`.
423
+
424
+ ### 5.4 Local verification
425
+
426
+ The CI steps were reproduced on the authoring machine (Windows, Node 24.21.0)
427
+ using the same binaries `node_modules/.bin` would expose:
428
+
429
+ ```
430
+ $ eslint . --format json -> 38 files linted, 19 with findings
431
+ exit 1 (CI fails the job here, by design)
432
+ $ prettier --check . -> 44 files, exit 1
433
+ $ prettier --list-different . | wc -l -> 44 (what the CI step echoes)
434
+ ```
435
+
436
+ `ci.yml` was validated as YAML and its job/step shape printed:
437
+
438
+ ```
439
+ ci.yml jobs : ['lint', 'test', 'live-api', 'pack']
440
+ lint runs-on='ubuntu-latest' steps=5
441
+ test runs-on='${{ matrix.os }}' steps=9
442
+ release.yml jobs: ['release'] (unchanged)
443
+ ```
444
+
445
+ ### 5.5 NOT verified here
446
+
447
+ - **The workflow has not been run on GitHub.** A hosted-runner run is the only
448
+ way to confirm `npm install` resolves on a clean Linux image; everything above
449
+ is a local simulation of the same commands.
450
+ - **Node 20 was not exercised.** The `lint` job pins Node 22 (matching the other
451
+ Linux jobs). ESLint 10 requires Node ≥ 20.19, so the job would also work on
452
+ 20, but the repository's `engines` floor is `>=20.0.0` — if a user on 20.0.x
453
+ runs `npm run lint` locally it may refuse. Worth raising the `engines` floor
454
+ to `>=20.19.0` in a later change, which is a `package.json` key this task was
455
+ told not to touch beyond scripts and devDependencies.
456
+ - **macOS/Windows lint was not run in CI**, and does not need to be: ESLint
457
+ results do not vary by platform here, and the goal was the cheapest useful
458
+ gate.
459
+
460
+ ---
461
+
462
+ ## 6. Files changed by this task
463
+
464
+ | File | Change |
465
+ |---|---|
466
+ | `eslint.config.mjs` | new |
467
+ | `.prettierrc` | new |
468
+ | `.prettierignore` | new |
469
+ | `.github/workflows/ci.yml` | +47 lines, one new job |
470
+ | `package.json` | +4 devDependencies, +4 scripts; `dependencies` still `{}` |
471
+
472
+ No file under `src/`, `test/`, `bin/`, `scripts/` or
473
+ `.github/workflows/release.yml` was modified. The one real defect (§3.1), the
474
+ three `no-control-regex` lines (§3.2) and the six `no-useless-assignment`
475
+ initialisers (§3.3) are **reported, not fixed** — they are all in `src/`.
476
+
477
+ ### 6.1 Side effect to decide on
478
+
479
+ `package.json`'s `files` array includes `docs/` and `deploy/` (only
480
+ `lib`, `src`, `bin`, `cordis.patch.yml`, `README.md`, `CHANGELOG.md`, `LICENSE`,
481
+ `PROTOCOL.md` and `PROTOCOL-v0.1.2.md` are matched as individual files), so
482
+ **any `.md` added under `docs/` enters the published tarball** — including this
483
+ audit. `scripts/pack.mjs` only checks that declared paths *exist*, not what is
484
+ inside them, so nothing breaks; but a contributor-facing audit is arguably not
485
+ runtime payload.
486
+
487
+ If you would rather it stayed out of the artifact, the smallest change is to
488
+ narrow `files` from `docs` to the specific documents that belong in the package
489
+ (`docs/DEPLOY.md`, `docs/TROUBLESHOOTING.md`). That touches a `package.json` key
490
+ beyond scripts/devDependencies, which this task was told not to modify — hence a
491
+ recommendation rather than an edit. Note it also affects the release hash, so it
492
+ belongs in a deliberate release change rather than this one.
@@ -0,0 +1,95 @@
1
+ # Pipeline mode: the design question that has to be answered first
2
+
3
+ Status: **unresolved, and `pipeline` is deliberately not implemented.** The relay rejects
4
+ `mode: "pipeline"` with `BAD_REQUEST` rather than guessing.
5
+
6
+ This file exists because the question is not obvious until you try to answer it, and writing the
7
+ analysis down is cheaper than re-deriving it — or than shipping a guess that half the system then
8
+ depends on.
9
+
10
+ ## What is already settled
11
+
12
+ `broadcast` (v0.3.2) covers "run once, tell everyone":
13
+
14
+ | mode | who runs | who is told |
15
+ |---|---|---|
16
+ | `replicate` | every machine | — |
17
+ | `split` | every machine, its own slice by index | — |
18
+ | `broadcast` | exactly one machine (deterministic) | every other machine, as an observer |
19
+
20
+ Observers hold an `observing` lease and are excluded from every aggregation step, so a machine that
21
+ did not run can never be counted as agreeing. That much is implemented and tested.
22
+
23
+ ## The question
24
+
25
+ A pipeline is a chain: stage 1 produces something, stage 2 consumes it, stage 3 consumes that.
26
+ On one machine that is unambiguous. On a fleet it is not, because the fleet can run stage 1 on *n*
27
+ machines and produce *n* different outputs. So before any code:
28
+
29
+ > **Which machine's stage-1 output does stage 2 receive?**
30
+
31
+ There is no default answer that is obviously right. The three defensible ones behave very
32
+ differently, and picking wrong is expensive to undo because the aggregate report, the CLI, the tool
33
+ schema and the protocol all encode the choice.
34
+
35
+ ## Option A — one machine runs the whole chain
36
+
37
+ The pipeline executes entirely on a single machine, stage after stage. `broadcast` then selects which
38
+ machine, and `pipeline` describes the sequence. Composition: `broadcast + pipeline` = "run this chain
39
+ on machine X and tell everyone".
40
+
41
+ - **Reproducible.** One machine, one chain, one record. Directly restartable: rerun the same chain
42
+ and you get the same shape.
43
+ - **Honest.** No cross-machine data flow to model, so nothing can be silently dropped in transit.
44
+ - **No new protocol surface.** Stages ship as a list inside the existing offer.
45
+ - **Cost:** no parallelism across stages, and the fleet is not used for the chain.
46
+
47
+ ## Option B — stages fan out, results converge
48
+
49
+ Stage 1 runs on the target set; the relay collects every machine's output and feeds the whole set to
50
+ stage 2, which also runs on the target set. This is a fan-out/fan-in graph.
51
+
52
+ - **Uses the fleet** for the chain, which is the obvious appeal.
53
+ - **Cost, and it is the serious one:** "feed the whole set to stage 2" is not a well-defined input.
54
+ Concatenated in what order? What does stage 2 do with three different outputs — run three times in
55
+ parallel on each machine, or once with all three as input? Each answer is a different feature, and
56
+ the aggregate report's meaning changes with each. Ordering alone is a correctness problem: two
57
+ machines finishing in a different order would produce different stage-2 inputs, so the same task id
58
+ would not describe the same execution — which breaks the property this project exists to provide.
59
+
60
+ ## Option C — each machine runs the whole chain independently
61
+
62
+ Every machine runs stage 1, then its own stage 2, then its own stage 3. No cross-machine flow at all;
63
+ it is `replicate` applied to a sequence.
64
+
65
+ - **Trivial to implement** — stage list inside the existing offer, no relay changes beyond validation.
66
+ - **Unambiguous** and keeps per-machine reproducibility.
67
+ - **Cost:** it is `replicate` with a for-loop. It adds no capability that running three `replicate`
68
+ tasks in sequence does not already give, so it earns little.
69
+
70
+ ## Recommendation
71
+
72
+ **Option A**, with Option C as a cheap follow-on if useful.
73
+
74
+ Reasons, in order of weight:
75
+
76
+ 1. Option B is the only one that needs cross-machine data flow, and it is the only one whose
77
+ semantics are unclear. Not implementing it is a decision, not an omission.
78
+ 2. Option A composes with `broadcast`, so it adds a real capability: "run this chain on that machine
79
+ and inform the fleet."
80
+ 3. Option A keeps the property the rest of the system is built on — a task id identifies one
81
+ execution, reproducible from its own record.
82
+
83
+ ## What implementing Option A requires
84
+
85
+ - `state.mjs`: accept `mode: 'pipeline'` with `stages: [{command_argv, cwd_rel, ...}]`; keep
86
+ `executor_machine_id` meaningful; reject `index_total > 1` for the same reason `broadcast` does.
87
+ - `agent.mjs`: execute stages in order, stop on the first non-`ok` stage, and report per-stage
88
+ outcomes. This is the substantial part — the envelope currently describes exactly one command.
89
+ - `report.mjs`: a chain failure must be distinguishable from a chain that never started.
90
+ - `tools.mjs`: `w2m_run` gains `stages`.
91
+ - `PROTOCOL`: the new field, and a compatibility note.
92
+
93
+ **Not verified, and it cannot be verified here:** whether Option A is what was actually wanted. That
94
+ is a product question, not a technical one, and guessing it is how a fleet ends up with a chained
95
+ mode nobody asked for.