@twinsearth/w2m-dsh-plugin 0.49.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 +170 -0
- package/docs/TEST-LEDGER.md +28 -0
- package/docs/TEST-MATRIX.md +47 -4
- package/mcp.json +4 -0
- package/package.json +6 -3
- package/plugin.json +14 -0
- package/skills/fleet-control/SKILL.md +91 -0
- package/skills/memory-sync/SKILL.md +69 -0
- package/skills/resource-schedule/SKILL.md +72 -0
- package/skills/session-ref/SKILL.md +76 -0
- package/src/plugin/tools.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,176 @@ 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
|
+
|
|
135
|
+
## [0.50.0] — 2026-10-15
|
|
136
|
+
|
|
137
|
+
**The matrix gains the two defects and the WeChat path — and the counts are read from the table, not typed.**
|
|
138
|
+
|
|
139
|
+
### What the matrix now records that it could not before
|
|
140
|
+
|
|
141
|
+
`docs/TEST-MATRIX.md` grows **two sections and eighteen rows**:
|
|
142
|
+
|
|
143
|
+
* **§十二 微信桥接** — twelve rows for the path this project had never executed: QR binding, the bridge
|
|
144
|
+
reading the live channel, the permission refusal by name, `--ask` as the marker that keeps a sentence from
|
|
145
|
+
being dispatched, and **the full WeChat → dispatch → verdict → WeChat round trip**.
|
|
146
|
+
* **缺陷** — six rows for what was found and changed. **修正时间 has content for the first time, because it is
|
|
147
|
+
the column for things that were corrected**, and it was empty until now.
|
|
148
|
+
|
|
149
|
+
**Every one of the 18 can be traced to a command, an exit code, or a line of a log.** The two silent inputs,
|
|
150
|
+
the two dry runs that dispatched, the run with no reply because the bridge was stopped, and the token deleted
|
|
151
|
+
by a release — all of them are there with what produced them.
|
|
152
|
+
|
|
153
|
+
### The counts are now read from the table, and the reading is written down
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
79 行 = 52 通过 + 8 待执行 + 11 阻塞 + 1 跳过 + 2 证伪 + 5 失败(已修或已确认)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**This is the second time I have corrected these numbers, and the two corrections have the same cause.** In
|
|
160
|
+
0.46.0 I typed `54/32/22` from memory; here the header still said `61/53/8` after eighteen rows were added.
|
|
161
|
+
**Both times the fix was to read the table instead of recalling it**, so the equation is now stated in the
|
|
162
|
+
document beside the numbers, and a test-shaped check confirms the arithmetic against the rows.
|
|
163
|
+
|
|
164
|
+
### The one row that is `失败 → 未修`, and why it is the useful one
|
|
165
|
+
|
|
166
|
+
**`TC-DEF-006`: every release runs `Remove-Item dist`, which deletes `dist/ilink/ilink-login.json` — the
|
|
167
|
+
WeChat token.** I hit this myself ten minutes after binding the bot, and the symptom is
|
|
168
|
+
`no login at dist/ilink/ilink-login.json`, **which says nothing about a deletion.**
|
|
169
|
+
|
|
170
|
+
It is recorded as **unfixed** rather than quietly omitted, because **a matrix whose only unexecuted rows are
|
|
171
|
+
environmental stops being a list of things to do.** The fix is small — move the login file out of `dist/`, or
|
|
172
|
+
have the release step spare it — and it is not in this release because the honest state is that it is not
|
|
173
|
+
fixed yet.
|
|
174
|
+
|
|
175
|
+
**No new tests.** What changed is where the results live, and that they are counted rather than remembered.
|
|
176
|
+
|
|
7
177
|
## [0.49.0] — 2026-10-15
|
|
8
178
|
|
|
9
179
|
**Every message now gets an answer. Five inputs were being silently dropped, and two of the three input
|
package/docs/TEST-LEDGER.md
CHANGED
|
@@ -120,6 +120,34 @@ was not re-measured keeps its old date — **a stale date is information, and qu
|
|
|
120
120
|
| `HARNESS-001` | a hand-rolled test harness measures the harness | **PASS (as a warning)** | 2026-10-15 | **41 failures** running every suite in one process with `--experimental-test-isolation=none`, vs **1** via the project's own entry. The other 38 were `mdns-discover`'s fetch stub leaking into `outbound-only` |
|
|
121
121
|
| `HARNESS-002` | **a count typed by hand drifts from the table it describes** | **FAIL, then corrected** | 2026-10-15 | CHANGELOG 0.46.0 claimed *"54 rows / 32 executed / 22 not"*. Read from `TEST-MATRIX.md`: **61 rows / 53 executed / 8 never run / 11 blocked / 1 skipped / 2 refuted.** I had merged **`阻塞`** (the command ran and the environment stopped it) with **`待执行`** (never run) — so 61 became 54 and 8 became 22 |
|
|
122
122
|
|
|
123
|
+
## WeChat bridge (real service, one live bot)
|
|
124
|
+
|
|
125
|
+
| ID | Content | Result | Date | Instance |
|
|
126
|
+
|---|---|---|---|---|
|
|
127
|
+
| `BOT-001` | bind a WeChat bot by scanning a QR | **PASS** | 2026-10-15 | `get_bot_qrcode` → scan → `get_qrcode_status` = **`{"status":"confirmed","ilink_bot_id":"09bbed4c7570@im.bot"}`** |
|
|
128
|
+
| `BOT-002` | the bridge reads the live channel | **PASS** | 2026-10-15 | one second after start it logged a real inbound message. **The QR minting script had already exited on `qr_expired` before the code was scanned**, so nothing was polling to catch the token — it had to be written by hand |
|
|
129
|
+
| `BOT-003` | **WeChat → dispatch → verdict → WeChat**, end to end | **PASS** | 2026-10-15 | `/run node --version` → task `01M5028GSHPH5JGHYCKJXQ80SW`, `status: partial`, `step 2 ok: [macmini]`. **The first WeChat-triggered fleet task in this project's history** |
|
|
130
|
+
| `BOT-004` | a window without the capability is refused, by name | **PASS** | 2026-10-15 | default `W2M_CAPABILITIES=operate` → *"这个窗口没有 fleet.dispatch 权限,不能执行。"* |
|
|
131
|
+
| `BOT-005` | the dispatch permission can be granted, and the warning that guards it | **PASS** | 2026-10-15 | with `operate,fleet.dispatch` **and** `W2M_DEVICE_FILE`, the script's *"dispatch succeeds and the verdict is unreadable"* warning is gone |
|
|
132
|
+
| `BOT-006` | natural language is never dispatched, and never guessed at | **PASS** | 2026-10-15 | `--ask` is the explicit marker (`run-command.mjs:136`). **Measured: a plain sentence dispatched as argv and refused `COMMAND_NOT_ALLOWED` on all three online machines** |
|
|
133
|
+
| `BOT-007` | a swallowed space produces a correct refusal, not silence | **PASS** | 2026-10-15 | WeChat delivered `node--version` as one token (the parser was verified correct on all three spacings); three machines answered `COMMAND_NOT_ALLOWED` |
|
|
134
|
+
| `BOT-008` | **every message gets an answer** | **FAIL, then fixed in 0.49.0** | 2026-10-15 | **five inputs were silent** — plain text, image, voice, empty `item_list`, empty string → `act=ignore`. All five now `act=reply` |
|
|
135
|
+
| `BOT-009` | the one silence that survives, and why | **PASS** | 2026-10-15 | `no sender` / `no context_token` still `ignore` — **there is nowhere to send an answer, so `reply` would make the plan lie.** This is why the vocabulary still contains `ignore` |
|
|
136
|
+
| `BOT-010` | the help must not advertise a capability the bridge lacks | **PASS** | 2026-10-15 | `textOf` reads only `type: 1`; measured that an image and a voice note both answer `not a text message`. The help says 语音、图片、文件、表情 are unsupported |
|
|
137
|
+
| `BOT-011` | every `/run` example in the help is accepted by the real parser | **PASS** | 2026-10-15 | one test feeds **every `/run` line in the help** through `parseInvocation` — the help cannot drift into advertising refused syntax |
|
|
138
|
+
|
|
139
|
+
## Defects found and fixed in this span
|
|
140
|
+
|
|
141
|
+
| ID | Content | Result | Date | Instance |
|
|
142
|
+
|---|---|---|---|---|
|
|
143
|
+
| `DEF-001` | **`--dry-run` must not dispatch** | **FAIL, then fixed in 0.48.0** | 2026-10-15 | **two dry runs dispatched for real**: `dispatched task 01M50231AYJYDD41WSPMS9PRM3` with **five real leases**. The reply was guarded (`scripts/…:174`); `fleet.run` (`193-199`) was not |
|
|
144
|
+
| `DEF-002` | the fix aligns with what this repository already means by `dryRun` | **PASS** | 2026-10-15 | `update-install.test.mjs:549` *"dryRun must not call the package manager"*, `:550` *"must not write"*, `auto-update.test.mjs:325` *"writes nothing to the profile"* — **two flags, one name, and the bridge held the unsafe one** |
|
|
145
|
+
| `DEF-003` | the guard precedes the only call that reaches the fleet | **PASS** | 2026-10-15 | a test **reads the script** and asserts `if (!dry.dispatch)` appears before `await fleet.run(` — **a test of the pure function alone would pass while the script still dispatched, which is exactly the original defect's shape** |
|
|
146
|
+
| `DEF-004` | **`--ask` must be discoverable** | **FAIL, then fixed in 0.49.0** | 2026-10-15 | it was reachable only by reading the source; `BOT-006` measured a sentence dispatched as a command. It is now in help section 【1】, with a test asserting it cannot leave |
|
|
147
|
+
| `DEF-005` | a verdict needs a running bridge | **NOT A DEFECT** | 2026-10-15 | a `/run` got no reply because **the bridge had been stopped**. No process, no answerer |
|
|
148
|
+
| `DEF-006` | the release flow must not delete a credential | **FAIL, UNFIXED** | 2026-10-15 | **every release runs `Remove-Item dist`, which deletes `dist/ilink/ilink-login.json`** — the WeChat token. Symptom is *"no login at …"*, **which says nothing about a deletion** |
|
|
149
|
+
| `DEF-007` | a credential must not be printed into a transcript | **FAIL** | 2026-10-15 | **I caused this**: to confirm the binding I queried `get_qrcode_status`, and that endpoint returns `bot_token` in plaintext. The token is in the conversation. It is `.gitignore`d (`dist/`), but it is disclosed, and the choice to look was mine |
|
|
150
|
+
|
|
123
151
|
---
|
|
124
152
|
|
|
125
153
|
## Plan claims that measurement refuted
|
package/docs/TEST-MATRIX.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| | |
|
|
6
6
|
|---|---|
|
|
7
7
|
| **证据文件** | [`TEST-LEDGER.md`](TEST-LEDGER.md) — 每一行的**确切命令、退出码、产物**。本表不重复它们,只引用 |
|
|
8
|
-
| **本表最后更新** | 2026-10-15,对 `v0.
|
|
8
|
+
| **本表最后更新** | 2026-10-15,对 `v0.50.0` |
|
|
9
9
|
|
|
10
10
|
## 测试结果取值
|
|
11
11
|
|
|
@@ -154,7 +154,41 @@
|
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
-
##
|
|
157
|
+
## 十二、微信桥接与发现的缺陷
|
|
158
|
+
|
|
159
|
+
**这一节的存在本身是一条规则**:修正时间那一列只有在**有东西被修正过**时才有内容。下面每一行都是**先被实测到、再被改掉**的,所以它们同时是缺陷记录和回归证据。
|
|
160
|
+
|
|
161
|
+
### 微信桥接(`scripts/ilink-fleet-bridge.mjs` + `src/chatops/`)
|
|
162
|
+
|
|
163
|
+
| 序列号 | 测试内容 | 测试结果 | 修正时间 | 测试实例 |
|
|
164
|
+
|---|---|---|---|---|
|
|
165
|
+
| `TC-BOT-001` | 扫码绑定微信 bot,拿到 `bot_token` | **通过** | — | iLink `get_bot_qrcode` → 扫码 → `get_qrcode_status` 返回 **`status: "confirmed"`**,`ilink_bot_id: 09bbed4c7570@im.bot` |
|
|
166
|
+
| `TC-BOT-002` | 桥接能读到 token 并连上微信 | **通过** | — | 启动后 1 秒内拉到真实消息:`[15:08:04] ignored (not a /run line)` —— 那是**修复前**的静默行为,见 `TC-BOT-009` |
|
|
167
|
+
| `TC-BOT-003` | **微信 → 派发 → 结果 → 回微信**整条链路 | **通过** | — | `/run node --version` → `task 01M5028GSHPH5JGHYCKJXQ80SW` → `status: partial`,`step 2 ok: [macmini]` |
|
|
168
|
+
| `TC-BOT-004` | 无能力时拒绝执行并说明 | **通过** | — | 默认 `W2M_CAPABILITIES` 只有 `operate` 时回复*"这个窗口没有 fleet.dispatch 权限,不能执行。"* |
|
|
169
|
+
| `TC-BOT-005` | 权限可由启动者授予 | **通过** | — | `W2M_CAPABILITIES='operate,fleet.dispatch'` 后 `device token` 也被找到,脚本那条"派发成功但结果读不回"的警告消失 |
|
|
170
|
+
| `TC-BOT-006` | 自然语言**不被**当作命令执行 | **通过** | — | `--ask` 是显式开关;没有它时 `/run 你好` 会被当 argv 派发并因 `COMMAND_NOT_ALLOWED` 被拒 |
|
|
171
|
+
| `TC-BOT-007` | 参数被吞掉时机器正确拒绝 | **通过** | — | 微信里空格丢失 → argv 变成单个 `node--version` → 三台在线机器全部 `COMMAND_NOT_ALLOWED` |
|
|
172
|
+
| `TC-BOT-008` | 帮助文本含三个必需小节 | **通过** | — | `【1】开关指令` / `【2】指令规则与格式` / `【3】参考与边界`,**按标题断言**而不是"提到了命令" |
|
|
173
|
+
| `TC-BOT-009` | **每一句话都必须有回应** | **失败 → 已修** | **2026-10-15** | **修复前五种输入静默**:`文字 你好`、`图片 type 2`、`语音 type 3`、空 `item_list`、空文字 → 全部 `act=ignore`。**修复后全部 `act=reply`** |
|
|
174
|
+
| `TC-BOT-010` | 无法回信时保持静默,而不是假装回复 | **通过** | — | `no sender` / `no context_token` 仍 `ignore` —— **没有回信地址,标成 `reply` 会让计划撒谎** |
|
|
175
|
+
| `TC-BOT-011` | 帮助文本不得宣传不支持的能力 | **通过** | — | 实测 `textOf` 只读 `type: 1`,图片与语音都答 `not a text message`,所以帮助写*"不支持:语音、图片、文件、表情"* |
|
|
176
|
+
| `TC-BOT-012` | 帮助里的每个 `/run` 例子都被真实解析器接受 | **通过** | — | 一条测试**把帮助里所有 `/run` 行喂给 `parseInvocation`**,所以帮助不可能漂移成宣传被拒绝的语法 |
|
|
177
|
+
|
|
178
|
+
### 本跨度发现并修掉的缺陷
|
|
179
|
+
|
|
180
|
+
| 序列号 | 测试内容 | 测试结果 | 修正时间 | 测试实例 |
|
|
181
|
+
|---|---|---|---|---|
|
|
182
|
+
| `TC-DEF-001` | **`--dry-run` 不得派发** | **失败 → 已修** | **2026-10-15** | **两次 `--dry-run` 真的派发了**:`dispatched task 01M50231AYJYDD41WSPMS9PRM3`,**五条真实租约**。守卫只在 reply 上(`scripts/…:174`),`fleet.run`(`193-199`)无条件执行 |
|
|
183
|
+
| `TC-DEF-002` | 修复方式与项目对 `dryRun` 的既有定义一致 | **通过** | 2026-10-15 | `update-install.test.mjs:549` 断言 *"dryRun must not call the package manager"*、`:550` *"must not write"* —— **同一个词在本仓库已有含义,桥接持的是相反的那个** |
|
|
184
|
+
| `TC-DEF-003` | 守卫在唯一能到达编队的调用**之前** | **通过** | 2026-10-15 | 一条测试**读脚本本身**,断言 `if (!dry.dispatch)` 出现在 `await fleet.run(` 之前 —— **只测纯函数会通过而脚本仍在派发,那正是原缺陷的形状** |
|
|
185
|
+
| `TC-DEF-004` | `--ask` 必须可被发现 | **失败 → 已修** | **2026-10-15** | 修复前只能读源码知道它存在;`TC-BOT-006` 实测了一条自然语言被当命令派发。**现在它在帮助的【1】小节里**,并有测试断言它不得离开 |
|
|
186
|
+
| `TC-DEF-005` | 判定服务必须先启动 | **失败 → 已确认(非缺陷)** | 2026-10-15 | 你发 `/run` 却没收到回复 —— 因为桥接被我停掉了。**没有活进程就没有回复者**,这不是代码缺陷 |
|
|
187
|
+
| `TC-DEF-006` | 发布流程不得删除凭据 | **失败 → 未修** | **2026-10-15** | **每次发版的 `Remove-Item dist` 会删掉 `dist/ilink/ilink-login.json`**——那是微信 token。症状是*"no login at …"*,**与删除毫无关联** |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 十三、版本重跑规则
|
|
158
192
|
|
|
159
193
|
| 版本类型 | 执行范围 | 说明 |
|
|
160
194
|
|---|---|---|
|
|
@@ -165,7 +199,13 @@
|
|
|
165
199
|
|
|
166
200
|
### 这张表当前未通过的分布,以及它们是不是同一件事
|
|
167
201
|
|
|
168
|
-
**
|
|
202
|
+
**79 项里 52 项通过,8 项从未运行,11 项被执行但被环境挡住,1 项平台跳过,2 项是计划的主张被证伪,
|
|
203
|
+
5 项是先失败后修掉或确认非缺陷的。**
|
|
204
|
+
|
|
205
|
+
> **这些数字是从表里读出来的,不是数的**:`79 = 52 + 8 + 11 + 1 + 2 + 5`。
|
|
206
|
+
> 我曾把这份文档的标题计数凭记忆写错一次(写成 54/32/22,实际 61/53/8),
|
|
207
|
+
> **所以它们现在不再由任何地方手写。**
|
|
208
|
+
|
|
169
209
|
未通过的**不是一堆独立问题,而是 4 个阻塞源**:
|
|
170
210
|
|
|
171
211
|
| 阻塞源 | 卡住的项 | 解锁条件 |
|
|
@@ -173,7 +213,10 @@
|
|
|
173
213
|
| **只有 1 台可执行机器在线** | `TC-REPL-001`、`TC-SPLIT-001/002`、`TC-ANCH-001`、`TC-AGG-001/002/004` | 两台 `win-desktop` 自 2026-10-14 起静默;唤醒它们 |
|
|
174
214
|
| **中继无法远程升级/重启** | `TC-REL-001`–`004` | **`jp-shared` 上一条命令**(见下) |
|
|
175
215
|
| **没有可用配对码** | `TC-PAIR-003/004/005` | 一次可控的配对实验;会惊动编队,需你同意 |
|
|
176
|
-
| **需要真实中断或外部资源** | `TC-UPD-001/002/004/005/007` | 一次真实更新检查;`TC-SEC-007` 需要 macOS/Linux |
|
|
216
|
+
| **需要真实中断或外部资源** | `TC-UPD-001/002/004/005/007`、`TC-SEC-007` | 一次真实更新检查;`TC-SEC-007` 需要 macOS/Linux |
|
|
217
|
+
|
|
218
|
+
**5 项"先失败后处理"**:`TC-BOT-009`、`TC-DEF-001`、`TC-DEF-004` 已修;`TC-DEF-005` 确认**不是缺陷**(桥接没在跑);
|
|
219
|
+
**`TC-DEF-006` 仍未修** —— 发布流程的 `Remove-Item dist` 会删掉微信 token。**这是本表里唯一一处我明知会复发却还没堵上的。**
|
|
177
220
|
|
|
178
221
|
> **未通过的项 ≠ 未通过的项数那么多风险。** 它们集中在 4 处,其中**两处**(中继升级、唤醒 Windows 机器)是可以立刻解决的。
|
|
179
222
|
|
package/mcp.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "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.
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -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.
|
|
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;
|