@twinsearth/w2m-dsh-plugin 1.3.1 → 1.4.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.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +709 -0
  4. package/bin/w2m-grant.mjs +98 -0
  5. package/bin/w2m-maintenance.mjs +112 -0
  6. package/codex/mcp-servers.toml +1 -1
  7. package/deploy/maintenance/bootstrap.ps1 +30 -0
  8. package/deploy/maintenance/bootstrap.sh +61 -0
  9. package/deploy/server/w2m-maintenance.path +10 -0
  10. package/deploy/server/w2m-maintenance.service +20 -0
  11. package/deploy/windows/w2m-maintenance.xml +31 -0
  12. package/docs/COLLAB/COLLABORATION-MODEL.md +240 -0
  13. package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +361 -0
  14. package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +70 -0
  15. package/docs/COLLAB/M1-MAINTENANCE.md +313 -0
  16. package/docs/COLLAB/M2-TASK-GRANT.md +231 -0
  17. package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +291 -0
  18. package/docs/COLLAB/M4-PROJECT-ROOM.md +260 -0
  19. package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +263 -0
  20. package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +231 -0
  21. package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +263 -0
  22. package/docs/COLLAB/TASK-MODEL.md +281 -0
  23. package/docs/MULTI-MACHINE-COLLABORATION-PLAN.md +186 -0
  24. package/docs/RELEASE-STABLE.md +193 -0
  25. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json +27 -0
  26. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-POSTUPDATE-DIAG.json +18 -0
  27. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-RESTART.json +24 -0
  28. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-UPDATE.json +24 -0
  29. package/docs/acceptance/1.3.11/M1-LIVE-DIAG.json +26 -0
  30. package/docs/acceptance/1.3.12-beta.1/M1-LIVE-DIAG.json +26 -0
  31. package/docs/acceptance/1.4.0/M1-LIVE-MAINTENANCE.json +20 -0
  32. package/docs/acceptance/1.4.0/REGRESSION-1.4.0.json +81 -0
  33. package/package.json +26 -5
  34. package/plugin.json +1 -1
  35. package/src/acceptance/evidence.mjs +79 -0
  36. package/src/agent/agent.mjs +141 -3
  37. package/src/agent/grant-gate.mjs +89 -0
  38. package/src/agent/maintenance-ferry.mjs +111 -0
  39. package/src/agent/outbox.mjs +138 -0
  40. package/src/agent/patch-artifact.mjs +53 -0
  41. package/src/agent/worktree.mjs +118 -0
  42. package/src/maintenance/actions.mjs +203 -0
  43. package/src/maintenance/grant.mjs +139 -0
  44. package/src/maintenance/operator-key.mjs +151 -0
  45. package/src/maintenance/protocol.mjs +204 -0
  46. package/src/maintenance/runner.mjs +189 -0
  47. package/src/plugin/tools.mjs +1 -1
  48. package/src/relay/blob-sessions.mjs +148 -0
  49. package/src/relay/merge-queue.mjs +137 -0
  50. package/src/relay/project-locks.mjs +179 -0
  51. package/src/relay/project-notifications.mjs +184 -0
  52. package/src/relay/project-refs.mjs +110 -0
  53. package/src/relay/project-reviews.mjs +161 -0
  54. package/src/relay/project-room.mjs +163 -0
  55. package/src/relay/reliability-metrics.mjs +180 -0
  56. package/src/relay/server.mjs +382 -1
  57. package/src/relay/state.mjs +308 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,715 @@
7
7
 
8
8
  All notable changes to this project are documented here.
9
9
 
10
+ ## [1.4.0] — 2026-10-22
11
+
12
+ **稳定版:执行授权、任务、协作三个模型的第一版落地。**
13
+
14
+ - **执行授权模型**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL/max_uses);relay 派发预检
15
+ (`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED` → 400);撤销 `POST /v1/grants/revoke`(幂等、
16
+ ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;结果信封携带 `authorization.provenance`
17
+ (grant/allow-list/maintenance);使用台账不可写时 `GRANT_LEDGER_UNAVAILABLE` fail-closed。
18
+ - **任务模型**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;agent 在独立 worktree 中执行、提交、
19
+ 生成相对 base 的 patch 并上传内容寻址 blob,结果携带 `{base_commit, head_commit, changed_files, patch_sha256,
20
+ patch_bytes, cleaned}`;relay 侧分支 refs(单调、必须声明 from=head)、合并队列(patch sha256 幂等、already_merged)、
21
+ 冲突结果带 conflict_sha256 且绝不推进分支。
22
+ - **协作模型**:项目 room/board/artifact 索引/decisions/project memory/通知(共享对象事件广播 + 每 reader 未读水位)/
23
+ 项目级 SSE(ready + since_seq 补齐 + 实时 project.event)。
24
+ - **M7**:指标注册表与 `/metrics` 接线、证据 schema 与 preflight 第 16 项证据门禁;候选版本可用 candidate line
25
+ fallback,stable 必须提供本线真机证据。
26
+
27
+ **发布时真机状态(如实)**:relay 宿主已运行 1.4.0 候选(1.4.0-beta.1,代码与 stable 相同),relay 侧真机回归 10/10
28
+ 通过(board/artifacts/decisions/memory/locks/blobs/refs/merge/notifications/项目 SSE)。M1 维护通道的 diag 在换版后
29
+ 未能执行:非特权换版路径(fleet node 通道)替换目录后由 kill 触发的重启没有拉起 `w2m-localside`(部署脚本的
30
+ systemctl 被拒绝);在控制台执行 `systemctl restart w2m-localside` 后即可继续完成 M1 复验与 stable 官方资产部署。
31
+
32
+ 证据:`docs/acceptance/1.4.0/`(REGRESSION-1.4.0.json、M1-LIVE-MAINTENANCE.json)。
33
+
34
+ ## [1.4.0-beta.1] — 2026-10-22
35
+
36
+ **v1.4.0 三模型候选:执行授权模型落地、任务模型落地、协作模型落地。**
37
+
38
+ 这是把 1.3.12–1.3.14 的 beta 线合成一版的候选,供 stable 前真机验收。
39
+
40
+ - **执行授权模型(v1.4.0)**:`bin/w2m-grant.mjs` 签发单次执行授权(绑定 task_id/argv/机器/≤10min TTL);
41
+ relay 派发预检(`GRANT_TASK_MISMATCH` / `GRANT_ARGV_MISMATCH` / `GRANT_EXPIRED`);撤销
42
+ `POST /v1/grants/revoke`(幂等、ledger 留痕、派发 `403 GRANT_REVOKED`)与审计 `GET /v1/grants`;
43
+ 结果信封带 `authorization.provenance`(grant/allow-list/maintenance);使用台账不可写时
44
+ `GRANT_LEDGER_UNAVAILABLE` fail-closed(真机发现的 EACCES 崩溃已修)。
45
+ - **任务模型(v1.4.1 前身)**:任务可声明 `workspace {kind:'worktree', base_commit, produce}`;
46
+ agent 在独立 worktree 执行 → 提交 → 生成相对 base 的 patch → 上传内容寻址 blob → 证据
47
+ `{base_commit, head_commit, changed_files, patch_sha256, patch_bytes, cleaned}`;
48
+ relay 侧分支 refs(单调、必须声明 from=head)、合并队列(按 patch sha256 幂等、already_merged)、
49
+ 冲突结果携带 conflict_sha256 且绝不推进分支;refs/merge-queue HTTP 路由。
50
+ - **协作模型(v1.4.2 前身)**:项目 room(成员/角色/快照+增量事件)、board、artifact 索引、
51
+ decisions(append-only)、project memory(版本文档)、通知(共享对象事件广播 + 每 reader 未读水位)、
52
+ 项目级 SSE(ready + since_seq 补齐 + 实时 project.event + keepalive)。
53
+
54
+ M7 证据门禁对候选版本增加 candidate line fallback:beta 候选可引用其所基于发布线的真机证据(其产物在机器升级前构建),stable 仍必须提供本线真机证据。
55
+
56
+ Verified: gate GREEN ×2;relay 测试、agent 测试、workspace/refs/board/notification/SSE 用例全绿。
57
+
58
+ ## [1.3.12-beta.4] — 2026-10-22
59
+
60
+ **执行授权模型真机修复:使用台账不可写时 fail closed,而不是 crashed。**
61
+
62
+ 在 relay 宿主上实测时发现"授权了但一执行就崩":维护状态目录 `/var/lib/w2m/maintenance` 由 root 拥有且 0755,agent 以
63
+ 非特权用户运行,grant 第一次通过后写 `grant-uses.json` 报 EACCES,异常冒泡成任务 `crashed` —— 授权判定其实已经通过
64
+ (同一 argv 无 grant 时是 `COMMAND_NOT_ALLOWED`),失败发生在记录环节。
65
+
66
+ - `grant-gate`:台账写入包 try/catch,失败返回 `GRANT_LEDGER_UNAVAILABLE`,不执行"未记录的一次授权"(否则单次授权会变成
67
+ 可重放漏洞),也不再把整个 handler 打崩。
68
+ - `bootstrap.sh`:`$BASE` 改为 `root:w2m 0775`,让默认安装就能写台账;queue 仍 0770、done/claimed 0750。
69
+ - 回归用例:usedFile 指向不可写路径,断言返回该 code 而非抛异常。
70
+
71
+ 真机对比(修复前、relay 宿主):
72
+ - 无 grant 的 `uname -s` → `refused/COMMAND_NOT_ALLOWED`;
73
+ - 有合法 grant 的同一 argv → 通过 gate,随后因台账 EACCES 报 crashed(本次修复的目标)。
74
+
75
+ Verified: grant-gate 7/7(含新用例);gate GREEN twice。
76
+
77
+ ## [1.3.12-beta.3] — 2026-10-22
78
+
79
+ **执行授权模型收尾:每个结果都说明"是谁授权的"。**
80
+
81
+ beta.1 让操作员能授权一次执行,beta.2 让授权可撤销、可审计;这一版补上最后一块——结果自身携带授权来源,而不是只能在
82
+ relay 台账里反查。
83
+
84
+ - 结果信封新增 `authorization` 字段:`{provenance: "grant" | "allow-list" | "maintenance", capability_id?, grant_id?}`。
85
+ grant 路径带 `grant_id`(可与 ledger 对账),maintenance 路径带 request_id(可与 M1 台账对账),allow-list 路径标记
86
+ provenance 即可。
87
+ - 一个决定、两个受众:gate 决定"跑不跑",信封记录"这是哪一种决定";refused 路径不伪造 provenance。
88
+ - e2e 断言:空 allow-list + 合法 grant 的结果 `authorization.provenance === "grant"` 且带 grant_id。
89
+ - 边界如实写进文档:provenance 是执行机器对自身判定的陈述,relay 存储但不验证它;签名仍由 agent 验证,派发仍由 relay
90
+ 预检,撤销仍由 relay 记账——台账才是权威。
91
+
92
+ Verified: agent 159/157(含 grant replay e2e 的 provenance 断言);gate GREEN twice。
93
+
94
+ ## [1.3.12-beta.2] — 2026-10-22
95
+
96
+ **执行授权模型补完:撤销与操作员审计。**
97
+
98
+ beta.1 让操作员能"授权某一次执行";这一版让授权可以被收回、可以被查账,而不是只能等 TTL 过期。
99
+
100
+ - **撤销**:`POST /v1/grants/revoke`(operator token)`{grant_id, reason?}`。幂等(第二次 `already_revoked: true`),写入
101
+ ledger `grant.revoked`,带 reason 与操作者。
102
+ - **派发拒绝**:携带已撤销 grant 的任务在 `POST /v1/task` 被拒,`403 GRANT_REVOKED`,机器收不到 offer;revocation 先于
103
+ argv/task 绑定检查,理由更直接。
104
+ - **审计视图**:`GET /v1/grants`(operator token)返回 `tasks`(task_id / grant_id / argv_hash / key_id / mode / created_at)
105
+ 与 `revoked` 列表;device token 为 401,授权台账不属于机器工作面。
106
+ - **边界**:撤销阻止的是**新派发**;agent 断网时仍会执行已到手 grant 里绑定的那一次(正确性仍取决于 relay 派发,与 M2 §4
107
+ 一致)。撤销后要重新授权必须签发新 grant_id。
108
+
109
+ Verified: grant 13/13;grant-issue 3/3;relay 191/191(新增撤销+审计用例);gate GREEN twice。
110
+
111
+ ## [1.3.12-beta.1] — 2026-10-22
112
+
113
+ **执行授权模型:静态 allow-list 之外,操作员可以显式授权某一次执行。**
114
+
115
+ 在此之前授权只有一层:机器本地配置的 allow-list,一个长期决定,没有"就这一次"的途径。这一版把 M2 grant 补成一条可用的
116
+ 路径——签发、派发、验证、审计——而不是一个只在测试里存在的协议。
117
+
118
+ - **`bin/w2m-grant.mjs`**:操作员签发一条 grant。绑定一个 task_id(必须先确定)、一个 argv、若干 machine_id、≤10 分钟 TTL
119
+ 和 max_uses(默认 1);stdout 输出可直接放进 `POST /v1/task` 的 `grant` 字段,stderr 打印 task_id/argv_hash/expires_at。
120
+ 该 CLI 同时登记为 package bin `w2m-grant`。
121
+ - **relay 派发预检**:task_id 不符 → `GRANT_TASK_MISMATCH`,argv_hash 不符 → `GRANT_ARGV_MISMATCH`,已过期 →
122
+ `GRANT_EXPIRED`,都是 HTTP 400 的调用方错误;relay 不替 agent 做授权裁决。
123
+ - **审计**:task summary 增加 `argv_hash` 与 `grant_key_id`(`grant_id` 已有),让"谁授权了哪条命令"可查。
124
+ - **agent 仍是验证者**:签名、任务/机器绑定、argv、TTL、max_uses、重放(`GRANT_REPLAY`)由机器本地用 pin 的操作员公钥判定;
125
+ 空 allow-list + 合法 grant 执行,无 grant 仍 `COMMAND_NOT_ALLOWED`(既有 e2e 覆盖)。
126
+ - **文档**:`docs/COLLAB/EXECUTION-AUTH-MODEL.md` 状态改为已实现,补上"取 task_id → 签发 → 携带派发 → 两处检查 → 审计字段"
127
+ 的完整路径和仍然不做的边界(无长期委托、无自动签发、无跨项目授权)。
128
+
129
+ Verified: grant-issue 3/3(真实 CLI 子进程签发并用签名载荷验证);grant 13/13;relay 190/190(新增派发预检用例);gate GREEN twice。
130
+
131
+ ## [1.3.11] — 2026-10-22
132
+
133
+ **Stable release: the collaboration stack's first seven modules, with the gate that measures them.**
134
+
135
+ This is the first stable since 1.3.9 (docs) that ships runtime work, and the first beta line to end with its acceptance evidence
136
+ enforced by the pre-flight rather than stored beside it. What is in it, by module:
137
+
138
+ - **M1 maintenance channel** (live on the relay host): signed update/restart/diag requests, operator-key pinning, a root runner and
139
+ a bootstrap for Linux/Windows. Accepted on the real machine: diag (9 sections / 62 KB), relay restart with a new relay_id and
140
+ the result delivered after it, update to 1.3.10-beta.10 (download + sha256 + atomic swap), and a post-update diag. Evidence:
141
+ docs/acceptance/1.3.10-beta.10/.
142
+ - **M2 grants**: single-use, task-bound Ed25519 grants with argv hash, TTL, use ledger and named refusals; e2e covers an empty
143
+ allow-list succeeding under a grant, replay, argv tampering and the no-grant refusal.
144
+ - **M3 Git-native tasks**: isolated worktrees at the caller's base commit, a content-addressed patch artifact contract, a merge
145
+ queue with merge-slot serialisation, strict/replay policies, already_merged and a structured conflict artifact. Module-level,
146
+ 24 tests across the three slices plus two composed workflow tests.
147
+ - **M4 project room**: project objects, membership roles, a deny-by-default action matrix, snapshot + incremental events, task
148
+ attribution (implicit default project, explicit projects refused when unknown), and the /v1/projects routes.
149
+ - **M5 multi-client primitives**: branch/file locks (leases, renewal, owner force-release, merge preflight conflicts), reviews
150
+ (self-review guard, duplicate-verdict refusal, approval gate by patch sha) and notifications (dedupe, digest, per-reader
151
+ watermarks, retention), with the lock routes live and their events projected into the project stream.
152
+ - **M6 cross-network reliability**: chunked, resumable blob uploads with verified commits, Range downloads, and an agent result
153
+ outbox (atomic, persisted exponential backoff, cross-restart recovery, bounded).
154
+ - **M7 metrics and acceptance**: a 39-metric contract with a cardinality guard, live on /metrics; a validator for acceptance
155
+ evidence; and the pre-flight's 16th check, which fails a release whose evidence does not validate or whose release line has no
156
+ record.
157
+
158
+ Verified: pre-flight GREEN twice (16/16 checks, 2828 tests: 2814 pass, 0 fail, 14 skip); beta line beta.1 through beta.25 shipped on
159
+ GitHub prereleases only.
160
+
161
+ Known limitations, stated rather than implied: the merge queue and the review/notification routes are not wired into the task path
162
+ yet; a request that restarts the agent itself loses the ferry result (relay restarts are fine); M4 role checks cover the lock routes
163
+ and reads, not every future write; the relay runs beta.10 and is upgraded separately from this tag.
164
+
165
+ ## [1.3.10-beta.25] — 2026-10-22
166
+
167
+ **M7 final core slice: acceptance evidence is a gate, not a folder.**
168
+
169
+ The pre-flight grew its 16th check. `acceptance evidence (every committed record validates, and the release line has one)`
170
+ does three things a file list cannot:
171
+
172
+ 1. every JSON record under `docs/acceptance/` is validated with the M7 schema (`src/acceptance/evidence.mjs`) — a record that
173
+ does not say which machine ran it, when, by whom and under which task id fails the gate;
174
+ 2. the current release line must have at least one record: shipping a beta that claims an accepted capability while pointing at
175
+ no evidence from its own `1.3.x` line is RED;
176
+ 3. it reports what it read ("4 acceptance record(s) validated; 4 for the 1.3.10 line"), so a green row is a statement about a
177
+ specific set of records rather than about an empty directory.
178
+
179
+ The check is bound to `docs/acceptance` in `GATE_INVENTORY`, so deleting the directory or the check breaks the inventory's
180
+ both-directions rule; the evidence validator's accepted-orphan entry is removed now that the gate imports it.
181
+
182
+ Not claimed: this gate proves the records are well-formed and current, not that the described runs happened — that is what the
183
+ task id, the machine ids and the executor field are for, and they are now mandatory.
184
+
185
+ Verified: gate GREEN twice with 16/16 registered checks; the evidence check passes on the four M1 records.
186
+
187
+ ## [1.3.10-beta.24] — 2026-10-22
188
+
189
+ **M7 second slice: acceptance evidence has a schema, and the committed evidence is validated against it.**
190
+
191
+ - `src/acceptance/evidence.mjs` validates every required field of the M7 evidence record: version, uppercase case id, kind
192
+ (positive/negative/chaos), requirement, at least one machine, ISO `executed_at`, executor (lead/human/agent), a result with a
193
+ verdict, and — for a positive case — the real `task_id`. Optional fields are validated when present: argv arrays,
194
+ `[from, to]` ledger ranges, `{sha256, bytes}` artifacts and an assertions object. Every problem is returned at once, because a
195
+ release check should list what to fix rather than one field per run.
196
+ - `summarizeEvidence()` counts valid records by kind and version and names the invalid ones, which is what a release note or a
197
+ gate summary reads.
198
+ - The test suite validates the **actual committed files** under `docs/acceptance/` — a schema that rejected the project's own
199
+ evidence would be a schema written for a fictional project. It also pins that both the flat `result.outcome` and the nested
200
+ `result.task.outcome` (the live restart record) are accepted as the same fact.
201
+
202
+ Registered as an accepted library with its exit named (the preflight evidence gate).
203
+
204
+ Still to wire for M7: the preflight check that a release's evidence exists and validates, and the remaining instrumentation.
205
+
206
+ Verified: evidence 6/6; gate GREEN twice.
207
+
208
+ ## [1.3.10-beta.23] — 2026-10-22
209
+
210
+ **M7 wiring: the registry is live on /metrics.**
211
+
212
+ - Relay state owns the M7 registry. The project room now notifies a listener per event, and that callback counts
213
+ `wm_project_events_total{type}` once for both the room's own events (project created, member changes, task linked) and the
214
+ lock/review/notification events projected into it — a projection is appended, then counted through the room hook, so it cannot
215
+ be double-counted.
216
+ - `GET /metrics` appends the registry body under a `# reliability registry` marker. A relay that never records into the registry
217
+ renders exactly what it rendered before, which is why the existing exposition tests still pass unchanged.
218
+ - Lock routes count `wm_lock_acquired_total{kind}` and `wm_lock_conflict_total{kind}`.
219
+ - One route test: after creating a project and acquiring a branch lock, `/metrics` carries
220
+ `wm_project_events_total{type="project.created"}` and `wm_lock_acquired_total{kind="branch"}`.
221
+
222
+ Verified: relay 189/189 (one new test); the registry and room suites are unchanged; gate GREEN twice.
223
+
224
+ ## [1.3.10-beta.22] — 2026-10-22
225
+
226
+ **M7 first slice: one metrics contract for the whole collaboration stack.** A local module and eight tests; the relay /metrics
227
+ route and the call sites do not record into it yet.
228
+
229
+ - `src/relay/reliability-metrics.mjs` declares every M7 metric name, its type and the labels it may carry — 39 definitions across tasks,
230
+ authorization, data plane, collaboration and transport. An undeclared name, an undeclared label, or a high-cardinality label
231
+ (`task_id`, `dedupe_key`, ids) is refused at record time, so a metrics endpoint cannot silently become a memory leak.
232
+ - Counters refuse decrements; gauges hold the last value; histograms keep cumulative buckets plus sum/count. `renderPrometheus()`
233
+ emits `_bucket/_sum/_count` with sorted, escaped label values, and `snapshot()` gives the same data as plain JSON for the panel.
234
+ - Eight tests: label aggregation, undeclared metric/label refusal, wrong-type and decrement refusal, gauge semantics, cumulative
235
+ buckets, Prometheus text (including `+Inf`), label escaping, and the contract's own shape.
236
+
237
+ Registered as an accepted library with its exit named (the /metrics route and instrumentation). Note on the name: the exposition module was already called metrics.mjs, so after a naming collision during this work its file and test were restored, and this registry ships as reliability-metrics.mjs; both test files pass.
238
+
239
+ Still to wire for M7: the /metrics route, the instrumentation call sites, the acceptance-evidence validator and the preflight
240
+ evidence check.
241
+
242
+ Verified: metrics 8/8; gate GREEN twice.
243
+
244
+ ## [1.3.10-beta.21] — 2026-10-22
245
+
246
+ **M6 second slice: the agent result outbox.** A local module and eight tests; the agent delivery path still uses the existing
247
+ spool and has not switched to it yet.
248
+
249
+ - `src/agent/outbox.mjs` — a result is written to disk (temp + rename, so a crash leaves either the old state or the complete
250
+ entry) before any network attempt, keyed by `(task_id, attempt)` so a retried handler is one entry. Delivery removes the entry
251
+ only after the relay acknowledged it; the relay's own dedupe makes a re-delivery safe when that acknowledgement was lost.
252
+ - Retries use exponential backoff persisted with the entry (`1s, 2s, 4s …` capped at 60s), so closing the laptop for an hour is
253
+ a resume rather than a restart. Past `max_attempts` the entry moves to `dead/` and is reported as exhausted.
254
+ - Limits: 200 entries / 16 MiB; beyond that the oldest entries are dropped and the caller sees `outbox_full: true`
255
+ (`OUTBOX_FULL` in the changelog's words) rather than a silent disk fill. A single oversize result is refused.
256
+ - Eight tests: atomic persist + depth, duplicate enqueue, restart recovery with attempt counts, backoff growth and exhaustion,
257
+ ack-only removal, count-limit drop keeping the newest, corrupt-file quarantine, oversize refusal.
258
+
259
+ Registered as an accepted library with its exit named (the agent delivery switch).
260
+
261
+ Still to wire for M6: the agent's upload/download decision for large artifacts, and P2P as acceleration-only with metrics.
262
+
263
+ Verified: outbox 8/8; gate GREEN twice.
264
+
265
+ ## [1.3.10-beta.20] — 2026-10-22
266
+
267
+ **M6 wiring: chunk session and Range routes.** Large artifacts now travel over HTTP without restarting from zero.
268
+
269
+ - `POST /v1/fleet/blob-sessions` declares `{sha256, size, chunk_bytes?}`; `PUT …/:sid/chunks/:index` uploads one idempotent chunk
270
+ (octet-stream); `GET …/:sid` returns received/missing so an interrupted upload resumes; `POST …/:sid/commit` verifies the
271
+ assembled sha256 and, on success, stores the bytes through the existing content-addressed blob store. A missing chunk answers
272
+ `409 BLOB_INCOMPLETE` with the indices; a mismatch deletes the session and creates nothing.
273
+ - `GET /v1/fleet/blobs/:sha?range=start-end` answers `206` with `Content-Range`, so a client can fetch a large patch (or a conflict
274
+ artifact) in slices instead of one allocation.
275
+ - The blob-session module's responses are now uniformly `{ok, session: {...}}`, which the routes and the original tests share.
276
+
277
+ Two HTTP tests: a chunked upload that commits and is then read whole plus as a Range, and an incomplete commit naming `[0,1]`.
278
+
279
+ Still to wire for M6: the agent result outbox, and P2P as acceleration-only with its observable success rate.
280
+
281
+ Verified: blob-sessions 9/9, relay 188/188 (2 new route tests); gate GREEN twice.
282
+
283
+ ## [1.3.10-beta.19] — 2026-10-22
284
+
285
+ **M6 first slice: large artifacts can be uploaded in resumable chunks.** A local module and nine tests; the routes and the
286
+ agent upload path are the wiring slice after it.
287
+
288
+ - `src/relay/blob-sessions.mjs` — a client declares the final `sha256` and `size` once, then uploads chunks in any order
289
+ (each index idempotent), asks what is missing, and commits. Only a commit whose assembled bytes hash to the declared sha256
290
+ becomes a blob; a mismatch deletes the session and returns `BLOB_SHA_MISMATCH`, so nothing can be referenced that does not
291
+ exist.
292
+ - Limits follow the spec: default 1 MiB chunks, `≤8 MiB` per chunk, `≤256` chunks and `≤64 MiB` per blob, two open sessions and
293
+ 512 MiB unfinished bytes per machine, 24h session TTL. A resume after a disconnect is `status()` plus the missing indices,
294
+ which is exactly the "don't restart a 40 MiB patch" property.
295
+ - Nine tests: plan/status, out-of-order + idempotent chunks, verified commit through the store hook, incomplete commit naming
296
+ the missing chunks, sha mismatch deleting the session, chunk length/size refusals, quotas, TTL expiry, and blob/chunk ceilings.
297
+
298
+ Registered as an accepted library with its exit named (the blob-session routes + agent upload).
299
+
300
+ Still to wire for M6: the HTTP chunk routes and Range downloads, the agent outbox for offline results, and P2P as acceleration
301
+ only.
302
+
303
+ Verified: blob-sessions 9/9; gate GREEN twice.
304
+
305
+ ## [1.3.10-beta.18] — 2026-10-22
306
+
307
+ **M5 wiring, part 1: lock routes and one event stream for the primitives.**
308
+
309
+ - Relay state now owns the three M5 primitives (locks, reviews, notifications) and projects their events into the project room
310
+ stream with the module sequence attached, so a client can reconstruct "what happened" from one cursor instead of three.
311
+ - `/v1/projects/:id/locks`: `GET` list, `POST` acquire, `POST …/:lock_id/renew`, `DELETE …/:lock_id` (holder release), and
312
+ `DELETE …?force=1&reason=…` (owner only). A conflict is an answer, not a transport error: `409` with the holder and expiry, so a
313
+ client can show who is editing instead of parsing a failure. Role checks sit on M4 membership (default project treats a paired
314
+ machine as worker); a non-member receives `404` and learns nothing about the project.
315
+ - Two route tests: acquire/list/conflict/foreign-release/ordered-release, plus non-member `404` and `LOCK_FORCE_DENIED` for a
316
+ worker trying to force release.
317
+
318
+ Still to wire: review and notification routes, the merge/review gate, and the panel views.
319
+
320
+ Verified: relay 186/186 (2 new lock route tests); gate GREEN twice.
321
+
322
+ ## [1.3.10-beta.17] — 2026-10-22
323
+
324
+ **M5 third primitive: notifications.** A local module and eight tests; no routes or SSE projection use it yet.
325
+
326
+ - `src/relay/project-notifications.mjs` — notifications for a project addressed to a member or broadcast, each with kind,
327
+ subject, refs and `requires_action`. Every row is also a `notification.created` event for the live stream, and the list route
328
+ is the catch-up path.
329
+ - Semantics pinned by tests: the same (kind, primary ref, target) inside 60s is one row; different refs/kinds are distinct;
330
+ overflow beyond 120 per minute collapses into one digest row that carries the count; unread is counted after a per-reader
331
+ watermark; a watermark never moves backwards; paging is `since_ms` + limit; retention drops rows older than 7 days (or beyond
332
+ 2000 per project).
333
+ - The reader is a member identity (machine or operator), not a transport: an offline client reconnects and reads the same
334
+ watermark it left.
335
+
336
+ Registered as an accepted library with its exit named (the notification routes).
337
+
338
+ M5 primitives are now all designed-and-implemented at module level: locks (beta.15), reviews (beta.16), notifications (beta.17).
339
+ Still missing for M5: the routes, the merge/ref gates using them, and the panel views.
340
+
341
+ Verified: project-notifications 8/8; gate GREEN twice.
342
+
343
+ ## [1.3.10-beta.16] — 2026-10-22
344
+
345
+ **M5 second primitive: review tasks.** A local module and ten tests; the review routes and the merge gate do not call it yet.
346
+
347
+ - `src/relay/project-reviews.mjs` — a review points at a content-addressed `{kind, sha256, source_task_id, diffstat}` target and
348
+ collects decisions from listed reviewers. Verdicts are records, not a mutable flag: `approve`/`changes_requested`/`comment`
349
+ accumulate, approvals are counted per distinct reviewer, and reaching `required_approvals` moves the review to `approved`.
350
+ - Safety rules the tests pin: self-review is refused unless an owner overrides it (and the override is its own event); reviewers
351
+ must be maintainer/owner; only listed reviewers may decide; a reviewer cannot record the same verdict twice; a
352
+ `changes_requested` closes the round and refuses a later approve; a due date expires a pending review and late decisions are
353
+ refused; `approvedFor(project, sha256)` answers the merge gate for an exact patch.
354
+ - Every state change is emitted as an event (`review.requested/decided/approved/changes_requested/expired/self_review_override`).
355
+
356
+ Registered as an accepted library with its exit named (the review routes + merge-requires-review gate).
357
+
358
+ Not in this slice: notifications (§4), the third M5 primitive.
359
+
360
+ Verified: project-reviews 10/10; gate GREEN twice.
361
+
362
+ ## [1.3.10-beta.15] — 2026-10-22
363
+
364
+ **M5 first primitive: branch and file locks.** A local module and eight tests; no routes or merge preflight call it yet.
365
+
366
+ - `src/relay/project-locks.mjs` — one exclusive holder per `(project, branch)` or `(project, path)`, leases with renewal, lazy
367
+ expiry that frees the target and records `lock.expired`, owner-only force release with a mandatory reason recorded as
368
+ `lock.force_released`, per-project/per-holder limits (20/10), and a `conflicts()` preflight that answers which live locks cover a
369
+ branch or a set of paths.
370
+ - Every mutation is returned as an event (`lock.acquired/renewed/released/expired/force_released`) so the project stream can carry
371
+ it; the module holds no relay state and takes the clock as an injection.
372
+ - Eight tests: acquire/list/release, LOCK_HELD naming the holder, expired re-entry with both events, renew ownership and
373
+ non-revival, force-release permission and reason, limits, preflight conflicts for branch and paths, and malformed-input refusal.
374
+
375
+ Registered as an accepted library with its exit named (the lock routes and the merge preflight check).
376
+
377
+ Not in this slice: review tasks (§3) and notifications (§4), which are the next two M5 primitives.
378
+
379
+ Verified: project-locks 8/8; gate GREEN twice.
380
+
381
+ ## [1.3.10-beta.14] — 2026-10-22
382
+
383
+ **M4 third slice: the `/v1/projects` routes.**
384
+
385
+ - `GET /v1/projects` — the default project plus the projects this machine is a member of (device token).
386
+ - `GET /v1/projects/:id` — snapshot with `last_event_seq`; a machine that is not a member gets `NOT_FOUND`, so a project's
387
+ existence is not leaked. The default project stays readable for any paired machine because it is the implicit home of unnamed
388
+ tasks.
389
+ - `GET /v1/projects/:id/events?since_seq=N` — the incremental slice and `next_seq`, so a client can resume after a disconnect.
390
+ - `POST /v1/projects` and `POST /v1/projects/:id/members` — operator token only (the dispatch credential), because these change
391
+ who may act on shared state; the body's initial `members` array is seeded atomically with the project.
392
+ - The routes authenticate per method and therefore sit above the shared device guard: an operator token is not a device token,
393
+ and the earlier placement made every write answer `UNAUTHORIZED`.
394
+ - Three HTTP tests: list/create/snapshot/events with a member, a non-member seeing `NOT_FOUND`, the operator requirement, and an
395
+ unknown project answering `NOT_FOUND`.
396
+
397
+ Still missing: artifact/memory/decision mounts, the panel view, and multi-client e2e.
398
+
399
+ Verified: relay 184/184 (3 new route tests); gate GREEN twice.
400
+
401
+ ## [1.3.10-beta.13] — 2026-10-22
402
+
403
+ **M4 second slice: tasks belong to a project, and the room records it.** Relay state now owns a project room; no HTTP routes yet.
404
+
405
+ - `POST /v1/task` (internal `createTask`) accepts an optional `project_id`. Unnamed tasks land in the implicit `default`
406
+ project, which is created lazily on first use; an explicit unknown project is refused with `BAD_REQUEST` rather than being
407
+ created silently.
408
+ - The project room records a `task.linked` event for every task, so a snapshot's `last_event_seq` moves with the work and a
409
+ second client can apply it as an incremental event.
410
+ - State exposes `projectSnapshot(projectId)` and `projectEvents(projectId, {sinceSeq})`; the task summary carries
411
+ `project_id`; the replay path restores it from the ledger.
412
+ - Three attribution tests: implicit default + linked event consistency, explicit project visible on the summary, and an unknown
413
+ project refused without creating one.
414
+
415
+ Still missing, stated: the `/v1/projects` HTTP routes, artefact/memory/decision mounts, the panel view, and multi-client e2e.
416
+
417
+ Verified: project-attribution 3/3, project-room 8/8, relay 181/181; gate GREEN twice.
418
+
419
+ ## [1.3.10-beta.12] — 2026-10-22
420
+
421
+ **M4 first slice: the project room object.** A local module and eight tests; the relay routes and the panel do not call it yet.
422
+
423
+ - `src/relay/project-room.mjs` — a project aggregate (project id, name, origin, settings, members), the four roles
424
+ (owner/maintainer/worker/observer), a deny-by-default action matrix (`read/dispatch/artifact/lock/memory/decide/merge/
425
+ settings/members`), member add/remove/role-change as recorded events, a snapshot carrying `last_event_seq`, incremental
426
+ events since a sequence, and per-member project listing.
427
+ - Safety rules the tests pin: the last owner cannot be removed; a duplicate member is refused by name; an unknown project is
428
+ refused rather than creating an empty one; the permission check returns the required and current role rather than a bare
429
+ false; a role change records `{from,to}`.
430
+
431
+ Registered as an accepted library with its exit named (the `/v1/projects` routes and the room panel), because nothing reaches it
432
+ yet.
433
+
434
+ Also in this tree since beta.11: the M1 live-acceptance evidence files under `docs/acceptance/1.3.10-beta.10/`.
435
+
436
+ Verified: project-room 8/8; gate GREEN twice.
437
+
438
+ ## [1.3.10-beta.11] — 2026-10-22
439
+
440
+ **M1 update path fixed and the full acceptance set recorded.**
441
+
442
+ The live update acceptance exposed two defects that made `update` unusable even though diag and restart already worked:
443
+
444
+ 1. **EXDEV on swap** — staging unpacked under `/tmp` (often tmpfs) and the swap is a rename into `/opt/w2m/app`, so the kernel
445
+ refused with `EXDEV: cross-device link`. Staging now lives on the same filesystem as the app directory
446
+ (`/opt/w2m/.maintenance-staging`).
447
+ 2. **Ferry reported success for a failed action** — the ferry returning a result file only means the runner wrote one; the
448
+ runner's own `status` was not inspected, so a failed update was reported as `ok`. The agent now reports
449
+ `MAINTENANCE_FAILED` with the runner's code, and a test covers the shape.
450
+
451
+ Acceptance evidence now in the tree:
452
+ - `docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json` (9 sections, 62,238 bytes, runner ok)
453
+ - `M1-LIVE-RESTART.json` (relay restarted, new relay_id, result delivered)
454
+ - `M1-LIVE-UPDATE.json` (download + sha256 + swap to 1.3.10-beta.10, no auto-restart)
455
+ - `M1-LIVE-POSTUPDATE-DIAG.json` (after update + restart, a new diag still returns 59,581 bytes)
456
+
457
+ Known limitation unchanged and documented: a request restarting the agent itself kills the ferry before it reads the result
458
+ file, so the accepted restart target is the relay; delayed restart is the next fix.
459
+
460
+ Verified: gate GREEN twice; actions/ferry unit tests pass.
461
+
462
+ ## [1.3.10-beta.10] — 2026-10-22
463
+
464
+ **M1 live acceptance: the maintenance channel works end to end on a real machine.**
465
+
466
+ The relay host (jp-shared) was bootstrapped and two signed requests were executed by the root runner:
467
+
468
+ - **diag** — the runner produced 9 sections (version/services/journal/disk/ports), the agent returned the task as `ok` with
469
+ 62,238 bytes of result, and the runner's own result file confirms `status: ok`. Evidence:
470
+ `docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json`.
471
+ - **restart (rabbit)** — the relay was restarted by the runner, the agent survived it and delivered the task result, and the
472
+ relay came back with a new `relay_id` and a fresh uptime. Evidence:
473
+ `docs/acceptance/1.3.10-beta.10/M1-LIVE-RESTART.json`.
474
+
475
+ The run found four real defects, all fixed here:
476
+
477
+ 1. The agent's envelope builder hashes and sizes the output; the maintenance ferry passed an execution object without
478
+ `stdout_sha256/stdout_bytes/stderr_sha256/stderr_bytes`, so every maintenance task ended as `crashed` **after** the action had
479
+ already run. The ferry now fills them.
480
+ 2. `--operator-key` without `--operator-key-id` claimed an empty key id, which the pin store correctly refused as a mismatch;
481
+ an absent id now means "trust the key that arrives".
482
+ 3. `bootstrap.sh` made the queue root-only, so the unprivileged agent could never enqueue a request; the queue is now
483
+ `root:w2m 0770` (write but not execute) and `done` is `root:w2m 0750`.
484
+ 4. Known limitation, documented rather than hidden: a request that restarts **the agent itself** kills the process still waiting
485
+ for its result file, so that result is lost (the action still runs). Restarting the relay is unaffected, which is why the
486
+ accepted case uses that target; the delayed-restart fix is next.
487
+
488
+ Also verified: gate GREEN twice; the local agent suite still passes with the ferry envelope fix.
489
+
490
+ ## [1.3.10-beta.9] — 2026-10-22
491
+
492
+ **M3 conflict contract completed: MERGE_CONFLICT now carries the conflict itself.**
493
+
494
+ When `git apply --3way` cannot apply a patch, the queue previously reported the changed files and the git message. It now also
495
+ captures the state git left in the merge worktree (conflict markers included) as a `conflictArtifact`:
496
+ `{ sha256, bytes, text }`. That is the evidence a reviewer needs to decide whether to resubmit against the new base or resolve
497
+ by hand, and it is the same content-addressed shape the patch artifacts use.
498
+
499
+ The head still does not move on a conflict; the test asserts both the artifact (with conflict markers) and the unchanged head,
500
+ and the composed M3 workflow test still passes.
501
+
502
+ Verified: merge-queue 7/7 (conflict artifact assertion included), m3-workflow 2/2; gate GREEN twice.
503
+
504
+ ## [1.3.10-beta.8] — 2026-10-22
505
+
506
+ **M3 composed: worktree -> patch -> merge queue, proven together.**
507
+
508
+ Two integration tests drive the three modules the way a real task pair would: both tasks start from the same base in isolated
509
+ worktrees, each commits its change and produces a patch artifact, and the queue merges them one after the other. The disjoint
510
+ case ends with both files changed and the branch advanced to the second merge; the same-file case ends with a structured
511
+ MERGE_CONFLICT listing the file, the head still on the first approved commit, and the first task's change intact.
512
+
513
+ No runtime wiring yet: the agent's task handler does not create worktrees, and the relay does not run the queue. This release
514
+ records that the modules agree with each other before either becomes reachable, which is the point of composing them now.
515
+
516
+ M1 real-machine acceptance remains pending: the relay host agent is offline and root SSH authentication is refused, so the
517
+ privileged bootstrap has not run.
518
+
519
+ Verified: m3-workflow 2/2, merge-queue 7/7, worktree 5/5, patch-artifact 5/5; gate GREEN twice.
520
+
521
+ ## [1.3.10-beta.7] — 2026-10-22
522
+
523
+ **M3 third block: the merge queue.** A local module and seven real-git tests; the relay task path does not import it yet.
524
+
525
+ - `src/relay/merge-queue.mjs` — one queue per project+branch with a single merge slot (holder + TTL), items named by the
526
+ patch sha256, `strict` and `replay` base policies, and terminal verdicts: MERGED (branch head advances), `already_merged`
527
+ for a repeat submission, `BASE_MISMATCH` under strict, and a structured `MERGE_CONFLICT` with the changed files that never
528
+ advances the head.
529
+ - Application happens in a detached throwaway worktree, then the repository's branch and working tree are moved to the new
530
+ commit — a merge that committed only in the worktree would leave the next reviewer looking at a head that never moved.
531
+ - Seven tests cover merge-and-advance, idempotent re-submission, stale base, conflict (head unchanged), replay onto a moved
532
+ head, merge-slot serialisation, and patch naming.
533
+
534
+ Registered as an accepted library with its exit named (the relay merge executor dispatch), because nothing reaches it yet.
535
+
536
+ Still pending, unchanged: the M1 real-machine acceptance (relay runs beta.5 and carries the maintenance field live, but the
537
+ relay host's agent is offline and root SSH authentication has not been restored), and the M2/M3 live runs.
538
+
539
+ Verified: merge-queue 7/7; the rest of the suite is unchanged; gate GREEN twice.
540
+
541
+ ## [1.3.10-beta.6] — 2026-10-21
542
+
543
+ **M3 first two blocks: isolated worktrees and a patch artifact contract. Plus the grant replay e2e.**
544
+
545
+ - `src/agent/worktree.mjs` — one git worktree per task, created detached at the caller's `base_commit`; hooks disabled,
546
+ commit identity fixed, a porcelain snapshot before and after, commit-if-changed, `format-patch` relative to the base, and
547
+ a removal that reports failure instead of leaking a directory. Unknown base commit and missing repository are named refusals.
548
+ - `src/agent/patch-artifact.mjs` — the transportable result: sha256 over the patch bytes, byte count, base/head commits,
549
+ sorted changed files, inline/blob mode, and `no_change` as a successful outcome for an empty patch. Non-ASCII patches are
550
+ measured in UTF-8 bytes, not characters.
551
+ - Five real-git tests for the worktree lifecycle and five for the artifact contract.
552
+ - `test/agent.test.mjs` gains the grant **replay** e2e: the same signed grant is used twice, the first run executes, the second
553
+ is refused with `GRANT_REPLAY` — the use ledger is what makes "single-use" true across processes.
554
+
555
+ Not in this slice (stated, not implied): the merge queue itself (strict/replay bases, merge-slot serialization,
556
+ `MERGE_CONFLICT`), review tasks and locks (M5), and the project room (M4). The M1 real-machine acceptance is also still pending:
557
+ the relay was upgraded to beta.5 and the maintenance field is verified live, but the relay host's agent is offline and root SSH
558
+ authentication is refused, so the privileged bootstrap has not run.
559
+
560
+ Verified: agent 159/157 (replay e2e included), relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4,
561
+ worktree 5/5, patch-artifact 5/5, layer-model 24/24, claims 47/47; gate GREEN twice.
562
+
563
+ ## [1.3.10-beta.5] — 2026-10-21
564
+
565
+ **M2 grant e2e: the explicit-authorisation path is proven end to end.**
566
+
567
+ Four fake-relay tests now drive the real agent process:
568
+
569
+ - an empty allow-list executes a command **only** because a valid grant authorises that exact argv (the envelope is `ok` and
570
+ the stdout hash matches);
571
+ - the same command with no grant is still `COMMAND_NOT_ALLOWED`;
572
+ - a grant that signed a different argv is refused with `GRANT_ARGV_MISMATCH`;
573
+ - a grant that names another machine is refused with `GRANT_WRONG_MACHINE`.
574
+
575
+ Two integration defects the e2e exposed, both fixed: the per-stage allow-list re-check ran again after step 5, so a grant that
576
+ had authorised the task was refused at the stage boundary; and the matcher's result carries `allowed` (not `ok`), so both the
577
+ gate result and the stage override had to use that shape or the refusal fell back to `COMMAND_NOT_ALLOWED`.
578
+
579
+ Verified: agent 158/156 (4 new e2e cases), relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4,
580
+ layer-model 24/24, claims 47/47; gate GREEN twice.
581
+
582
+ ## [1.3.10-beta.4] — 2026-10-21
583
+
584
+ **M1 bootstrap tooling: one privileged step, then the channel is remote.** No runtime behaviour changes.
585
+
586
+ - `deploy/maintenance/bootstrap.sh` (Linux, run once as root) — installs the fixed-action path unit/service, creates the
587
+ queue/done/claimed directories with root ownership, optionally pins the operator public key, and enables the watcher. It
588
+ refuses to run without `--machine-id` and refuses a tree where `bin/w2m-maintenance.mjs` is absent.
589
+ - `deploy/maintenance/bootstrap.ps1` (Windows, run once as Administrator) — same shape via a SYSTEM scheduled task; it
590
+ substitutes the app directory and machine id into the task XML rather than editing the shipped file.
591
+ - `docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md` — the real-machine acceptance cases (diag/restart/update/rollback plus four named
592
+ refusals), what "green" means, and the limitations that must appear in the release note (privileged bootstrap, TOFU pinning,
593
+ no automatic recovery if a restarted machine cannot be reached).
594
+
595
+ Both scripts install **only** the fixed update/restart/diag runner; neither installs a shell, and neither touches the
596
+ allow-list. The design's security boundary is unchanged: the agent ferries a signed request, the root runner decides.
597
+
598
+ Honest gap, unchanged: the M1 real-machine acceptance run itself still needs a machine with a privileged bootstrap and a
599
+ relay carrying the maintenance fields; and the M2 grant path still lacks a fake-relay e2e case. Both are next.
600
+
601
+ Verified: gate GREEN twice; pack carries the new scripts and document.
602
+
603
+ ## [1.3.10-beta.3] — 2026-10-21
604
+
605
+ **M2 first slice: task-level grants become the explicit authorisation channel.** Design: `docs/COLLAB/M2-TASK-GRANT.md`.
606
+
607
+ - `src/maintenance/grant.mjs` — grant protocol: task binding, named machine ids, argv hash + expanded argv for the operator
608
+ to read, max uses, TTL capped at 10 minutes, nonce, and constraint fields that may only tighten the task (mode/cwd/write/
609
+ write_scope). Verification has a named refusal for every distinct failure: wrong machine, wrong task, argv mismatch,
610
+ expired, future, TTL too long, replay, uses exhausted, revoked key, unknown key, bad signature, constraint widening.
611
+ - `src/agent/grant-gate.mjs` — the agent-side gate: pins the operator key on first sight, verifies the grant against the
612
+ concrete argv, and persists use counts so a spent grant stays spent across restarts. A corrupt use ledger is never read as
613
+ "nothing used yet".
614
+ - The agent asks the grant gate **before** falling through to the allow-list refusal, and reports a grant refusal with its own
615
+ `GRANT_*` code instead of folding it into `COMMAND_NOT_ALLOWED`.
616
+ - The relay accepts an optional `grant` on `POST /v1/task` (8 KiB cap, `grant_id` + 64-hex `argv_hash` checked), carries it on
617
+ the offer, writes `grant_summary` into the ledger and exposes `grant_id` on the task summary.
618
+
619
+ Honest gap: the grant path has unit coverage (protocol 10, gate 6) and the existing agent/relay suites are unchanged-green,
620
+ but there is no fake-relay e2e yet for "empty allow-list executes under a grant". That case and the real two-machine run land
621
+ in the next slice together with the M2 acceptance evidence.
622
+
623
+ Verified: agent 154/152, relay 181/181, grant 10/10, grant-gate 6/6, maintenance 17/17, ferry 4/4, layer-model 24/24; gate
624
+ GREEN twice.
625
+
626
+ ## [1.3.10-beta.2] — 2026-10-21
627
+
628
+ **M1 maintenance channel: relay passthrough + agent ferry.** The remote loop now has both ends.
629
+
630
+ - The relay accepts an optional `maintenance` request and `operator_key` on `POST /v1/task` (operator token already required),
631
+ registers the key once (`operator.key.registered` in the ledger, replayed on restart), carries the request on the offer, and
632
+ exposes `maintenance_request_id` on the task summary.
633
+ - `src/agent/maintenance-ferry.mjs` — the agent pins the operator key on first sight (TOFU, rotation chain thereafter), verifies
634
+ the request against the pin, writes it atomically into the queue the root runner watches, waits for the runner's result file,
635
+ and reports it as the task result. A refusal is a named value (`MAINTENANCE_NO_PINNED_KEY`, `MAINTENANCE_BAD_SIGNATURE`,
636
+ `MAINTENANCE_RUNNER_TIMEOUT`, ...), never a thrown error, so the task result says why.
637
+ - The agent never gains the privilege to run update/restart/diag: the root runner (beta.1) remains the only executor.
638
+ - Layer model declares `maintenance` as a capability layer with an `agent -> maintenance` edge, so the new dependency is
639
+ explained rather than excused.
640
+
641
+ Known gap, stated: no fake-runner agent-e2e test for the ferry yet (the ferry's own four cases cover pin/verify/queue/refusal);
642
+ the end-to-end case lands with the real-machine acceptance run in beta.3.
643
+
644
+ Verified: gate GREEN twice with the bundled node on PATH: 2731 tests / 2717 pass / 0 fail / 14 skip in 135 files
645
+ (agent 154/152, relay 181/181, maintenance 17/17, ferry 4/4, layer-model 24/24, claims 44/44).
646
+
647
+ ## [1.3.10-beta.1] — 2026-10-21
648
+
649
+ **M1 maintenance channel, first implementation slice.** Design: `docs/COLLAB/M1-MAINTENANCE.md` (the design series names this slice v1.3.2-beta; it ships as 1.3.10-beta.1 because 1.3.9 was already published as a stable release and versions must stay ordered).
650
+
651
+ The maintenance channel lets an operator authorise exactly one of three fixed actions on one machine (update, restart, diag)
652
+ with an Ed25519-signed request, executed by a root/SYSTEM runner that reads a queue the unprivileged agent may write to. An
653
+ allow-list entry is not needed: the signature is the authorisation, and widening the allow-list is never the answer.
654
+
655
+ ### What this slice contains
656
+
657
+ - `src/maintenance/protocol.mjs` — request envelope, canonical signing bytes, parameter whitelists for the three actions,
658
+ and the complete refusal vocabulary (wrong machine, expired, future, TTL too long, replay, revoked key, unknown key,
659
+ bad signature, bad action, bad params, too large, missing signature). TTL is capped at 10 minutes and only the pinned
660
+ operator key can sign.
661
+ - `src/maintenance/operator-key.mjs` — Ed25519 key generation, relay registration payload, trust-on-first-use pinning with a
662
+ rotation chain (a new key must be signed by the pinned one), revocation, and a corrupt pin store that refuses instead of
663
+ trusting nothing.
664
+ - `src/maintenance/runner.mjs` — queue scanning, claim-before-verify, signature verification, persisted replay sets
665
+ (request id + nonce), idempotent re-runs, and a result file for every request it looks at, refusals included.
666
+ - `src/maintenance/actions.mjs` — the three fixed actions with no shell interpolation: diag (fixed read-only command set,
667
+ capped output), restart (fixed unit order and health waits), update (https download, sha256 verify, staged extraction,
668
+ version check inside the artifact, backup, atomic swap, health check, automatic rollback).
669
+ - `bin/w2m-maintenance.mjs` — the root entry point (once/watch, pin bootstrap, root guard).
670
+ - `deploy/server/w2m-maintenance.{service,path}` and `deploy/windows/w2m-maintenance.xml` — the runner as a systemd path
671
+ unit / SYSTEM scheduled task.
672
+
673
+ ### What this slice deliberately does not contain
674
+
675
+ The relay routes that distribute the operator public key and the agent-side ferry that carries a request from a task offer
676
+ to the queue (and the result back) are the next slice (1.3.10-beta.2). Until then the channel is exercised by placing a
677
+ signed request in the queue directory directly — which is also the offline/bootstrap path the design keeps.
678
+
679
+ Tests: 17 new cases cover the protocol, pinning/rotation/revocation and the runner's refusal and replay behaviour. The
680
+ destructive actions are not executed in tests; they are covered by the real-machine acceptance runbook (M7 evidence).
681
+
682
+ Verified: gate GREEN twice.
683
+
684
+ ## [1.3.9] — 2026-10-21
685
+
686
+ **Design-first release: the collaboration line is specified, not yet implemented.**
687
+
688
+ This release ships the design specification for the multi-machine / multi-client collaboration capability, and no behaviour change: the
689
+ plugin, relay and agent are byte-identical in function to 1.3.1. It exists so the plan is reviewable, versioned, and citable before any
690
+ of it is built.
691
+
692
+ ### What is in this release
693
+
694
+ - `docs/MULTI-MACHINE-COLLABORATION-PLAN.md` — capability audit (what exists today is dispatch + aggregation, not collaboration),
695
+ root causes (missing execution-authorization model, missing workspace model, missing shared-object model), and the M1–M7 module plan.
696
+ - `docs/COLLAB/M1-MAINTENANCE.md` — v1.3.2-beta design: signed maintenance requests executed by a root/SYSTEM unit through a queue
697
+ directory; remote update/restart/diag without SSH and without widening the allow-list.
698
+ - `docs/COLLAB/M2-TASK-GRANT.md` — v1.3.3-beta design: operator-signed per-execution grants (argv hash, machine ids, TTL, task binding),
699
+ agent-side verification, ledger audit, revocation; the allow-list stops being the only execution authority.
700
+ - `docs/COLLAB/M3-GIT-NATIVE-TASKS.md` — v1.3.4-beta design: per-task git worktrees, patch artifacts as the transportable result, a
701
+ serialized merge queue, and MERGE_CONFLICT as a first-class outcome.
702
+ - `docs/COLLAB/M4-PROJECT-ROOM.md` — v1.3.5-beta design: relay-side project objects (members, board, artifacts, memory, decisions,
703
+ refs), snapshot + incremental event protocol, role-based permissions.
704
+ - `docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md` — v1.3.6-beta design: branch/file locks, review tasks with findings and merge gating,
705
+ and notifications with delivery guarantees and read watermarks.
706
+ - `docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md` — v1.3.7-beta design: relay as the authoritative channel, chunked/resumable blobs,
707
+ agent result outbox, and P2P demoted to opportunistic acceleration.
708
+ - `docs/COLLAB/M7-METRICS-ACCEPTANCE.md` — v1.3.8-beta design: every capability must be triggerable by a real task, negative tests are
709
+ mandatory, metrics are computed by the relay, and evidence is checked by pre-flight.
710
+
711
+ ### What is explicitly not in this release
712
+
713
+ No code for M1–M7. The three model-level designs (v1.4.0 execution-authorization, v1.4.1 task, v1.4.2 collaboration) are not written yet.
714
+ The next stable release that changes behaviour will be the one that implements M1 + M2 (the remote-operations loop), with real-machine
715
+ acceptance evidence recorded as described in M7.
716
+
717
+ Verified: gate GREEN twice on this tree; the package differs from 1.3.1 only in documentation and version metadata.
718
+
10
719
  ## [1.3.1] — 2026-10-21
11
720
 
12
721
  **A red Compat and a green local gate: the loader-entry test was the gap.**