@twinsearth/w2m-dsh-plugin 0.8.99-tokenprobe.1 → 0.9.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/CHANGELOG.md CHANGED
@@ -4,6 +4,104 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.9.0] — 2026-10-14
8
+
9
+ **Three defects that were mounted, wired, and doing nothing — and the lint gate that had stopped being able to say so.**
10
+
11
+ ### The shape all three share
12
+
13
+ Each was, in the words an earlier release note already used for this class: *the record was created,
14
+ and the part that makes it useful was not filled in*. All three were invisible to the suite, and all
15
+ three were found by reading lint output rather than by running anything.
16
+
17
+ ### 1. `ReferenceError: all is not defined` — on the path an operator actually reaches
18
+
19
+ `src/relay/state.mjs` threw an undefined identifier while *building a refusal*:
20
+
21
+ ```js
22
+ if (targets.length === 0) {
23
+ throw new ProtocolError('NO_ONLINE_DEVICE', 'no paired device available for this task', {
24
+ known_devices: all.length, // ← `all` exists nowhere in the file
25
+ });
26
+ }
27
+ ```
28
+
29
+ `targets` is empty exactly when the fleet has nothing to dispatch to — a freshly configured machine
30
+ before its first peer joins, or a fleet whose machines are all scoped out. So the one case where the
31
+ operator needs the detail most answered with a `ReferenceError` instead of the refusal. The count they
32
+ need is the **unfiltered** device set, because the answer distinguishes *"nothing is paired"* from
33
+ *"something is paired but scoped out"*; that is `this.devices`, the same collection handed to
34
+ `targetsFor` a few lines above.
35
+
36
+ The project's own lint audit records `no-undef` as the rule that "caught the one real defect" of its
37
+ round, and notes why it matters: *an unbound identifier in an ESM module is a `ReferenceError` at
38
+ evaluation time, not an `undefined` that quietly takes a branch.* This is the same defect one release
39
+ later.
40
+
41
+ ### 2. The in-process agent's status was built and then dropped
42
+
43
+ `src/plugin/tools.mjs` documented a feature and never performed it:
44
+
45
+ ```js
46
+ // §8.2: when the agent runs inside this process its `rttMs` is reachable directly, so `w2m_status`
47
+ // can report it without a file.
48
+ const statusGetter = agent.status ?? agent.state ?? null; // ← and nothing else
49
+ ```
50
+
51
+ `readRtt` opens with `if (inProcessAgentStatus)` and skips the whole in-process branch when it is
52
+ null. Nothing ever assigned it, so on a machine whose agent *is* in that process the tool fell through
53
+ to reading a state file and reported:
54
+
55
+ ```
56
+ no agent round-trip time is available: the agent is not running in this process and no agent
57
+ state file was found under <stateDir>
58
+ ```
59
+
60
+ — wrong twice, and wrong in the direction that costs most: it tells the reader to doubt the agent, on
61
+ the one call they make to find out what is actually happening. The getter is now assigned.
62
+
63
+ ### 3. A parameter that promised a binding it never made
64
+
65
+ `src/chatops/bridge.mjs` had `function actionsFor(windowKey)` under a doc line claiming the window was
66
+ "bound in", and a body that never mentioned `windowKey`. The real binding is per call
67
+ (`pending: (window) => broker.listFor(window)`), so the parameter is gone rather than renamed: a
68
+ caller passing a window there was reading a promise the signature did not keep.
69
+
70
+ ### The lint gate, which is why nobody saw any of this
71
+
72
+ `eslint .` reported **155 errors**, and the release pre-flight requires zero — so the gate was red, and
73
+ a gate that is red for 117 pieces of noise is a gate nobody reads. Which is how three defects sat in
74
+ the tree.
75
+
76
+ | Rule | Count | Disposition |
77
+ |---|---|---|
78
+ | `no-console` | 117 | **Configuration.** Every one is in `scripts/` — `ilink-*.mjs` operator probes whose entire output *is* a transcribed QR flow, and `chatops-*-e2e.mjs` end-to-end drivers whose value is their trace. The rule's rationale ("a leftover debug statement") does not describe a file that returns nothing to a caller. `scripts/**` joins `bin/**` and `deploy/**` in the existing exception; `src/` and `test/` keep it at error. |
79
+ | `no-unused-vars` | 20 | 13 dead imports removed; 7 unused **bindings** annotated with a leading underscore, per the config's own convention. Two of those are load-bearing calls whose *result* is unused — `stats = await lstat(lexical)` is how a missing file is detected — so the call stays and the binding is annotated. |
80
+ | `no-promise-executor-return` | 4 | The idiomatic `new Promise((r) => setTimeout(r, ms))`. The rule is right that the executor returns a value; braces remove it without changing what the promise does. |
81
+ | `prefer-const` | 2 | Both real; one (`cursor`) carried a `let` that suggested a resumption the script does not implement. |
82
+ | `no-sequences` | 4 | Comma-operator arrows in a test, rewritten as block bodies — same order, same value, readable without knowing the operator. |
83
+ | `no-loss-of-precision` | 2 | **Dispositioned, not fixed.** These are the nineteen-digit measured `message_id` values the iLink contract fixture exists to demonstrate: JavaScript rounds them on parse, and a production path recovers the exact digits from the raw text *because* of that. Quoting the literals would delete the case under test, so they carry a scoped `eslint-disable` with the reason inline. |
84
+
85
+ Result: **0 errors**, measured with the repository's own config and `--report-unused-disable-directives`,
86
+ so a suppression that no longer suppresses anything fails the run instead of sitting there looking
87
+ like it does.
88
+
89
+ `docs/LINT-AUDIT.md` keeps its measurements as written and gains an addendum for this round, because
90
+ an audit document that silently disagrees with the tree is worse than no audit document.
91
+
92
+ ### Verified
93
+
94
+ - `node scripts/verify-join.mjs` — 16/16, unchanged by the edits above.
95
+ - `node scripts/check-entrypoints.mjs` — all entry points import.
96
+ - The four touched suites — **119 tests / 25 suites / 0 failures** (`chatops-commands`,
97
+ `chatops-ilink-contract`, `chatops-approval`, `chatops-durability`).
98
+ - `eslint .` — 0 errors.
99
+
100
+ The three defects do **not** carry new tests, and that is a stated gap rather than an oversight: each
101
+ sits on a path the existing suite does not construct (an empty target set at dispatch, an in-process
102
+ agent status getter, a bridge action set built with an argument). They were found by lint and fixed by
103
+ reading the surrounding contract; a test for each is the honest next step.
104
+
7
105
  ## [0.8.11] — 2026-10-14
8
106
 
9
107
  **The in-process join threw before it could pair — and a missing fingerprint took the fleet key with it.**
File without changes
File without changes
package/bin/w2m-stun.mjs CHANGED
File without changes
@@ -31,8 +31,8 @@
31
31
  * Usage: node mac-self-update.mjs --tarball <path> --sha256 <hex> [--verify-fleet <url>]
32
32
  */
33
33
 
34
- import { execFileSync, spawn } from 'node:child_process';
35
- import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync, readdirSync, statSync } from 'node:fs';
34
+ import { execFileSync } from 'node:child_process';
35
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync, readdirSync } from 'node:fs';
36
36
  import { createHash } from 'node:crypto';
37
37
  import { homedir, tmpdir } from 'node:os';
38
38
  import { join } from 'node:path';
@@ -120,7 +120,7 @@ function findDshProcess() {
120
120
  * `machineName` is read from the environment the agent was started with, falling back to the
121
121
  * hostname — the relay lists machines by the name they paired under.
122
122
  */
123
- async function fleetHasMachine(url, name) {
123
+ async function fleetHasMachine(url, _name) {
124
124
  try {
125
125
  const res = await fetch(new URL('/healthz', url).toString(), { signal: AbortSignal.timeout(10_000) });
126
126
  const body = await res.json();
@@ -133,7 +133,7 @@ async function fleetHasMachine(url, name) {
133
133
  }
134
134
  }
135
135
 
136
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
136
+ const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
137
137
 
138
138
  async function main() {
139
139
  const install = findInstall();
@@ -12,6 +12,51 @@
12
12
 
13
13
  ---
14
14
 
15
+ ## Addendum — v0.9.0: the gate was cleared, and this document is why it had to be
16
+
17
+ *The measurements above are left exactly as written. They were correct for 2026-10-08 and they are
18
+ the record of a real decision. But they no longer describe the tree, and a reader arriving at this
19
+ file today would draw the wrong conclusion from it — so the change is recorded here rather than by
20
+ editing the numbers.*
21
+
22
+ **What happened between then and v0.9.0.** The audit's disposition for most findings was "report, do
23
+ not fix", on the reasoning that ~30 findings were rule noise on idiomatic code and that rewriting
24
+ working code to satisfy a heuristic is the wrong trade. That reasoning still holds. What changed is
25
+ volume: the tree grew `scripts/ilink-*.mjs` and `scripts/chatops-*-e2e.mjs`, which between them added
26
+ **117 `no-console` errors** — every one of them a CLI script for which the console *is* the output
27
+ channel. `eslint .` went from 31 blocking errors to **155**.
28
+
29
+ The consequence is the one this document could not have anticipated: a lint gate that is red for 117
30
+ pieces of noise is a gate nobody reads, and **three real defects then sat in the tree undetected** —
31
+ including `ReferenceError: all is not defined` in `src/relay/state.mjs`, thrown while building a
32
+ refusal on the exact path an operator reaches when their fleet has nothing to dispatch to. §3.1's
33
+ claim that `no-undef` is "what caught the one real defect" was true, and it stayed true: `no-undef`
34
+ caught this one too, once anyone was reading.
35
+
36
+ **What v0.9.0 did about it**, per rule (full reasoning in `CHANGELOG.md`):
37
+
38
+ | Rule | Then | Now |
39
+ |---|---|---|
40
+ | `no-console` | 31 blocking, all in `test/`+`src/` as errors | 117 moved to **configuration**: `scripts/**` joins `bin/**`+`deploy/**` in the existing `no-console: off` block. `src/` and `test/` unchanged. |
41
+ | `no-unused-vars` | 21 | 13 dead imports removed, 7 unused bindings annotated `_`-prefixed. |
42
+ | `no-promise-executor-return` | 31 (demoted to warn in `test/`+`scripts/`) | The 4 **errors** in `src/`/`deploy/` given braces. The warnings are untouched. |
43
+ | `prefer-const` | — | 2 fixed. The rule's report on `scripts/ilink-talk.mjs`'s `cursor` was correct: the `let` implied a resumption the script does not implement. |
44
+ | `no-sequences` | — | 4 fixed in `test/chatops-commands.test.mjs`. |
45
+ | `no-loss-of-precision` | — | 2 **dispositioned**, not fixed: the nineteen-digit measured ids that the iLink fixture exists to demonstrate. Scoped `eslint-disable` with the reason inline. |
46
+ | `no-useless-assignment`, `no-control-regex` | off / left as error (§1.2) | Unchanged. `no-control-regex` still has no occurrence to fire on. |
47
+
48
+ **Current state: 0 errors, 127 warnings.** Measured with this repository's config plus
49
+ `--report-unused-disable-directives`, so a suppression that has stopped suppressing anything fails the
50
+ run rather than remaining as decoration.
51
+
52
+ **What §1.1 still costs, and it is not fixed by any of this.** Type-aware rules remain unavailable,
53
+ so `@typescript-eslint/no-floating-promises` — the canonical "you forgot `await`" check — is still
54
+ absent. Two of the three defects v0.9.0 fixed were *unused* things (an unassigned variable read
55
+ nowhere, a parameter mentioned nowhere), which `no-unused-vars` catches; the third was `no-undef`. A
56
+ missing `await` would still be invisible. That conclusion is unchanged.
57
+
58
+ ---
59
+
15
60
  ## 0. TL;DR
16
61
 
17
62
  | Question | Answer |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.8.99-tokenprobe.1",
4
- "description": "DSH: Windows2MacOS 鈥?drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
3
+ "version": "0.9.0",
4
+ "description": "DSH: Windows2MacOS — drive the same project on online Windows and macOS machines from one DeepSeek account, across networks and regions. P2P hole punching is the default path (relay + shared server as rendezvous, ledger and fallback), with a bundled STUN responder, Tailscale/public-VPS/tunnel shapes, Rabbit relay + per-machine Localside + eight DSH tools (replicate / split), nine aggregate verdicts and signed requests.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "bin": {
@@ -110,6 +110,23 @@
110
110
  "optional": true
111
111
  }
112
112
  },
113
+ "scripts": {
114
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
115
+ "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
116
+ "test:relay": "node --test --test-force-exit test/relay.test.mjs",
117
+ "test:agent": "node --test --test-force-exit test/agent.test.mjs",
118
+ "test:tools": "node --test --test-force-exit test/tools.test.mjs",
119
+ "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
120
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
121
+ "pack": "node scripts/pack.mjs",
122
+ "verify:join": "node scripts/verify-join.mjs",
123
+ "lint": "eslint .",
124
+ "lint:fix": "eslint . --fix",
125
+ "format": "prettier --write .",
126
+ "format:check": "prettier --check .",
127
+ "verify": "node scripts/check-entrypoints.mjs && node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
128
+ "test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs"
129
+ },
113
130
  "keywords": [
114
131
  "dsh-bundle",
115
132
  "dsh-plugin",
@@ -134,22 +151,5 @@
134
151
  "publishConfig": {
135
152
  "access": "public"
136
153
  },
137
- "license": "MIT",
138
- "scripts": {
139
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
140
- "test:e2e": "node --test --test-force-exit test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/p2p-agent.test.mjs",
141
- "test:relay": "node --test --test-force-exit test/relay.test.mjs",
142
- "test:agent": "node --test --test-force-exit test/agent.test.mjs",
143
- "test:tools": "node --test --test-force-exit test/tools.test.mjs",
144
- "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
145
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/chatops-fleet-join.test.mjs",
146
- "pack": "node scripts/pack.mjs",
147
- "verify:join": "node scripts/verify-join.mjs",
148
- "lint": "eslint .",
149
- "lint:fix": "eslint . --fix",
150
- "format": "prettier --write .",
151
- "format:check": "prettier --check .",
152
- "verify": "node scripts/check-entrypoints.mjs && node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/tools.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/p2p-node.test.mjs test/p2p-agent.test.mjs test/client-bundle.test.mjs test/loader-entry.test.mjs",
153
- "test:p2p": "node --test --test-force-exit test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/stun-server.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs"
154
- }
155
- }
154
+ "license": "MIT"
155
+ }
@@ -241,8 +241,16 @@ export function createBridge(options) {
241
241
  return { action: routed.kind, name: routed.name, refusal: routed.refusal, taskId: routed.taskId };
242
242
  }
243
243
 
244
- /** The action set the command table sees, with the window bound in. */
245
- function actionsFor(windowKey) {
244
+ /**
245
+ * The action set the command table sees.
246
+ *
247
+ * v0.9.0: this took a `windowKey` and never used it. The doc line above used to claim the window
248
+ * was "bound in", which was not true of the body -- the real binding is that `pending` takes the
249
+ * window per call (`broker.listFor(window)`), so the injected set is window-independent by design.
250
+ * The parameter is gone rather than renamed: a caller passing a window here was reading a promise
251
+ * the signature did not keep.
252
+ */
253
+ function actionsFor() {
246
254
  const injected = options.actions ?? {};
247
255
  return {
248
256
  ...injected,
@@ -164,9 +164,13 @@ export function createFileReader(options) {
164
164
  // Cheap refusal first, so a traversal never even reaches the filesystem.
165
165
  if (!contains(root, lexical)) return refuse(FILE_REFUSAL.ESCAPED, `lexical: ${lexical}`);
166
166
 
167
- let stats;
167
+ // The binding is unused by design: the *call* is the check. A missing path must be refused with
168
+ // `MISSING` here rather than surfacing as a `realpath` failure below, because the two are
169
+ // different answers for the caller -- "there is no such file" versus "there is one and it escapes
170
+ // the root". Underscored, per the config's own convention for an unused binding.
171
+ let _stats;
168
172
  try {
169
- stats = await lstat(lexical);
173
+ _stats = await lstat(lexical);
170
174
  } catch {
171
175
  return refuse(FILE_REFUSAL.MISSING, relPath);
172
176
  }
@@ -111,7 +111,7 @@ export function createFleetExecutor(options) {
111
111
  const timeoutMs = options.timeoutMs ?? 30_000;
112
112
  const doFetch = options.fetch ?? globalThis.fetch;
113
113
  const now = options.now ?? (() => Date.now());
114
- const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
114
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => { setTimeout(r, ms); }));
115
115
  const audit = options.audit ?? (() => {});
116
116
 
117
117
  /** One relay call. Resolves for every HTTP status: a refusal is an answer, not an exception. */
@@ -36,17 +36,7 @@ import { CHANNEL_STATE, CHANNEL_FEATURE } from './channel.mjs';
36
36
  // from here; the definitions live in ilink-protocol.mjs, which is what the contract suite pins.
37
37
  export * from './ilink-protocol.mjs';
38
38
  import { createTransport } from './ilink-transport.mjs';
39
- import {
40
- QR_BASE_URL,
41
- ENDPOINTS,
42
- BOT_TYPE,
43
- LOGIN_STATUS,
44
- MESSAGE,
45
- baseInfo,
46
- isAllowedApiUrl,
47
- extractText,
48
- messageId,
49
- } from './ilink-protocol.mjs';
39
+ import { QR_BASE_URL, ENDPOINTS, BOT_TYPE, MESSAGE, baseInfo, isAllowedApiUrl, extractText, messageId } from './ilink-protocol.mjs';
50
40
 
51
41
  /** Long poll: the platform holds the request open this long, so the deadline must exceed it. */
52
42
  const LONG_POLL_MS = 35_000;
@@ -117,7 +107,7 @@ export function createILinkChannel(options = {}) {
117
107
  return apiBase;
118
108
  }
119
109
 
120
- const url = (endpoint) => new URL(endpoint, QR_BASE_URL).toString();
110
+ const _url = (endpoint) => new URL(endpoint, QR_BASE_URL).toString();
121
111
 
122
112
  /** One request, with the outcome returned as data. Never throws on a business refusal. */
123
113
  async function call(endpoint, body, { base = null, authenticated = true, timeoutMs = 20_000, signal } = {}) {
@@ -37,7 +37,7 @@ import { createILinkChannel } from './ilink.mjs';
37
37
  import { isAllowedApiUrl } from './ilink-protocol.mjs';
38
38
  import { createFleetExecutor } from './fleet.mjs';
39
39
  import { createChatopsActions } from './actions.mjs';
40
- import { createAccountIdentity, parseSn, fleetOf, SN_STATE_KEY } from './account.mjs';
40
+ import { createAccountIdentity, SN_STATE_KEY } from './account.mjs';
41
41
  import { helpText, commandTable } from './commands.mjs';
42
42
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
43
43
  import { join, dirname } from 'node:path';
@@ -120,7 +120,7 @@ export function createOutboundQueue(options) {
120
120
  const random = options.random ?? Math.random;
121
121
  const audit = options.audit ?? (() => {});
122
122
 
123
- const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
123
+ const realSleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
124
124
  const sleep = options.sleep ?? realSleep;
125
125
 
126
126
  /** window -> lane -> { tail, pending, lastSentAt } */
@@ -180,7 +180,7 @@ const MAX_STAGES = 16;
180
180
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
181
181
  * overrides it at runtime for tests and packagers.
182
182
  */
183
- const PLUGIN_VERSION = '0.8.11';
183
+ const PLUGIN_VERSION = '0.9.0';
184
184
 
185
185
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
186
186
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -4930,7 +4930,7 @@ async function startLocalStack(ctx, cfg) {
4930
4930
  if (attempt < attempts) {
4931
4931
  const waitMs = Math.min(2000 * 2 ** (attempt - 1), 30000);
4932
4932
  ctx.logger?.info?.(`w2m: no fleet key yet (${describeIdentityFailure(last)}); retrying in ${waitMs}ms`);
4933
- await new Promise((resolve) => setTimeout(resolve, waitMs));
4933
+ await new Promise((resolve) => { setTimeout(resolve, waitMs); });
4934
4934
  }
4935
4935
  }
4936
4936
  // Not fatal: the caller may pair by code, and a later attempt may still succeed.
@@ -5057,7 +5057,6 @@ async function startLocalStack(ctx, cfg) {
5057
5057
  /** True when the last attempt failed without sending anything to the relay. */
5058
5058
  let joinLastWasKeyless = false;
5059
5059
  let joinSucceeded = false;
5060
- let joinAttempts = 0;
5061
5060
  let joinLastError = null;
5062
5061
  async function joinFleet() {
5063
5062
  if (joinSucceeded) return { paired: true, by: 'existing token' };
@@ -5067,7 +5066,6 @@ async function startLocalStack(ctx, cfg) {
5067
5066
  if (coolingDown) return joinPromise;
5068
5067
  joinPromiseAt = Date.now();
5069
5068
  joinPromise = (async () => {
5070
- joinAttempts += 1;
5071
5069
  if (alreadyPaired) {
5072
5070
  joinSucceeded = true;
5073
5071
  ctx.logger?.info?.(`w2m: already paired (token in ${loaded.path}); not pairing again`);
@@ -5190,10 +5188,29 @@ async function startLocalStack(ctx, cfg) {
5190
5188
  }
5191
5189
  if (!alreadyPaired) scheduleJoinRetry();
5192
5190
 
5193
- // §8.2: when the agent runs inside this process its `rttMs` is reachable directly, so `w2m_status`
5191
+ // §8.2: when the agent runs inside this process its `rttMs` is reachable directly, so `w2m_status`
5194
5192
  // can report it without a file. When the agent is its own process this stays null and the tool
5195
5193
  // says so instead of guessing.
5194
+ //
5195
+ // v0.9.0: **this assignment is the whole feature, and it was missing.**
5196
+ //
5197
+ // The getter was built and then dropped on the floor, so `inProcessAgentStatus` stayed `null` for
5198
+ // the life of the process. `readRtt` opens with `if (inProcessAgentStatus)` and skips the entire
5199
+ // in-process branch when it is null -- so it fell through to reading a state file, and reported
5200
+ //
5201
+ // no agent round-trip time is available: the agent is not running in this process and no agent
5202
+ // state file was found under <stateDir>
5203
+ //
5204
+ // on a machine whose agent *was* in that process and *was* publishing that file. The wording is
5205
+ // wrong twice over, and it is wrong in the direction that costs the most: it tells the reader to
5206
+ // doubt the agent, on the one call they make to find out what is actually happening.
5207
+ //
5208
+ // This is the defect shape the release notes keep recording -- *the record was created and the
5209
+ // part that makes it useful was not filled in* -- and it is the same shape as the identity line
5210
+ // fixed above. ESLint's `no-unused-vars` is what surfaced it: the variable was read nowhere
5211
+ // because nothing was ever assigned to the thing it was meant to feed.
5196
5212
  const statusGetter = agent.status ?? agent.state ?? null;
5213
+ if (typeof statusGetter === 'function') inProcessAgentStatus = statusGetter;
5197
5214
 
5198
5215
  return async () => {
5199
5216
  clearJoinRetry();
@@ -16,7 +16,7 @@ import { createHash, randomBytes } from 'node:crypto';
16
16
  // v0.7.7: fleet scoping. Imported rather than inlined so the rule has one home with its own
17
17
  // exhaustive tests — three lines inside this object would be untestable without a live relay.
18
18
  import { targetsFor } from '../chatops/fleet-wiring.mjs';
19
- import { fleetForPairingCode, deviceFleetFields } from '../chatops/fleet-wiring.mjs';
19
+ import { deviceFleetFields } from '../chatops/fleet-wiring.mjs';
20
20
  // v0.8.1: the account-derived fleet key, so a machine can join with no pairing code.
21
21
  import { planJoin } from '../chatops/fleet-join.mjs';
22
22
 
@@ -1488,7 +1488,25 @@ export class RabbitState {
1488
1488
 
1489
1489
  if (targets.length === 0) {
1490
1490
  throw new ProtocolError('NO_ONLINE_DEVICE', 'no paired device available for this task', {
1491
- known_devices: all.length,
1491
+ /**
1492
+ * v0.9.0: `all` was never defined here — this threw `ReferenceError: all is not defined`
1493
+ * instead of the refusal it was written to be, and it did so on **the one path an operator
1494
+ * actually reaches**: `targets` is empty exactly when the fleet has nothing to dispatch to,
1495
+ * which is what a freshly configured machine sees before its first peer joins.
1496
+ *
1497
+ * The count an operator needs there is "how many devices does this relay know at all",
1498
+ * because the answer distinguishes *"nothing is paired"* from *"something is paired but
1499
+ * scoped out"* — and `targets` being fleet-filtered cannot answer that. `this.devices` is
1500
+ * the unfiltered set the same call hands to `targetsFor` a few lines above (see the
1501
+ * `devices: [...this.devices.values()]` there), so its size is that number.
1502
+ *
1503
+ * ESLint's `no-undef` found this, which is the rule the project's own lint audit credits as
1504
+ * "what caught the one real defect" of that round. This is the same class of defect one
1505
+ * release later, and the audit's note about it is worth repeating: an unbound identifier in
1506
+ * an ESM module is a `ReferenceError` at evaluation time, not an `undefined` that quietly
1507
+ * takes a branch.
1508
+ */
1509
+ known_devices: this.devices.size,
1492
1510
  });
1493
1511
  }
1494
1512