@twinsearth/w2m-dsh-plugin 0.50.0 → 0.53.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,196 @@ 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.53.0] — 2026-10-15
8
+
9
+ **The shared-memory layer gets a suite, and it corrected three things this session had already "verified".**
10
+
11
+ ### The layer was measured by hand; now it is measured by the suite
12
+
13
+ Field-level omission at level 0, the refusal of an unproducible ancestor, the level 0/70 difference — **all of
14
+ that was real evidence, produced through actual dispatches to an actual machine.** It was also **not
15
+ repeatable**: it lived in a session, not in the suite. **A measurement that cannot be re-run is a memory of a
16
+ measurement.**
17
+
18
+ `test/memory-layer.test.mjs` adds **22 assertions**, and the suite grew to 84 files.
19
+
20
+ ### Three corrections, each of which the hand-run had got wrong
21
+
22
+ **1. `evidence` costs 50, not 70.**
23
+
24
+ ```
25
+ FIELD_LEVELS = { subject: 0, body: 0, evidence: 50 }
26
+ ```
27
+
28
+ A level-70 dispatch succeeded, and **70 was written down as the threshold.** It is not: 49 is refused and 50 is
29
+ allowed, and **50 is not arbitrary — it is the bottom of the `captain` band**, which is why the number is what
30
+ it is.
31
+
32
+ **2. `confidence` is an invariant, not a high-cost field.**
33
+
34
+ `NEVER_ACCEPTED.confidence` = *"a receiver does not adopt the sender's certainty (**invariant, not a
35
+ default**)"*. Level 90 is refused exactly as level 0 is, and there is no level that satisfies it. **The reason
36
+ string says "invariant" on purpose: a reader who thinks it is a threshold will try to raise it.**
37
+
38
+ **3. `FIELD_UNDECLARED` does not mean "forbidden", and the two APIs answer different questions.**
39
+
40
+ | call | question | `origin` / `observed_at` / `lineage` |
41
+ |---|---|---|
42
+ | `acceptField` | *may a receiver choose this?* | `FIELD_UNDECLARED` — no rule lets a receiver pick it |
43
+ | `acceptWithAuthority` | *what does this receiver keep?* | **present in `accepted`** — they are inherited |
44
+
45
+ **So `UNDECLARED` means "not a receiver's to choose", not "refused".** Reading it as a refusal would have the
46
+ receiver drop the ancestry that makes the block meaningful — and `INHERITED_FIELDS` plus
47
+ `RECEIVER_CHOOSABLE_FIELDS` partition the seven exactly, so neither list can change without the other.
48
+
49
+ ### Four assertions of mine that were wrong, and the last one is the interesting one
50
+
51
+ * **`ancestry` takes a `Map`, not an array**, and returns `{chain, missing, cycle, depth}` — **there is no `ok`
52
+ and no `code`.**
53
+ * **`MISSING_PARENT` is not an `ancestry` outcome.** `ancestry` **collects** gaps in `missing` — *"knowing how
54
+ much of a history is absent is more useful than knowing that some of it is"* — while `acceptBlock` is what
55
+ **refuses**, returning `accepted: null`.
56
+ * **`isDescendantOf` returns `{descendant, chain, missing, checked}`, not a boolean.** It returns more than a
57
+ boolean because that is its point: *"a claim that descends from something this machine already thought is an
58
+ **echo**, not corroboration, and **a fleet that cannot tell those apart will confidently agree with
59
+ itself**."* A parent that is not held gives `descendant: false` — and my assertion had demanded `true`.
60
+ * **I could not construct a cycle, and that turned out to be the finding.** `blockHash` covers `lineage`, so
61
+ rebuilding a block changes its hash and the edge no longer closes — **a real cycle would need a hash
62
+ preimage.** The walk produces a *diamond* instead, and `CYCLE` is reachable **only** from a `have` map whose
63
+ keys disagree with the blocks under them: a corrupted or hostile store, where **terminating is the point,
64
+ because an unbounded walk on hostile input is a denial of service.**
65
+
66
+ **Three attempts at that test asserted a cycle the code was right not to report.** The fourth printed the
67
+ traversal instead of predicting it, which is the only one of the four that produced a fact.
68
+
69
+ ## [0.52.0] — 2026-10-15
70
+
71
+ **Agent Plugins 1.0.0 manifests, validated against the published schemas — and the honest note about who
72
+ reads them.**
73
+
74
+ ### What shipped
75
+
76
+ ```
77
+ plugin.json root manifest, Agent Plugins 1.0.0, $schema pinned to the canonical identifier
78
+ mcp.json schema-valid with an EMPTY server map -- see below
79
+ skills/ unchanged; the four SKILL.md files were already the portable form
80
+ ```
81
+
82
+ `scripts/plugin-manifest-check.mjs` validates both, `pre-flight` gained a fourteenth check, and
83
+ `test/agent-plugins.test.mjs` adds fourteen assertions across both manifests and the skills.
84
+
85
+ ### The schemas were read, and the constraints are written out rather than fetched
86
+
87
+ `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json` and `.../mcp.schema.json` are both live, and the
88
+ rules come from them — including the `name` pattern `^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$`,
89
+ **which refuses `--` and `..` and which I would not have guessed.** They are **not fetched at check time**:
90
+ a validator that needs the network cannot run offline, and one that skips itself when the fetch fails reports
91
+ success for the case it could not measure.
92
+
93
+ **What this cannot check, stated rather than implied:** the schema's own description says *"the Agent Plugins
94
+ specification defines additional semantic and operational requirements."* Path containment, symlink
95
+ resolution, MCP transport support and the `PLUGIN_ROOT`/`PLUGIN_DATA` expansion rules are **not** expressible
96
+ here and **are not claimed**.
97
+
98
+ ### `mcp.json` ships with zero servers, and that is the honest version
99
+
100
+ The request specified MCP as the unified calling protocol. **DSH has no MCP client.** Measured across all
101
+ 8543 small code and config files in the application bundle: `plugin.json` 0, `mcp.json` 0, `agent-plugins` 0,
102
+ **and `mcpServers` 0 occurrences in host-owned code** — the 202 hits are third-party SDKs inside
103
+ `node_modules`, which support MCP without this host reading a config for it.
104
+
105
+ **And there is no MCP server in this package.** So the file is schema-valid with an empty map: a portable
106
+ client reads it and starts nothing, which is true. **The alternative — declaring a server that does not
107
+ exist — is a manifest that launches a failure**, and shipping that to satisfy a specification would be the
108
+ same mistake as advertising a capability the code lacks.
109
+
110
+ ### The one rule that is not from a schema caught me immediately
111
+
112
+ `plugin.json`'s `version` must equal `package.json`'s. **On the validator's first run it failed: the manifest
113
+ said 0.52.0 and `package.json` still said 0.51.0.** That is the whole justification for the rule — DSH loads
114
+ plugins through `package.json#dsh` and a Cordis bundle patch, **so nothing in this host ever reads
115
+ `plugin.json`.** A manifest with no consumer has no way to notice it is stale.
116
+
117
+ **A file nobody reads is a file that cannot be wrong.** The schema check and the drift check are the only
118
+ things standing between this manifest and quiet decay.
119
+
120
+ ### Four assertions that were wrong, mine
121
+
122
+ * **`frontmatter('---\nname: a\n')` returns `null`, not `{ data: undefined }`** — an unclosed block is not
123
+ frontmatter, and my assertion demanded the shape the code does not return. **The code was right.**
124
+ * **I typed `disableModelInvoction`** in the rejected-spelling set. **A misspelled name in a rejection list
125
+ is a name that is not rejected** — the exact failure the list exists to prevent.
126
+ * **I exported a contract object above the constants it read**, so importing the module threw
127
+ `Cannot access 'PLUGIN_FIELDS' before initialization`.
128
+ * **I wrote the `implementations` Map entry as `['name']: () => …`** inside an array of pairs, matching an
129
+ anchor line without reading the structure around it. **This is the third time in this project that an
130
+ edit matched an anchor string instead of the block containing it.**
131
+
132
+ Both validators now export their rules and keep the CLI in `main()`, so **the suite asserts them without
133
+ spawning a child** — a test that needs a subprocess is skipped wherever piped stdio is refused, which is
134
+ exactly where a check must still run.
135
+
136
+ ## [0.51.0] — 2026-10-15
137
+
138
+ **Four Agent Skills, written against the loader's real contract — and a gate that makes the contract matter.**
139
+
140
+ ### The four skills
141
+
142
+ ```
143
+ skills/
144
+ ├── fleet-control/SKILL.md the four modes, the nine verdicts, every refusal code
145
+ ├── memory-sync/SKILL.md field-level acceptance, omission versus null, ancestry
146
+ ├── session-ref/SKILL.md the dsh-session: codec, its three checks, its silent trap
147
+ └── resource-schedule/SKILL.md lease states, the ~50s ceiling, what "expired" means
148
+ ```
149
+
150
+ **Every rule in them was measured before it was written.** The refusal codes are the ones this session
151
+ actually produced, the ~50-second ceiling is one that was hit more than eight times, and the `dsh-session:`
152
+ section describes a codec whose three checks were read out of `@deepseek-ai/dsh-session-reference` — **including
153
+ the one that surprises people, where a payload that decodes correctly is still refused for not being
154
+ *canonical*.**
155
+
156
+ ### The contract came from the loader, not from a specification
157
+
158
+ `parseSkillFile` reads **`name`** and **`description`**, tests the name against
159
+ `/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, and **rejects three renamed spellings outright** —
160
+ `disableModelInvocation`, `modelInvocable`, `userInvocable` — in favour of the hyphenated forms. On any
161
+ failure it logs a warning and **returns**.
162
+
163
+ **That is the whole reason this ships with a gate.** A skill that violates the contract is not an error: it
164
+ simply never appears, the directory still looks shipped, and nothing is red. **It is a file whose only symptom
165
+ of being wrong is not being there.**
166
+
167
+ ### New check: `agent skills`, and the twelfth became the thirteenth
168
+
169
+ `scripts/skill-check.mjs` mirrors the loader's rules, and `pre-flight` now runs it.
170
+
171
+ ```
172
+ PASS agent skills (frontmatter satisfies the loader, so no skill is silently dropped): 4 skill(s) satisfy the loader contract
173
+ pre-flight: 13 pass, 0 fail, 1 unverifiable (13/13 registered check(s) reported) -- GREEN
174
+ ```
175
+
176
+ It is bound in **three** places — `DECLARED_CHECKS`, `GATE_INVENTORY` (five artifact rows: the validator and
177
+ the four `SKILL.md` files), and the implementation map — because this repository's own rule is that a
178
+ declared check with no artifact, or an artifact with no check, is red in both directions.
179
+
180
+ ### Two mistakes of mine, both worth keeping visible
181
+
182
+ **The check reported `exit null` and nothing else.** The cause was `result.error` — an `EPERM` from
183
+ `spawnSync` under a `workspace-write` sandbox that had replaced the `danger-full-access` of earlier runs. **The
184
+ code was right and the environment was refusing.** But the diagnosis was bad on its own terms: **a check that
185
+ prints an exit status and drops the reason names nothing**, so the failure now says
186
+ `EPERM: spawnSync …` and the next reader does not have to rediscover the sandbox boundary.
187
+
188
+ **I edited the `implementations` Map into invalid syntax** — `['name']: () => …` inside an array of pairs —
189
+ because I matched an anchor line without reading the structure it sat in. **This is the same error shape as
190
+ the `accept_block` branch in 0.41.0, and it is twice now.**
191
+
192
+ **The skills ship** — `skills` is in `package.json#files` beside `docs`, so the four `SKILL.md` files are in
193
+ the tarball. **They load only when a host points `DSH_BUNDLED_SKILL_DIR` at the directory**, which is a host
194
+ setting rather than something the package can assert; the alternative roots are `<project>/.dsh/skills`,
195
+ `<project>/.agents/skills`, `<dshHome>/skills` and `~/.agents/skills`.
196
+
7
197
  ## [0.50.0] — 2026-10-15
8
198
 
9
199
  **The matrix gains the two defects and the WeChat path — and the counts are read from the table, not typed.**
package/mcp.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {}
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "0.50.0",
3
+ "version": "0.53.0",
4
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",
@@ -19,11 +19,14 @@
19
19
  "./package.json": "./package.json"
20
20
  },
21
21
  "files": [
22
+ "mcp.json",
23
+ "plugin.json",
22
24
  "lib",
23
25
  "src",
24
26
  "bin",
25
27
  "deploy",
26
28
  "docs",
29
+ "skills",
27
30
  "cordis.patch.yml",
28
31
  "README.md",
29
32
  "CHANGELOG.md",
@@ -113,13 +116,13 @@
113
116
  }
114
117
  },
115
118
  "scripts": {
116
- "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.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",
119
+ "test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.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/agent-plugins.test.mjs test/memory-layer.test.mjs test/chatops-fleet-join.test.mjs",
117
120
  "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",
118
121
  "test:relay": "node --test --test-force-exit test/relay.test.mjs",
119
122
  "test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
120
123
  "test:tools": "node --test --test-force-exit test/tools.test.mjs",
121
124
  "test:crossnetwork": "node --test --test-force-exit test/crossnetwork.test.mjs",
122
- "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.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",
125
+ "test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.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/agent-plugins.test.mjs test/memory-layer.test.mjs test/chatops-fleet-join.test.mjs",
123
126
  "pack": "node scripts/pack.mjs",
124
127
  "verify:join": "node scripts/verify-join.mjs",
125
128
  "lint": "eslint .",
package/plugin.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "twinsearth-w2m-dsh",
4
+ "version": "0.53.0",
5
+ "description": "Cross-platform fleet execution between Windows and macOS: one DeepSeek Harness account, a Rabbit relay, and signed device identity. Ships four Agent Skills describing fleet dispatch, field-level memory acceptance, session references, and resource scheduling.",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "agent-skills",
9
+ "fleet",
10
+ "multi-machine",
11
+ "w2m",
12
+ "dsh"
13
+ ]
14
+ }
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: fleet-control
3
+ description: Use when dispatching a command to the W2M fleet, choosing between replicate, split, broadcast and pipeline, interpreting a verdict's aggregate state or a per-machine refusal_reason, or diagnosing why a task was partial instead of consistent. Covers the four modes, the anchors that decide comparability, and every refusal code a dispatch can return.
4
+ ---
5
+
6
+ # Fleet control
7
+
8
+ Dispatch one command to many machines and read the verdict honestly. The tools are `w2m_run`, `w2m_wait`,
9
+ `w2m_report`, `w2m_devices`, `w2m_stats`, `w2m_history`, `w2m_status`.
10
+
11
+ ## The relay carries one exit code and no output
12
+
13
+ **This is the single fact that changes how you read every result.** A result envelope carries
14
+ `exit_code`, `stdout_sha256`, `stdout_bytes`, `stdout_normalized_sha256`, `stderr_sha256`, and `artifacts` —
15
+ **never stdout itself.** So:
16
+
17
+ - You can compare two machines byte-for-byte and you cannot read what either printed.
18
+ - To get a value back, encode it in the **exit code**. One bit per question, eight bits per exit code.
19
+ - To get bytes back, write a file and declare it in `artifacts`.
20
+
21
+ Do not write a task that prints its answer and expect to read it. **The verdict is the product.**
22
+
23
+ ## Pick the mode by what you mean, not by how many machines answer
24
+
25
+ | Mode | Meaning | `index_total` |
26
+ |---|---|---|
27
+ | `replicate` | every machine runs the whole command, results are compared | must be 1 |
28
+ | `split` | each machine takes its share by index | the number of slices |
29
+ | `broadcast` | **one** machine runs it; the rest are told the outcome and never run anything | 1 |
30
+ | `pipeline` | every machine runs the whole chain in order, stopping at the first failing stage | 1 |
31
+
32
+ `broadcast` needs `executor_machine_id`. Without it the first machine the relay lists is chosen
33
+ deterministically — **so the same task record describes the same execution twice**, which is the point.
34
+
35
+ **`broadcast`'s observers are recorded as `observing` and are never counted as having verified the result.**
36
+ If you want N machines to agree, you want `replicate`, not `broadcast`.
37
+
38
+ **A single command is wrapped as a one-stage chain and run through the pipeline loop.** There is exactly one
39
+ spawn path, so a mode-specific claim about `cwd` or argv must hold for the chain form too.
40
+
41
+ ## Read the verdict before you act on it
42
+
43
+ Nine states, and **the difference between two of them is the difference between "it ran" and "it never
44
+ started"**:
45
+
46
+ | State | Means |
47
+ |---|---|
48
+ | `consistent` | every machine that answered agreed |
49
+ | `divergent` | machines answered, and their comparable fields differ |
50
+ | `divergent-platform` | **the differences are fully explained by OS/arch** — not a defect |
51
+ | `partial` | some machines answered and others did not |
52
+ | `failed` | a machine ran the command and it returned non-zero |
53
+ | `unverifiable` | the comparison could not be made |
54
+ | `timeout` | the wait elapsed |
55
+ | `cancelled` | an operator cancelled it |
56
+ | `degraded` | the relay reports a reduced condition |
57
+
58
+ **`exit_code: null` with `lease_state: expired` means the machine never ran it.** `exit_code` non-zero with
59
+ `lease_state: done` means it ran and failed. **A non-zero exit is a result; an expiry is an absence.**
60
+
61
+ ## Refusals are values, not exceptions
62
+
63
+ A refusal is a machine declining before execution, and it is reported per machine:
64
+
65
+ | `refusal_reason` | Means | Your move |
66
+ |---|---|---|
67
+ | `COMMAND_NOT_ALLOWED` | the entry is not in **that machine's** allow-list | check the allow-list; do not retry blindly |
68
+ | `COMMAND_REPARSING_ARGUMENT` | an argument would make the interpreter load code | name the flag in an entry of its own, or stop |
69
+ | `COMMAND_EXECUTABLE_PATH_MISMATCH` | `argv[0]` is not the executable that entry names | call the entry by the name it declares |
70
+ | `COMMAND_EXECUTABLE_RELATIVE_PATH` | `argv[0]` is a relative path | it names a different file in every directory, which is why it is refused |
71
+ | `CWD_OUTSIDE_PROJECT` | `cwd_rel` escapes the project | pass `cwd_outside_project: true` **only** when that is intended |
72
+ | `PATH_INVALID` | no such cwd | **A missing directory is reported as a missing executable** — read the detail, not just the name |
73
+
74
+ **Allow-lists are per machine.** The same command can be allowed on one host and refused on another in the
75
+ same task, and that is a `partial` verdict rather than a bug.
76
+
77
+ ## Anchors decide whether the comparison was meaningful
78
+
79
+ `base_commit`, `base_tree`, and `command_hash` are computed from the dispatcher's working tree; each machine
80
+ reports its own `pre_tree_fingerprint`. **When the dispatcher's own tree cannot be fingerprinted, no anchor is
81
+ declared and the note says so** — agreement is then agreement *with each other*, not with a named tree.
82
+
83
+ **Read that note.** A `consistent` verdict with no anchor is weaker than it looks, and `w2m_report` states
84
+ which case you are in.
85
+
86
+ ## Before dispatching
87
+
88
+ 1. `w2m_devices` — who is actually online. **A machine listed may still refuse the next command.**
89
+ 2. `w2m_stats` — the cheap shape of the fleet, and whether the relay restarted.
90
+ 3. Choose the mode from the *meaning*, not the machine count.
91
+ 4. After the wait, read `counts`, `notes`, and every `refusal_reason` **before** reading the aggregate state.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: memory-sync
3
+ description: Use when writing or reading a block through w2m_memory, when a field was omitted rather than nulled, when a block names an ancestor the caller cannot produce, or when deciding whether a fact belongs in durable memory at all. Covers field-level acceptance, the per-actor cost of each field, and the refusal codes that mean "kept, but not for you".
4
+ ---
5
+
6
+ # Memory sync
7
+
8
+ `w2m_memory` stores blocks that carry **provenance**, and it decides field by field what each reader may see.
9
+ The tool is small; the rules are the part that matters.
10
+
11
+ ## Acceptance is per field, and omission is not null
12
+
13
+ A block is a set of named fields. When an actor's authority cannot cover a field, **the field is absent from
14
+ what they receive — it is not present-with-`null`, and it is not an error.**
15
+
16
+ ```
17
+ accepted = [subject, body] refused = [evidence]
18
+ hasOwnProperty('evidence') === false
19
+ ```
20
+
21
+ **Test for the key, not for a falsy value.** `if (!block.evidence)` cannot tell "omitted" from "empty", and
22
+ those mean different things: one is a permission decision, the other is content.
23
+
24
+ **Never reconstruct a refused field.** If you need it, the answer is a higher-authority actor or a different
25
+ block, not a guess.
26
+
27
+ ## A block cannot name an ancestor its caller cannot produce
28
+
29
+ Providing a block whose `parent` is not in the caller's own reachable set is refused as `BLOCK_MISSING_PARENT`,
30
+ and **the block is rejected rather than re-rooted.**
31
+
32
+ **This is deliberate.** Silently re-rooting would make the ancestry claim unfalsifiable — every block would
33
+ "fit" somewhere, and the chain would stop carrying information. When you hit this refusal, supply the missing
34
+ ancestor or drop the claim; do not look for a flag that relaxes it.
35
+
36
+ ## Cost is a property of the field, not of the request
37
+
38
+ Authority bands, and what they reach:
39
+
40
+ | Band | Range | Reaches |
41
+ |---|---|---|
42
+ | `ordinary` | 0–49 | the low-cost fields |
43
+ | `captain` | 50–69 | more, including some evidence |
44
+ | `org_admin` | 70–89 | `evidence` and above |
45
+ | `system_admin` | 90+ | everything |
46
+
47
+ **A field's cost is declared where the field is declared**, and the acceptance decision is made against the
48
+ actor's level at write and at read. So the same block read by two actors is two different objects, and that is
49
+ the feature rather than a leak.
50
+
51
+ Do not design a workflow that depends on a high-authority actor being present to make a low-authority read
52
+ work. **Model the read you actually need.**
53
+
54
+ ## Writing: say what you can prove
55
+
56
+ - **Name the ancestor only if you hold it.** A parent you cannot produce fails the whole block.
57
+ - **Put the load-bearing fact in the cheapest field that can carry it.** A fact in `evidence` is invisible to
58
+ everyone below `org_admin`, so a block whose meaning lives in `evidence` means nothing to most readers.
59
+ - **One block, one claim.** Ancestry is how blocks relate; packing two claims into one block makes each
60
+ unreadable to whoever may see only one of them.
61
+ - **Expect omission to be silent.** Nothing in the result says "a field was hidden from you" beyond the field
62
+ being absent, so a reader who needs a field must check for it.
63
+
64
+ ## Before you write
65
+
66
+ 1. What is the **cheapest field** that can carry this fact?
67
+ 2. Do you **hold the parent** you are about to name?
68
+ 3. Who needs to read this, and **what band do they hold**? If the answer is "everyone", the fact does not
69
+ belong in a costly field.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: resource-schedule
3
+ description: Use when deciding how to consume the fleet's capacity across machines, when a task came back partial because a machine was offline, when a lease expired instead of failing, or when a long command needs a timeout that fits inside the agent's execution ceiling. Covers lease states, the ~50s agent ceiling, w2m_update's schedule, and why an expiry is not a failure.
4
+ ---
5
+
6
+ # Resource scheduling
7
+
8
+ The fleet is a small pool of heterogeneous machines, and most disappointing results come from treating it as
9
+ a queue of identical workers.
10
+
11
+ ## Lease states are the scheduling vocabulary
12
+
13
+ | State | Means | Retry? |
14
+ |---|---|---|
15
+ | `queued` | handed to the machine | — |
16
+ | `offered` | published, not yet taken | — |
17
+ | `done` | **the command ran.** `exit_code` is meaningful, including non-zero | no |
18
+ | `refused` | the machine declined before running; read `refusal_reason` | only after fixing the cause |
19
+ | `expired` | **nobody ran it.** `exit_code` is `null` | yes — the machine was not there |
20
+ | `observing` | `broadcast` chose another executor; this machine was told, not asked | no, by design |
21
+
22
+ **`expired` and `failed` are different findings with different responses.** An expiry says the fleet lacked
23
+ capacity; a failure says the command was wrong. Aggregating them into "it didn't work" loses the only
24
+ information that decides what to do next.
25
+
26
+ ## The ~50 second ceiling, measured repeatedly
27
+
28
+ **An agent-side execution has a ceiling near 50 seconds, and it has been hit more than eight times in a single
29
+ working session.** Symptoms are an expiry rather than an error, so it reads as a missing machine.
30
+
31
+ Consequences for scheduling:
32
+
33
+ - **Keep a dispatched command under the ceiling**, or expect the lease to expire and the task to come back
34
+ `partial`.
35
+ - `--timeout` and `timeout_ms` bound **the whole pipeline chain, not each stage.** Three 20-second stages
36
+ inside a 50-second timeout will be cut off at the third.
37
+ - **A long job belongs on one machine with a written artifact, not replicated.** Replicating a job that
38
+ outlives the ceiling multiplies the expiries by the machine count.
39
+
40
+ ## Offline is the normal case, not an error
41
+
42
+ `w2m_devices` lists machines the relay holds a lease for; `w2m_stats` reports how many are online.
43
+ **These two have disagreed in practice** — a machine can be listed and answer `silent`. Treat "online" as
44
+ "worth trying", and read the per-machine outcome afterwards.
45
+
46
+ **Partial is therefore the expected verdict for any replicated task on a fleet with offline members**, and it
47
+ is not a signal that the command was wrong. Check `counts.expired` before concluding anything about the code.
48
+
49
+ ## Time is not the only scarce resource
50
+
51
+ - **One writable lease at a time** is available via `require_exclusive_write`. Two machines writing the same
52
+ paths without it is a race the relay will not prevent for you.
53
+ - **`write_scope` is project-relative prefixes.** A write outside the declared scope is not made, so a task
54
+ that needs to write must declare where.
55
+ - **Read-only is the default.** A dispatch that must not modify anything needs no flags — that is the default,
56
+ and `write: true` is the deliberate opt-in.
57
+
58
+ ## Scheduled work in this system
59
+
60
+ - **Update checks run at 00:00, 03:00 and 05:00 Beijing time**, and a new version is installed **only after
61
+ verifying the hash published with it**. A check that cannot reach the network comes back as a **recorded
62
+ error**, which is deliberately different from "already up to date" — **do not read a negative result as a
63
+ positive one.**
64
+ - **An install takes effect at the next start.** Nothing hot-swaps, and `w2m_status` reports
65
+ `restart_required` so the deferral is visible rather than silent.
66
+
67
+ ## Scheduling checklist
68
+
69
+ 1. `w2m_devices` or `w2m_stats` — **how much capacity actually exists right now.**
70
+ 2. Estimate the runtime against **the ~50s ceiling**, and reshape the work if it exceeds it.
71
+ 3. Choose `replicate` only when you need agreement; otherwise pick the smallest set that answers the question.
72
+ 4. After the wait, read `counts` first: `expired` is capacity, `failed` is the command, `refused` is policy.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: session-ref
3
+ description: Use when a session must be named across tools, when a w2m tool takes a session_id, when constructing or parsing a dsh-session: URI, or when the error "invalid session reference URI" appears. Covers the canonical encoding, the three checks a URI must pass, and which per-session state the W2M plugin keys by session id.
4
+ ---
5
+
6
+ # Session references
7
+
8
+ Several things in this system are **per session**, and naming a session is where they break when a caller
9
+ guesses at the format.
10
+
11
+ ## The canonical URI is not the session id with a prefix
12
+
13
+ `dsh-session:` carries a **base64url-encoded JSON string**, not the raw id:
14
+
15
+ ```
16
+ dsh-session: + base64url(JSON.stringify(sessionId))
17
+ ```
18
+
19
+ **A DSH session id is a UUID** (36 characters), so the encoded form is its JSON string quoted and re-encoded —
20
+ **it looks nothing like the id.** Writing `dsh-session:` + the raw UUID produces a URI that fails decoding.
21
+
22
+ To build one, call `formatSessionReferenceMention({ sessionId, label })` from
23
+ `@deepseek-ai/dsh-session-reference`. **Never concatenate the scheme by hand.**
24
+
25
+ ## Three checks, and the error names none of them
26
+
27
+ `decodeSessionReferenceUri` rejects a URI that fails any of:
28
+
29
+ 1. it does not start with `dsh-session:`;
30
+ 2. the payload is not `/^[A-Za-z0-9_-]+$/`;
31
+ 3. the payload does not decode to a **JSON string**, or **re-encoding it does not reproduce the URI
32
+ byte-for-byte**.
33
+
34
+ All three throw the same `invalid session reference URI "<uri>"` with code
35
+ `SESSION_REFERENCE_INVALID_REFERENCE`. **The message names the URI and never the check**, so diagnose by
36
+ re-encoding: take the decoded value, encode it again, and compare.
37
+
38
+ **Check 3 is the one that surprises people.** It requires the *canonical* encoding, not merely a decodable
39
+ one — base64url has several spellings for the same bytes, and a non-canonical one is refused even though it
40
+ decodes correctly.
41
+
42
+ ## Bare URIs are extracted too, and that is a real trap
43
+
44
+ The text pattern matches two forms:
45
+
46
+ ```
47
+ @[label](dsh-session:payload) an explicit Markdown mention -- fails on ANY malformed URI
48
+ dsh-session:payload a bare URI -- taken as a reference when the payload is base64url-shaped
49
+ ```
50
+
51
+ **So a `dsh-session:`-looking string inside ordinary prose is parsed as a reference**, and in an explicit
52
+ mention a malformed URI is a hard failure rather than a fallback to text. **If you are quoting a URI —
53
+ in a changelog, a bug report, an example — quote it in a fenced code block or break the scheme**, or a parser
54
+ downstream will try to resolve it.
55
+
56
+ ## Per-session state in this plugin
57
+
58
+ `w2m_run` accepts an optional `session_id`, and the fleet-target store keys the session's chosen machine by it:
59
+
60
+ - **The bucket is the session id**, so one chat's target cannot silently steer another chat's dispatch.
61
+ - **A dispatch with no session in reach falls back to a single `(no-session)` bucket** rather than throwing —
62
+ so a bare `execute()` in a test and a future non-session caller share one bucket instead of two.
63
+ - `session_id` is capped at **200** characters, which exists to bound the number of buckets, not the bytes.
64
+ A DSH session id is 36 characters, so the cap is room to spare rather than a limit you should approach.
65
+
66
+ **Pass the id you actually hold.** A fabricated id creates a bucket nothing else will ever read, which looks
67
+ like a target that silently fails to apply.
68
+
69
+ ## Diagnosing an invalid-reference error
70
+
71
+ 1. **Find where the URI was built.** If it was `'dsh-session:' + id`, that is the bug — build it with the
72
+ codec instead.
73
+ 2. **Re-encode and compare.** Decode the payload, `JSON.stringify` it, `base64url` it, and compare to the
74
+ original. A difference means check 3.
75
+ 3. **Look for prose.** A URI quoted in a message body is extracted as a bare reference, so the failure may be
76
+ nowhere near the code that handles sessions.
@@ -208,7 +208,7 @@ const MAX_STAGES = 16;
208
208
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
209
209
  * overrides it at runtime for tests and packagers.
210
210
  */
211
- const PLUGIN_VERSION = '0.50.0';
211
+ const PLUGIN_VERSION = '0.53.0';
212
212
 
213
213
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
214
214
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;