@twinsearth/w2m-dsh-plugin 0.50.0 → 0.52.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,134 @@ 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.52.0] — 2026-10-15
8
+
9
+ **Agent Plugins 1.0.0 manifests, validated against the published schemas — and the honest note about who
10
+ reads them.**
11
+
12
+ ### What shipped
13
+
14
+ ```
15
+ plugin.json root manifest, Agent Plugins 1.0.0, $schema pinned to the canonical identifier
16
+ mcp.json schema-valid with an EMPTY server map -- see below
17
+ skills/ unchanged; the four SKILL.md files were already the portable form
18
+ ```
19
+
20
+ `scripts/plugin-manifest-check.mjs` validates both, `pre-flight` gained a fourteenth check, and
21
+ `test/agent-plugins.test.mjs` adds fourteen assertions across both manifests and the skills.
22
+
23
+ ### The schemas were read, and the constraints are written out rather than fetched
24
+
25
+ `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json` and `.../mcp.schema.json` are both live, and the
26
+ rules come from them — including the `name` pattern `^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$`,
27
+ **which refuses `--` and `..` and which I would not have guessed.** They are **not fetched at check time**:
28
+ a validator that needs the network cannot run offline, and one that skips itself when the fetch fails reports
29
+ success for the case it could not measure.
30
+
31
+ **What this cannot check, stated rather than implied:** the schema's own description says *"the Agent Plugins
32
+ specification defines additional semantic and operational requirements."* Path containment, symlink
33
+ resolution, MCP transport support and the `PLUGIN_ROOT`/`PLUGIN_DATA` expansion rules are **not** expressible
34
+ here and **are not claimed**.
35
+
36
+ ### `mcp.json` ships with zero servers, and that is the honest version
37
+
38
+ The request specified MCP as the unified calling protocol. **DSH has no MCP client.** Measured across all
39
+ 8543 small code and config files in the application bundle: `plugin.json` 0, `mcp.json` 0, `agent-plugins` 0,
40
+ **and `mcpServers` 0 occurrences in host-owned code** — the 202 hits are third-party SDKs inside
41
+ `node_modules`, which support MCP without this host reading a config for it.
42
+
43
+ **And there is no MCP server in this package.** So the file is schema-valid with an empty map: a portable
44
+ client reads it and starts nothing, which is true. **The alternative — declaring a server that does not
45
+ exist — is a manifest that launches a failure**, and shipping that to satisfy a specification would be the
46
+ same mistake as advertising a capability the code lacks.
47
+
48
+ ### The one rule that is not from a schema caught me immediately
49
+
50
+ `plugin.json`'s `version` must equal `package.json`'s. **On the validator's first run it failed: the manifest
51
+ said 0.52.0 and `package.json` still said 0.51.0.** That is the whole justification for the rule — DSH loads
52
+ plugins through `package.json#dsh` and a Cordis bundle patch, **so nothing in this host ever reads
53
+ `plugin.json`.** A manifest with no consumer has no way to notice it is stale.
54
+
55
+ **A file nobody reads is a file that cannot be wrong.** The schema check and the drift check are the only
56
+ things standing between this manifest and quiet decay.
57
+
58
+ ### Four assertions that were wrong, mine
59
+
60
+ * **`frontmatter('---\nname: a\n')` returns `null`, not `{ data: undefined }`** — an unclosed block is not
61
+ frontmatter, and my assertion demanded the shape the code does not return. **The code was right.**
62
+ * **I typed `disableModelInvoction`** in the rejected-spelling set. **A misspelled name in a rejection list
63
+ is a name that is not rejected** — the exact failure the list exists to prevent.
64
+ * **I exported a contract object above the constants it read**, so importing the module threw
65
+ `Cannot access 'PLUGIN_FIELDS' before initialization`.
66
+ * **I wrote the `implementations` Map entry as `['name']: () => …`** inside an array of pairs, matching an
67
+ anchor line without reading the structure around it. **This is the third time in this project that an
68
+ edit matched an anchor string instead of the block containing it.**
69
+
70
+ Both validators now export their rules and keep the CLI in `main()`, so **the suite asserts them without
71
+ spawning a child** — a test that needs a subprocess is skipped wherever piped stdio is refused, which is
72
+ exactly where a check must still run.
73
+
74
+ ## [0.51.0] — 2026-10-15
75
+
76
+ **Four Agent Skills, written against the loader's real contract — and a gate that makes the contract matter.**
77
+
78
+ ### The four skills
79
+
80
+ ```
81
+ skills/
82
+ ├── fleet-control/SKILL.md the four modes, the nine verdicts, every refusal code
83
+ ├── memory-sync/SKILL.md field-level acceptance, omission versus null, ancestry
84
+ ├── session-ref/SKILL.md the dsh-session: codec, its three checks, its silent trap
85
+ └── resource-schedule/SKILL.md lease states, the ~50s ceiling, what "expired" means
86
+ ```
87
+
88
+ **Every rule in them was measured before it was written.** The refusal codes are the ones this session
89
+ actually produced, the ~50-second ceiling is one that was hit more than eight times, and the `dsh-session:`
90
+ section describes a codec whose three checks were read out of `@deepseek-ai/dsh-session-reference` — **including
91
+ the one that surprises people, where a payload that decodes correctly is still refused for not being
92
+ *canonical*.**
93
+
94
+ ### The contract came from the loader, not from a specification
95
+
96
+ `parseSkillFile` reads **`name`** and **`description`**, tests the name against
97
+ `/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, and **rejects three renamed spellings outright** —
98
+ `disableModelInvocation`, `modelInvocable`, `userInvocable` — in favour of the hyphenated forms. On any
99
+ failure it logs a warning and **returns**.
100
+
101
+ **That is the whole reason this ships with a gate.** A skill that violates the contract is not an error: it
102
+ simply never appears, the directory still looks shipped, and nothing is red. **It is a file whose only symptom
103
+ of being wrong is not being there.**
104
+
105
+ ### New check: `agent skills`, and the twelfth became the thirteenth
106
+
107
+ `scripts/skill-check.mjs` mirrors the loader's rules, and `pre-flight` now runs it.
108
+
109
+ ```
110
+ PASS agent skills (frontmatter satisfies the loader, so no skill is silently dropped): 4 skill(s) satisfy the loader contract
111
+ pre-flight: 13 pass, 0 fail, 1 unverifiable (13/13 registered check(s) reported) -- GREEN
112
+ ```
113
+
114
+ It is bound in **three** places — `DECLARED_CHECKS`, `GATE_INVENTORY` (five artifact rows: the validator and
115
+ the four `SKILL.md` files), and the implementation map — because this repository's own rule is that a
116
+ declared check with no artifact, or an artifact with no check, is red in both directions.
117
+
118
+ ### Two mistakes of mine, both worth keeping visible
119
+
120
+ **The check reported `exit null` and nothing else.** The cause was `result.error` — an `EPERM` from
121
+ `spawnSync` under a `workspace-write` sandbox that had replaced the `danger-full-access` of earlier runs. **The
122
+ code was right and the environment was refusing.** But the diagnosis was bad on its own terms: **a check that
123
+ prints an exit status and drops the reason names nothing**, so the failure now says
124
+ `EPERM: spawnSync …` and the next reader does not have to rediscover the sandbox boundary.
125
+
126
+ **I edited the `implementations` Map into invalid syntax** — `['name']: () => …` inside an array of pairs —
127
+ because I matched an anchor line without reading the structure it sat in. **This is the same error shape as
128
+ the `accept_block` branch in 0.41.0, and it is twice now.**
129
+
130
+ **The skills ship** — `skills` is in `package.json#files` beside `docs`, so the four `SKILL.md` files are in
131
+ the tarball. **They load only when a host points `DSH_BUNDLED_SKILL_DIR` at the directory**, which is a host
132
+ setting rather than something the package can assert; the alternative roots are `<project>/.dsh/skills`,
133
+ `<project>/.agents/skills`, `<dshHome>/skills` and `~/.agents/skills`.
134
+
7
135
  ## [0.50.0] — 2026-10-15
8
136
 
9
137
  **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.52.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/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/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.52.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.52.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;