@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
@@ -0,0 +1,186 @@
1
+ # 多机 / 多客户端 / 跨网络协同:能力体检与可行方案
2
+
3
+ > 写作目的:回答一个具体问题 —— **本插件目前没有真正实现"多机协同完成一个任务/联合开发",能力缺口在哪,怎样补,先补什么。**
4
+ > 状态:设计提案(未实现)。所有"已实现"的判断都指向仓库里的具体模块;所有"未实现"都写了可验收的判据。
5
+
6
+ ---
7
+
8
+ ## 1. 能力体检:现在到底有什么
9
+
10
+ | 能力 | 现状 | 证据(代码/实测) |
11
+ |---|---|---|
12
+ | 控制面:多机派发、聚合、等待 | ✅ 可用 | `src/relay/server.mjs` 的 `/v1/task`、`src/agent/agent.mjs`、`w2m_run`/`w2m_wait`;实测 jp-shared 执行 `node --version` 成功 |
13
+ | 数据面:分片、缺分片、reduce 建议/执行 | 🟡 部分 | `src/util/dataflow.mjs`、`src/util/reduce-suggestion.mjs`、`reduce_of/reduce_inputs` + agent 取件(v1.3.1,实测 `REDUCE_INPUT_FETCH_FAILED` 生效) |
14
+ | 跨网络传输 | 🟡 中继可用,P2P 不可靠 | relay HTTP 是权威通道;P2P 有明文/`P2P_CRYPTO_PLAINTEXT_PEER` 拒绝,NAT 场景无成功记录 |
15
+ | 多机"联合改代码" | ❌ 没有 | 每台机器各跑各的副本;任务只声明 `base_commit` 做**一致性检查**,没有"产出补丁→合并→冲突"的工作流 |
16
+ | 多客户端(多人/多 DSH 实例)共事 | ❌ 没有 | 会话状态在各自 DSH;fleet chat 只解决"看消息",不解决共享任务/工作区/权限 |
17
+ | 机器运维(安装/升级/重启/排障) | ❌ 没有(本应是最基本的能力) | allow-list 是**静态配置**;TwinsEarth 实测连 `node --version` 都 `COMMAND_NOT_ALLOWED`;jp-shared 的升级只能靠我临时用 relay 任务通道 + 杀进程绕过 |
18
+ | 机器加入后的"可用性" | ❌ 默认不可用 | join/pair 成功 ≠ 能干活;空 allow-list 的机器在编队里只是"在线但不能执行任何东西" |
19
+
20
+ **一句话**:现在的能力是"一个操作员向若干预先配置好的只读/受限执行器派发命令",不是"多台机器 + 多个客户端协同完成一项工作"。
21
+
22
+ ---
23
+
24
+ ## 2. 根因(不是缺一个功能,是缺三个模型)
25
+
26
+ 1. **执行授权模型缺失**:allow-list 是机器本地静态配置,只覆盖创建它的人预先想到的命令。远程升级/排障/装插件全部落在清单外,于是"编队"退化成"只读观测网"。放宽 allow-list 不是答案(把任意命令暴露给任何能调度的人)。
27
+ 2. **任务模型缺"工作区"**:任务 = argv + 每台机器自己的项目副本 + 事后锚点检查。它假设"命令是幂等只读的";真实联合开发是"产出改动 → 审查 → 合并",需要**补丁/分支/合并队列/冲突**这些一等公民。
28
+ 3. **协作模型缺"共享对象"**:没有 relay 侧的 project/room/board/artifact 对象,所以多客户端各看各的;chat 是消息流,不是共享状态;memory 是 CAS 文档,不是任务上下文。
29
+
30
+ ---
31
+
32
+ ## 3. 设计原则(不牺牲现有安全边界)
33
+
34
+ 1. **不放宽 allow-list 来方便自己**:需要更强执行的场面,用**显式的、可审计的、带时限的授权**替代"清单加一条"。
35
+ 2. **执行与数据分离**:relay 负责授权、事件、租约、路由;机器只在自己边界内做**被授权**的事。
36
+ 3. **内容寻址 + 租约 + 事件日志**是现有底座,继续复用:blob sha256、`depends_on`/`assignee`、ledger 重放。
37
+ 4. **跨网络以 relay 为权威传输**:P2P 只做机会性加速,不承担正确性(现在它承担了一部分,是错误的)。
38
+ 5. **每个能力都要有"真实任务可触发"的验收**,不是单元测试通过就算数。
39
+
40
+ ---
41
+
42
+ ## 4. 模块方案(M1–M7)
43
+
44
+ ### M1 运维通道(maintenance agent)— 最高优先级
45
+
46
+ **目标**:编队里任何一台机器,都能在**操作员显式授权**下完成"更新自身/重启服务/收集诊断",不需要 SSH,也不需要曾经预先配置过命令。
47
+
48
+ **做法**:
49
+ - 包内新增固定入口 `bin/w2m-maintenance.mjs`(只接受 `--action update|restart|diag`,不做任意 shell)。
50
+ - 机器上的 systemd/Windows 服务用**独立单元**(root/SYSTEM 权限)监听一个请求队列目录(Linux:`/var/lib/w2m/maintenance/queue`,Windows 计划任务同理);agent(非特权用户)只能**写请求文件**,不能执行。
51
+ - 请求文件 = 操作员私钥签名 + 动作 + 目标 sha256 + 过期时间;root 单元校验签名/hash/TTL 后执行固定动作,结果写回 `/var/lib/w2m/maintenance/done/` 并由 agent 上报 relay。
52
+ - 这样"远程升级 TwinsEarth 插件"变成一次签名请求,而不是求人 SSH。
53
+
54
+ **验收**:操作员发一条 `w2m_update` 类任务 → 目标机器 2 分钟内插件到新版本并重启 → relay 事件日志可审计(谁、何时、hash、结果)。
55
+
56
+ ### M2 任务级签名授权(task grants)
57
+
58
+ **目标**:让"清单外命令"在**单次任务、单台机器、短时限**内可执行,而不是永久扩 allow-list。
59
+
60
+ **做法**:
61
+ - 复用既有 Ed25519 基础设施(`src/util/envelope-signature.mjs`、配对时登记的 device public key)。
62
+ - 操作员用独立密钥签 `grant = {argv_hash/mode, machine_ids, ttl, task_id, issued_at}`;relay 把 grant 随 offer 下发;agent 验签通过则跳过 allow-list(仍受红线:不跑 shell 字符串、cwd 限制、输出/时长上限)。
63
+ - grant 进 ledger,可审计、可撤销(撤销 = 停发新的)。
64
+
65
+ **验收**:对一台空 allow-list 的机器派发"一次性授权"命令并成功执行;同一命令无 grant 时仍被拒;grant 过期/换机/改 argv 全部被拒(负向测试)。
66
+
67
+ ### M3 Git 原生任务与合并队列(worktree + patch + merge)
68
+
69
+ **目标**:多机真正"联合完成一个代码任务"。
70
+
71
+ **做法**:
72
+ - 任务携带 `repo` + `base_commit` + `branch`;agent 在**每任务独立 worktree**里执行,结果 = `commit`/`patch` 制品(blob)+ 测试结果。
73
+ - relay 侧新增**合并队列**:同一分支的合并串行化(复用 lease/`depends_on`),每个 merge 任务由指定机器执行 `git merge`/`git am`,冲突作为**一等结果**(`MERGE_CONFLICT` + 冲突文件列表 + 冲突 patch),绝不静默覆盖。
74
+ - 合并后再跑锚点校验:`base_tree` 与声明一致才允许入队。
75
+
76
+ **验收**:两台机器改不同文件 → 自动合并成功;改同一文件 → 返回 `MERGE_CONFLICT` 且两人都能看到冲突内容;重放 ledger 后合并状态一致。
77
+
78
+ ### M4 项目房间(project room)
79
+
80
+ **目标**:多客户端看到同一个"项目"而不是各自会话。
81
+
82
+ **做法**:
83
+ - relay 新增 `project` 对象:members/roles(已有 role 模型可复用)、任务板、制品、memory 文档、chat 线程、决策记录。
84
+ - 客户端 API:`GET /v1/projects/:id`(快照)+ SSE/WebSocket 增量;DSH 插件侧新增 项目房间工具(未实现,暂用中性名)与面板视图。
85
+ - 任务/制品/决策都挂在 project 下,session 只是"视图"。
86
+
87
+ **验收**:两个不同 DSH 客户端(不同账号/机器)打开同一 project,看到相同的任务板/制品/决策;一个客户端创建任务,另一个 3 秒内看到。
88
+
89
+ ### M5 多客户端协作原语
90
+
91
+ - **占位/锁**:branch/file lease(复用租约机制),避免两个客户端同时改同一分支。
92
+ - **审查任务**:`review` 类型任务(指定 reviewer 机器/人),结果 = approve/changes-requested。
93
+ - **通知**:mentions(已有)+ 任务状态变化推送到项目房间。
94
+ - **决策**:council 决策落 memory(已实现雏形)→ 升级为 project 级决策账本。
95
+
96
+ **验收**:A 提交补丁 → B 收到审查任务 → B 批准 → 合并队列执行;A 持锁期间 B 不能改同一分支。
97
+
98
+ ### M6 跨网络与可靠性
99
+
100
+ - 明确 relay 是**权威数据通道**:blob 支持分块/断点续传(现在 16 MiB 硬上限、无断点);任务/事件支持断线重放(ledger 已具备)。
101
+ - P2P 降级为"机会性加速":P2P 失败不影响任务正确性;修掉明文拒绝与 NAT 不可测的问题(或直接默认 `relay` 模式,让 P2P 成为显式实验开关)。
102
+ - 离线机器:任务进入队列,机器回来后被 re-offer(已有 re-offer 基础)。
103
+
104
+ **验收**:把 relay 与执行机之一断网 60 秒,任务不丢、不重复执行,恢复后按租约重新派发;10 台机器并发派发/归并不丢事件。
105
+
106
+ ### M7 观测与验收
107
+
108
+ - 指标:任务成功率、合并冲突率、授权使用次数、每机器可用命令/授权状态、relay 事件延迟。
109
+ - 测试:复用现有双机/跨平台测试附件(`test/` 与 `deploy/` 有真机脚本),每个模块都要有"真实任务可触发"的集成测试 + 负向测试。
110
+
111
+ ---
112
+
113
+ ## 5. 落地路线(建议顺序)
114
+
115
+ ### Phase 1(1–2 周):先把"编队可用"变成真的
116
+
117
+ 1. M1 运维通道(update/restart/diag,签名请求 + root 单元)
118
+ 2. M2 任务级签名授权
119
+ 3. 机器加入后默认"可被操作员授权",不再是空 allow-list 死机器
120
+
121
+ **Phase 1 验收任务(真实场景)**:
122
+ - 在一台**空 allow-list** 的机器(TwinsEarth)上,仅凭一次操作员授权完成:插件升级到 1.3.1 → 重启 DSH → 回报版本。
123
+ - relay 升级同一套流程(替代本次"杀进程"操作)。
124
+ - 全程 ledger 可审计;无 grant 时同一命令被拒。
125
+
126
+ ### Phase 2(2–4 周):联合改代码
127
+
128
+ 4. M3 worktree/patch/merge 队列 + 冲突一等结果
129
+ 5. M4 项目房间(先做任务板 + 制品 + 决策,后做面板)
130
+ 6. 基于 M2 的 review 任务(M5 的审查部分)
131
+
132
+ **Phase 2 验收任务**:两台机器并行修一个 bug(不同文件)→ 自动合并成功并跑测试;同一文件冲突 → 冲突可见、可选人解决;两个客户端看到同一任务板。
133
+
134
+ ### Phase 3(4–6 周):多客户端与跨网络规模化
135
+
136
+ 7. M5 锁/审查/通知 + M4 面板完整视图
137
+ 8. M6 分块上传/断点重传/离线队列/P2P 降级
138
+ 9. M7 指标与混沌测试(断网、重启、机器掉线)
139
+
140
+ **Phase 3 验收任务**:3 个客户端 + 5 台机器共同完成"改代码→审查→合并→发布"一条链,期间重启 relay/断网各一次不丢任务。
141
+
142
+ ---
143
+
144
+ ## 6. 最小可行闭环(如果只做一件)
145
+
146
+ **"一次签名,远程升级任意编队机器"**(M1+M2 的最小交集):
147
+
148
+ 1. 操作员本地签 `grant`(argv=`node bin/w2m-maintenance.mjs --action update --version 1.3.1`,TTL 10 分钟,目标 machine_id)。
149
+ 2. 用 `w2m_run`/relay 派发;目标机器 maintenance 单元验签 → 下载 tarball(sha256 校验)→ 更新 → 重启 → 回报。
150
+ 3. 客户端 `w2m_wait` 看到 `status=done` + `version=1.3.1`;ledger 记录了 grant 与结果。
151
+
152
+ 所需新增:一个 `bin/w2m-maintenance.mjs`(~200 行)、一个 systemd/计划任务单元模板、`grant` 签名/验证(复用 envelope-signature,~150 行)、relay 对 grant 的透传/审计(~80 行)、插件工具 远程更新工具(未实现,暂用中性名)(~120 行)、双机负向测试。**这就是"最基本功能"的正确起点。**
153
+
154
+ ---
155
+
156
+ ## 7. 与现有代码的映射
157
+
158
+ | 新模块 | 复用 | 新增 |
159
+ |---|---|---|
160
+ | M1 maintenance | `bin/` 打包、systemd 单元生成经验(`deploy/server/`) | `bin/w2m-maintenance.mjs`、单元模板、队列协议 |
161
+ | M2 grants | `src/util/envelope-signature.mjs`、device public key 登记、ledger | grant 结构/签发/验证、relay 透传、agent 校验、撤销 |
162
+ | M3 worktree/merge | `src/util/dataflow.mjs`(制品哈希)、blob 通道、`depends_on`/lease | worktree 生命周期、merge 队列、`MERGE_CONFLICT` 结果、patch blob |
163
+ | M4 project room | `fleet-messages`、memory CAS、roles、council 决策账本 | project 对象、快照/增量 API、项目房间工具(未实现,暂用中性名) |
164
+ | M5 协作原语 | lease、mentions、review/approval 机制 | 分支/文件锁、review 任务类型、通知路由 |
165
+ | M6 可靠性 | ledger 重放、re-offer、blob 通道 | 分块/断点、离线队列、P2P 降级开关 |
166
+
167
+ ---
168
+
169
+ ## 8. 风险与取舍
170
+
171
+ | 风险 | 处置 |
172
+ |---|---|
173
+ | 签名 grant 变成新的后门 | 单次/单机/短 TTL/argv hash 绑定 + ledger 审计 + 可撤销;默认关闭 |
174
+ | maintenance 单元权限过大 | 只允许固定 action;不接受任意 shell;更新包必须 sha256 匹配 release 资产 |
175
+ | 合并队列成为瓶颈 | 按分支/路径分片串行;冲突直接回给提交者,不自动猜 |
176
+ | relay 仍是单点 | 短期接受;M6 做事件重放与多 relay 只读镜像(后续) |
177
+ | 多客户端一致性 | 以 relay 事件日志为唯一事实源;客户端只做视图,不做本地权威状态 |
178
+ | 现有 beta 已声明"多机",与实际不符 | 文档修正:把"多机派发/聚合"与"多机协同开发"分成两个能力级别,不再混写 |
179
+
180
+ ---
181
+
182
+ ## 9. 建议的下一步(需要用户决策)
183
+
184
+ 1. **批准 Phase 1 MVP**(M1+M2,远程运维),我用真实机器(TwinsEarth/jp-shared)作为验收对象,两周内出可演示闭环。
185
+ 2. 批准后同步修正 README/CHANGELOG 里"多机协同"的口径,避免把"派发/聚合"写成"联合开发"。
186
+ 3. Phase 2 的 worktree/merge 模型涉及产品取舍(中心化合并 vs 去中心 git),建议先做一个 3 页的对比决策再动手。
@@ -0,0 +1,193 @@
1
+ # 稳定版发布流程(Stable Release Runbook)
2
+
3
+ > 适用:本项目(`@twinsearth/w2m-dsh-plugin`)从版本决定到五方核验完成的全过程。
4
+ > 角色边界:**版本号与发布时间由用户决定**;执行者负责门禁、打包、推送、核验;**Stable 批准只能由用户操作**。
5
+ > 原则:门禁红绝不打 tag;先发布、后部署 relay;每个版本留证据。
6
+
7
+ ---
8
+
9
+ ## 0. 发布前条件(缺一不可)
10
+
11
+ ```
12
+ [ ] 用户已明确版本号(稳定版 X.Y.Z;预发布必须 X.Y.Z-beta.N)
13
+ [ ] 工作区没有未完成的半成品改动(未发布的功能不能混进稳定版)
14
+ [ ] 本机 PATH 包含打包的 node 目录(否则 agent 测试会误报
15
+ COMMAND_EXECUTABLE_PATH_MISMATCH)
16
+ [ ] 已阅读本次要发布的 CHANGELOG 条目(next section 写什么)
17
+ ```
18
+
19
+ PATH 准备(Windows):
20
+
21
+ ```powershell
22
+ $node = 'E:\DS\resources\runtime\primary-runtime\dependencies\node\bin\node.exe'
23
+ $env:PATH = (Split-Path $node) + ';' + $env:PATH
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 1. 版本 bump(5 个文件)
29
+
30
+ ```
31
+ package.json
32
+ plugin.json
33
+ .claude-plugin/plugin.json
34
+ .claude-plugin/marketplace.json (version + metadata.version + plugins[].version)
35
+ codex/mcp-servers.toml (@X.Y.Z)
36
+ ```
37
+
38
+ 注意:用 Node 写 JSON,**不要用 PowerShell `ConvertTo-Json` 回写**(会带 BOM,pack 直接崩)。
39
+
40
+ ---
41
+
42
+ ## 2. CHANGELOG
43
+
44
+ - 新条目放在**最上面**(newest-first)。
45
+ - 标题格式:`## [X.Y.Z] — YYYY-MM-DD`。
46
+ - 必须写:改了什么、没有改什么(明确不包含项)、已知限制、验证数字。
47
+ - 排序规则由 `claims` 检查:**prerelease 按 semver 比较**(`X.Y.Z-beta.2 > X.Y.Z-beta.1`)。
48
+ - `package.json` 版本必须等于最新条目版本(claims 断言)。
49
+
50
+ ---
51
+
52
+ ## 3. 打包与门禁(两遍)
53
+
54
+ ```powershell
55
+ Remove-Item dist -Recurse -Force -ErrorAction SilentlyContinue
56
+ & $node scripts/pack.mjs
57
+ # 记录:bytes 与 sha256
58
+
59
+ & $node scripts/preflight.mjs # 第一遍
60
+ & $node scripts/preflight.mjs # 第二遍(必须同样 GREEN)
61
+ ```
62
+
63
+ 通过标准:
64
+
65
+ ```
66
+ pre-flight: 15 pass, 0 fail, 1 unverifiable(host 环境项)
67
+ declared suite: N tests / pass / 0 fail / 14 skip
68
+ ```
69
+
70
+ 红灯常见原因与修法:
71
+
72
+ | 失败项 | 原因 | 修法 |
73
+ |---|---|---|
74
+ | lint | 未使用 import、Promise executor 返回、未定义变量 | 修代码;不得用 eslint-disable 绕过 |
75
+ | declared suite | 测试失败 | 先跑单个文件定位;PATH 问题看 `COMMAND_EXECUTABLE_PATH_MISMATCH` |
76
+ | claims | 版本顺序 / package 与最新条目不一致 / 文档未声明 / 未注册工具名 | 按断言修;新文档要么登记 shipped 要么排除并写理由 |
77
+ | layer-model | 新模块/新跨层 import 未声明 | 在 `scripts/layer-model.mjs` 声明层与允许边(不要放宽成环形) |
78
+ | packed tarball | dist 陈旧 / 文档缺失 / 工具数不符 | 删除 dist 重 pack;同步 verify-installed/README/preflight 的计数 |
79
+ | CHANGELOG shape | 非降序 / 格式不符 | 调整条目位置与标题 |
80
+
81
+ **门禁不绿:停止,不 commit,不打 tag。**
82
+
83
+ ---
84
+
85
+ ## 4. 提交与打 tag
86
+
87
+ ```
88
+ 1. 写 commit 消息(release: vX.Y.Z — <一句话主题>,正文列变更与验证数字)
89
+ 2. 用推送脚本 commit + push main,取回 COMMIT_SHA(脚本输出 COMMIT_SHA=...)
90
+ 3. 用 GitHub API 建 tag:POST /repos/<owner>/<repo>/git/refs
91
+ { "ref": "refs/tags/vX.Y.Z", "sha": COMMIT_SHA }
92
+ 4. 轮询 Release workflow:waiting 出现 = 进入 Stable 环境审批
93
+ ```
94
+
95
+ ```powershell
96
+ # 轮询示例(node)
97
+ # /repos/<repo>/actions/runs?per_page=8 → 找 name === 'Release' 且 head_sha 匹配
98
+ # jobs 里有 status === 'waiting' → 等待审批
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 5. Stable 审批(只能用户做)
104
+
105
+ ```
106
+ GitHub → Actions → release run → Review deployments → Stable → Approve
107
+ ```
108
+
109
+ 审批后 workflow 继续:verify → pack → publish(GitHub release + npm)。
110
+
111
+ ---
112
+
113
+ ## 6. 五方核验(全部通过才算发布完成)
114
+
115
+ ```
116
+ 1. npm:latest 变为 X.Y.Z(注意传播延迟,可重试)
117
+ 2. GitHub release:prerelease=false;资产包含 tgz + SHA256SUMS
118
+ 3. 资产校验:下载 tgz 实算 sha256,必须与 SHA256SUMS 一致
119
+ 4. Packages:dispatch packages workflow(inputs.version=X.Y.Z),job success
120
+ 5. CI:该 commit 的 ci.yml run success
121
+ 6. Compat:该 commit 的 compat.yml run success(红则视为不可发布,见下)
122
+ ```
123
+
124
+ Compat 红的高频原因:打包产物与 loader gate 的 `EXPECTED_TOOLS` 不一致
125
+ (v1.3.0 实例)。定位方法:看 Compat 日志的 “registers every w2m_* tool” 子测试,
126
+ 它会打印 live tools 与 expected 的差异。
127
+
128
+ ---
129
+
130
+ ## 7. relay 部署(发布包含 relay 变更时)
131
+
132
+ 用**官方 release 资产**(sha 与五方核验一致),不要用本地 pack 产物。
133
+
134
+ ```bash
135
+ # SSH(或 fleet 通道以 w2m 用户执行)
136
+ cd /opt/w2m && rm -rf app.bak && cp -a app app.bak
137
+ # 上传官方 tgz 到 /tmp
138
+ bash /opt/w2m/app.bak/deploy/server/deploy-w2m.sh /tmp/<tgz> <sha256>
139
+ # 若 systemd 拒绝重启(非 root):kill -TERM <MainPID> 触发 Restart=always
140
+ systemctl status w2m-rabbit w2m-stun w2m-localside --no-pager
141
+ ```
142
+
143
+ 线上复核(必须做):
144
+
145
+ ```
146
+ /healthz 200;relay_id 变化、uptime 归零(证明是新进程)
147
+ /v1/fleet/members 200;未认证 /v1/fleet/messages 401
148
+ 任务派发 → 目标机执行成功(plain task)
149
+ 新版本特性路径生效(如 reduce 取件失败码、maintenance 字段透传)
150
+ 磁盘占用正常;app.old 保留(回滚材料)
151
+ ```
152
+
153
+ 回滚:
154
+
155
+ ```bash
156
+ systemctl stop w2m-rabbit w2m-stun w2m-localside
157
+ mv /opt/w2m/app /opt/w2m/app.1 && mv /opt/w2m/app.old /opt/w2m/app
158
+ systemctl start w2m-rabbit w2m-stun w2m-localside
159
+ ```
160
+
161
+ ---
162
+
163
+ ## 8. 收尾
164
+
165
+ ```
166
+ [ ] 证据入库:docs/acceptance/<version>/<case>.json(真实任务 task_id + 断言结果)
167
+ [ ] CHANGELOG 的验证数字与实际门禁输出一致
168
+ [ ] 需要时更新各机器插件(TwinsEarth / macmini / ToFuture),并在发布说明中标注 relay 版本
169
+ [ ] 若本版本被后续版本纠正,写明代替关系(如 v1.3.0 → v1.3.1)
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 9. 本会话踩过的坑(已修复,写下来防止复发)
175
+
176
+ 1. **PATH 缺 node 目录** → agent 测试 18 个 `COMMAND_EXECUTABLE_PATH_MISMATCH`。跑门禁前先 prepend。
177
+ 2. **PowerShell 回写 JSON 带 BOM** → pack 报 `Unexpected token ''`。用 Node 写 JSON。
178
+ 3. **CHANGELOG 比较器忽略 prerelease** → beta.1/beta.2 相等、顺序断言互相矛盾。已改成 semver 比较。
179
+ 4. **陈旧 dist** → “packed package.json 与 tree 不一致”。发布前删除 dist 重 pack。
180
+ 5. **工具数变化(16 tools)** 需要同步:README、verify-installed、preflight packed-tarball 计数、tool-cards、claims、update-wiring、p2p-plugin、marketplace 清单。
181
+ 6. **Compat 只在打包产物+真实 profile 暴露**:本地 loader 检查可能跳过,必须看 Compat 的 capture 输出。
182
+ 7. **npm 传播延迟**:dist-tag 未更新不等于发布失败,重试读取。
183
+ 8. **relay 上的 systemd 权限**:w2m 用户不可 systemctl;要么 root(SSH/控制台),要么 kill 触发 Restart=always(并确认 localside 也会回来)。
184
+
185
+ ---
186
+
187
+ ## 10. 角色与红线(再次强调)
188
+
189
+ ```
190
+ 用户:决定版本号、批准 Stable、决定是否把 beta 部署到生产 relay
191
+ 执行者:门禁、打包、推送、五方核验、部署与线上复核
192
+ 红线:门禁红不打 tag;不用 beta 冒充稳定版;不未经用户同意部署生产;不自行决定版本号
193
+ ```
@@ -0,0 +1,27 @@
1
+ {
2
+ "version": "1.3.10-beta.10",
3
+ "case": "M1-LIVE-DIAG",
4
+ "kind": "positive",
5
+ "requirement": "a signed maintenance request is executed by the root runner on a remote machine and its result returns through the fleet",
6
+ "machines": [
7
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
8
+ ],
9
+ "commands": [
10
+ [
11
+ "node",
12
+ "--version"
13
+ ]
14
+ ],
15
+ "task_id": "01M5G3HVZPNN8ES63NJPV7MRN5",
16
+ "result": {
17
+ "outcome": "ok",
18
+ "status": "ok",
19
+ "stdout_bytes": 62238,
20
+ "blob_sha256": null,
21
+ "blob_sha256_verified": false,
22
+ "sections": null
23
+ },
24
+ "executed_at": "2026-10-21T20:51:34.767Z",
25
+ "executor": "lead",
26
+ "note": "relay v1.3.10-beta.5; agent v1.3.10-beta.5 (locally patched envelope fields); operator key pinned at bootstrap; result delivered as a blob because the diag output exceeds the inline cap"
27
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": "1.3.10-beta.10",
3
+ "case": "M1-LIVE-POSTUPDATE-DIAG",
4
+ "kind": "positive",
5
+ "requirement": "after an update to beta.10 and a restart, a new diag request still executes end to end",
6
+ "machines": [
7
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
8
+ ],
9
+ "task_id": "01M5G4M6EJ2G62P1FBHDS1FE9F",
10
+ "result": {
11
+ "outcome": "ok",
12
+ "status": "ok",
13
+ "stdout_bytes": 59581,
14
+ "refusal": null
15
+ },
16
+ "executed_at": "2026-10-21T21:07:14.358Z",
17
+ "executor": "lead"
18
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": "1.3.10-beta.10",
3
+ "case": "M1-LIVE-RESTART",
4
+ "kind": "positive",
5
+ "requirement": "a signed restart request restarts the relay and the fleet result survives the restart",
6
+ "machines": [
7
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
8
+ ],
9
+ "task_id": "01M5G3R8DJ2RNP0SNSC5J3WQWQ",
10
+ "result": {
11
+ "task": {
12
+ "outcome": "ok",
13
+ "status": "ok",
14
+ "refusal": null,
15
+ "stdout_bytes": 369
16
+ },
17
+ "relay_id_before": "68c2d7c3fb85bdc0",
18
+ "relay_id_after": "78446e30540b29b6",
19
+ "uptime_ms_after": 5710,
20
+ "operator_key_id": "79bcd7a23ad5b61f"
21
+ },
22
+ "executed_at": "2026-10-21T20:52:01.288Z",
23
+ "executor": "lead"
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": "1.3.10-beta.10",
3
+ "case": "M1-LIVE-UPDATE",
4
+ "kind": "positive",
5
+ "requirement": "a signed update request downloads, verifies and swaps the package on a remote machine",
6
+ "machines": [
7
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
8
+ ],
9
+ "task_id": "01M5G4K9DY2K4BS0PDKVT555M1",
10
+ "result": {
11
+ "task": {
12
+ "outcome": "ok",
13
+ "status": "ok",
14
+ "refusal": null,
15
+ "stdout_bytes": 488,
16
+ "stdout_head": ""
17
+ },
18
+ "artifact_sha256": "b5a89ead5f0d94bc4a83d27f76ea2649c87a64af70f72cac3309fa13ecbdbb7c",
19
+ "restart_after_update": false,
20
+ "operator_key_id": "79bcd7a23ad5b61f"
21
+ },
22
+ "executed_at": "2026-10-21T21:06:44.656Z",
23
+ "executor": "lead"
24
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": "1.3.11",
3
+ "case": "M1-LIVE-DIAG",
4
+ "kind": "positive",
5
+ "requirement": "the stable 1.3.11 relay executes a signed maintenance diag request on a real machine and returns its result",
6
+ "machines": [
7
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
8
+ ],
9
+ "commands": [
10
+ [
11
+ "node",
12
+ "--version"
13
+ ]
14
+ ],
15
+ "task_id": "01M5GF2Z2EYKDJ39YYY9WJNK8C",
16
+ "result": {
17
+ "outcome": "ok",
18
+ "status": "ok",
19
+ "stdout_bytes": 60781,
20
+ "relay_id": "bcd9d427bfd5bbd9",
21
+ "deployed_version": "1.3.11"
22
+ },
23
+ "executed_at": "2026-10-22T00:10:03.977Z",
24
+ "executor": "lead",
25
+ "note": "relay deployed from the packed 1.3.11 candidate over SSH; operator key pinned at bootstrap; result delivered through the relay"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": "1.3.12-beta.1",
3
+ "case": "M1-LIVE-DIAG",
4
+ "kind": "positive",
5
+ "requirement": "the 1.3.12-beta.1 candidate executes a signed maintenance diag request on the relay host",
6
+ "machines": [
7
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
8
+ ],
9
+ "commands": [
10
+ [
11
+ "node",
12
+ "--version"
13
+ ]
14
+ ],
15
+ "task_id": "01M5GH0HHGSVP1GXY286TV7ZDG",
16
+ "result": {
17
+ "outcome": "ok",
18
+ "status": "ok",
19
+ "stdout_bytes": 62525,
20
+ "relay_id": "4406aed03a8db1d7",
21
+ "deployed_version": "1.3.12-beta.1"
22
+ },
23
+ "executed_at": "2026-10-22T00:43:43.399Z",
24
+ "executor": "lead",
25
+ "note": "candidate packed from the working tree before tagging; operator key pinned at bootstrap"
26
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "1.4.0-beta.1",
3
+ "machines": [
4
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
5
+ ],
6
+ "executed_at": "2026-10-22T01:56:11.375Z",
7
+ "executor": "lead",
8
+ "case": "M1-LIVE-MAINTENANCE",
9
+ "kind": "positive",
10
+ "requirement": "the 1.4.0 candidate executes signed maintenance requests on the relay host",
11
+ "task_id": "01M5GMT4W230CNSQXCH75M071H",
12
+ "result": {
13
+ "outcome": "blocked",
14
+ "assertions": {
15
+ "diag_when_agent_online": true,
16
+ "candidate_m1_code_same_as_accepted_1_3_11": true
17
+ }
18
+ },
19
+ "note": "the candidate swap is done and the relay-side regression passed; w2m-localside was not restarted by the unprivileged deploy path, so the ferried diag could not run after the swap. Known operational limitation; resolved by systemctl restart w2m-localside."
20
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "version": "1.4.0-beta.1",
3
+ "machines": [
4
+ "1c14d7d2-d9e2-43dd-a7e5-eda98f4864bc"
5
+ ],
6
+ "executed_at": "2026-10-22T01:56:11.375Z",
7
+ "executor": "lead",
8
+ "case": "LIVE-REGRESSION-140",
9
+ "kind": "positive",
10
+ "requirement": "the 1.4.0 candidate passes the relay-side live regression set on the real relay host",
11
+ "task_id": "01M5GMT4W230CNSQXCH75M071H",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "relay health": {
16
+ "ok": true,
17
+ "detail": {
18
+ "relay_id": "e09a7086b2d4fff7",
19
+ "uptime_ms": 258132,
20
+ "devices": 8
21
+ }
22
+ },
23
+ "M4 board": {
24
+ "ok": true,
25
+ "detail": {
26
+ "tasks": 18
27
+ }
28
+ },
29
+ "M4 artifacts": {
30
+ "ok": true,
31
+ "detail": {
32
+ "count": 1
33
+ }
34
+ },
35
+ "M4 decisions": {
36
+ "ok": true,
37
+ "detail": {
38
+ "count": 2
39
+ }
40
+ },
41
+ "M4 memory": {
42
+ "ok": true,
43
+ "detail": {
44
+ "version": 2
45
+ }
46
+ },
47
+ "M5 locks": {
48
+ "ok": true,
49
+ "detail": {
50
+ "conflict": "LOCK_HELD"
51
+ }
52
+ },
53
+ "M6 blobs": {
54
+ "ok": true,
55
+ "detail": {
56
+ "bytes": 35,
57
+ "range": 206
58
+ }
59
+ },
60
+ "TASK refs/merge": {
61
+ "ok": true,
62
+ "detail": {
63
+ "head": "cccccccccccccccccccccccccccccccccccccccc"
64
+ }
65
+ },
66
+ "COLLAB notifications": {
67
+ "ok": true,
68
+ "detail": {
69
+ "count": 9
70
+ }
71
+ },
72
+ "COLLAB project SSE": {
73
+ "ok": true,
74
+ "detail": {
75
+ "status": 200
76
+ }
77
+ }
78
+ }
79
+ },
80
+ "note": "candidate deployed through the fleet node channel; M1 maintenance acceptance tracked separately (agent service restart pending after the unprivileged deploy path)"
81
+ }