@twinsearth/w2m-dsh-plugin 1.3.1 → 1.3.11

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 (52) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +588 -0
  4. package/bin/w2m-maintenance.mjs +112 -0
  5. package/codex/mcp-servers.toml +1 -1
  6. package/deploy/maintenance/bootstrap.ps1 +30 -0
  7. package/deploy/maintenance/bootstrap.sh +59 -0
  8. package/deploy/server/w2m-maintenance.path +10 -0
  9. package/deploy/server/w2m-maintenance.service +20 -0
  10. package/deploy/windows/w2m-maintenance.xml +31 -0
  11. package/docs/COLLAB/COLLABORATION-MODEL.md +240 -0
  12. package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +250 -0
  13. package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +70 -0
  14. package/docs/COLLAB/M1-MAINTENANCE.md +313 -0
  15. package/docs/COLLAB/M2-TASK-GRANT.md +231 -0
  16. package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +291 -0
  17. package/docs/COLLAB/M4-PROJECT-ROOM.md +260 -0
  18. package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +263 -0
  19. package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +231 -0
  20. package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +263 -0
  21. package/docs/COLLAB/TASK-MODEL.md +281 -0
  22. package/docs/MULTI-MACHINE-COLLABORATION-PLAN.md +186 -0
  23. package/docs/RELEASE-STABLE.md +193 -0
  24. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json +27 -0
  25. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-POSTUPDATE-DIAG.json +18 -0
  26. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-RESTART.json +24 -0
  27. package/docs/acceptance/1.3.10-beta.10/M1-LIVE-UPDATE.json +24 -0
  28. package/docs/acceptance/1.3.11/M1-LIVE-DIAG.json +26 -0
  29. package/package.json +22 -4
  30. package/plugin.json +1 -1
  31. package/src/acceptance/evidence.mjs +79 -0
  32. package/src/agent/agent.mjs +64 -2
  33. package/src/agent/grant-gate.mjs +82 -0
  34. package/src/agent/maintenance-ferry.mjs +111 -0
  35. package/src/agent/outbox.mjs +138 -0
  36. package/src/agent/patch-artifact.mjs +53 -0
  37. package/src/agent/worktree.mjs +109 -0
  38. package/src/maintenance/actions.mjs +203 -0
  39. package/src/maintenance/grant.mjs +139 -0
  40. package/src/maintenance/operator-key.mjs +151 -0
  41. package/src/maintenance/protocol.mjs +204 -0
  42. package/src/maintenance/runner.mjs +189 -0
  43. package/src/plugin/tools.mjs +1 -1
  44. package/src/relay/blob-sessions.mjs +148 -0
  45. package/src/relay/merge-queue.mjs +137 -0
  46. package/src/relay/project-locks.mjs +179 -0
  47. package/src/relay/project-notifications.mjs +182 -0
  48. package/src/relay/project-reviews.mjs +161 -0
  49. package/src/relay/project-room.mjs +163 -0
  50. package/src/relay/reliability-metrics.mjs +180 -0
  51. package/src/relay/server.mjs +164 -1
  52. package/src/relay/state.mjs +128 -1
@@ -0,0 +1,231 @@
1
+ # M2 任务级签名授权(grant)设计规格
2
+
3
+ > 状态:**设计稿**。只定义协议、边界、失败语义与验收,不含实现。
4
+ > 版本:`1.3.3-beta.*`(GitHub prerelease,不入 npm)。
5
+ > 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` M2;与 `docs/COLLAB/M1-MAINTENANCE.md` 共用签名/规范化模块。
6
+
7
+ ---
8
+
9
+ ## 1. 目标与非目标
10
+
11
+ ### 目标
12
+
13
+ 1. **单次授权**:操作员对"某台机器上执行某条具体 argv"签一次名,agent 验签后放行一次;无需把该命令写进永久 allow-list。
14
+ 2. **可审计**:每次使用 grant 的任务在 relay ledger 中留下 `grant_id / argv_hash / machine / 结果`,能回答"这条命令是谁授权的、什么时候、在哪些机器上"。
15
+ 3. **可撤销**:停止签发新 grant;对被撤销的 grant_id,agent 从 relay 的撤销集拒绝执行(在 TTL 窗口内也拒绝)。
16
+ 4. **与 allow-list 共存**:allow-list 仍是默认路径(只读/常规命令);grant 是**显式例外通道**,不是替代品。
17
+ 5. **不放宽红线**:仍然只接受 argv 数组(不接受 shell 字符串)、仍受 cwd 限制、超时、输出上限、写范围约束。
18
+
19
+ ### 非目标(本版本不做)
20
+
21
+ - 不做"任意命令的长期通行证"(grant 必须绑定 argv_hash + TTL + 机器集合)。
22
+ - 不做交互式会话(不是 SSH 替代品)。
23
+ - 不做提权(agent 仍以自身用户执行;root 动作只走 M1 的固定 action)。
24
+ - 不做"操作员密钥硬件保护"(HSM/智能卡属后续)。
25
+
26
+ ---
27
+
28
+ ## 2. 授权分层(本版本落地 L2,L1 沿用)
29
+
30
+ ```
31
+ L0 allow-list(机器本地静态配置,v1.3.1 已有)
32
+ L1 签名维护请求(M1:固定 action,root 单元执行;不走 allow-list)
33
+ L2 任务级 grant(本版本:任意 argv 的单次授权;agent 侧验签)
34
+ ```
35
+
36
+ 判定顺序(agent 收到 offer 时):
37
+
38
+ ```
39
+ 1. 任务是否带 grant?
40
+ 2. 有 grant → 验证(见 §5)→ 通过则**跳过 allow-list**,进入执行前检查(cwd/写范围/超时)
41
+ 拒绝则按 grant 错误码返回;不回落 allow-list(防止"签错名但恰好命令在清单里"的歧义)
42
+ 3. 无 grant → 走 allow-list(行为与 v1.3.1 完全相同)
43
+ ```
44
+
45
+ 边界规则:
46
+
47
+ | 场景 | 结果 |
48
+ |---|---|
49
+ | allow-list 命中的普通命令 | 照旧执行,ledger 记录 `auth: "allow-list"` |
50
+ | grant 命中的命令(即使也在 allow-list 里) | 执行,ledger 记录 `auth: "grant"` + grant_id(审计优先显示显式授权) |
51
+ | 两者都不命中 | `COMMAND_NOT_ALLOWED`(与现在一致) |
52
+ | grant 无效 | 对应 `GRANT_*` 错误码,不执行 |
53
+
54
+ ---
55
+
56
+ ## 3. Grant 对象(签名载荷)
57
+
58
+ UTF-8 JSON,**≤ 8 KiB**,字段如下(签名覆盖除 `signature` 外全部):
59
+
60
+ ```json
61
+ {
62
+ "protocol": "w2m-grant/v1",
63
+ "grant_id": "01M6...26 字符 ULID",
64
+ "task_id": "被授权任务的 id(可空;非空时必须与 offer 的 task_id 相等)",
65
+ "machine_ids": ["1c14d7d2-…", "6c92b251-…"],
66
+ "argv_hash": "sha256(canonical argv 数组)",
67
+ "argv_head": ["node", "-e", "console.log('hi')"],
68
+ "max_uses": 1,
69
+ "issued_at": "2026-10-21T13:00:00Z",
70
+ "expires_at": "2026-10-21T13:10:00Z",
71
+ "nonce": "16–64 字符随机串",
72
+ "issued_by": "operator 名(仅审计)",
73
+ "signature": { "alg": "ed25519", "key_id": "operator 公钥指纹前 16 hex", "value_base64": "…" }
74
+ }
75
+ ```
76
+
77
+ 规范化:键按字典序、无空白、UTF-8;`argv_hash = sha256(JSON.stringify(argv))`,argv 为字符串数组原样(不做 shell 解析、不做路径展开)。
78
+
79
+ ### 3.1 约束字段(同一 grant 内可再收紧执行面)
80
+
81
+ ```json
82
+ "mode": "replicate|broadcast", // 可选;与 task 不一致则拒绝
83
+ "cwd_rel": ".", // 可选;与 task 不一致则拒绝
84
+ "write": false, // 可选;grant 不能把 write 从 false 放宽到 true
85
+ "write_scope": ["src/"] // 可选;只能收窄,不能扩大机器配置
86
+ ```
87
+
88
+ 原则:**grant 只能收紧,不能放大机器自身的执行约束**(写范围、cwd、超时、输出上限都由机器说了算;grant 只能进一步限制)。
89
+
90
+ ---
91
+
92
+ ## 4. Grant 的签发与下发
93
+
94
+ ### 4.1 签发(控制端插件内)
95
+
96
+ - 操作员密钥:独立 Ed25519(与 M1 共用同一 operator key;**不复用** operator token 明文)。
97
+ - 输入:argv、目标 machine_ids、TTL(≤10 分钟)、可选 task_id/max_uses。
98
+ - 输出:grant JSON + 签名;控制端把 grant 随任务派发(POST /v1/task 的 `grant` 字段)。
99
+ - 操作员看到的是**展开的 argv 列表**(`argv_head`),签名覆盖 hash;UI/CLI 必须展示完整 argv 后才允许签名(防止"签 hash 但不知道内容")。
100
+
101
+ ### 4.2 relay 行为
102
+
103
+ 1. POST /v1/task 接收 `grant`(≤8 KiB):
104
+ - 校验形状与协议版本(不验签,验签权威在 agent);
105
+ - 拒绝 `machine_ids` 之外的 lease(只给列出的机器 offer);
106
+ - 校验 `expires_at` 未过期(早拒);过期拒绝 `GRANT_EXPIRED`。
107
+ 2. ledger 写 `task.created` 时附 `grant: {grant_id, argv_hash, key_id, max_uses, expires_at}`(**不存完整签名**,避免膨胀;完整 grant 随 offer 下发)。
108
+ 3. offer 携带 `grant`;agent 验签。
109
+ 4. GET task 的机器结果里暴露 `grant_status`(verified/refused + 错误码)供审计。
110
+ 5. 撤销集:relay 内存 + ledger 记录 `grant.revoked` 事件;GET /v1/fleet/grants/revoked 供 agent 拉取(缓存 30s)。
111
+
112
+ ### 4.3 agent 验签与使用
113
+
114
+ ```
115
+ 1. 形状/大小/协议版本
116
+ 2. 找 operator 公钥:本地 pinned key(优先)→ relay 下发(TOFU,见 §6)
117
+ 3. 验签(canonical 重构)
118
+ 4. 时间窗口:issued_at ≤ now ≤ expires_at(时钟偏差容忍 ±60s)
119
+ 5. machine_ids 包含本机
120
+ 6. task_id 若存在必须等于 offer.task_id
121
+ 7. argv_hash 等于 sha256(canonical argv)(逐字节比较)
122
+ 8. max_uses:本地持久化使用计数;单机视角计数(同一 grant 给 N 台机器时每台最多 max_uses 次)
123
+ 9. nonce 未在本地见过(防重放,跨重启保留)
124
+ 10. 撤销检查(relay 撤销集缓存命中则拒绝)
125
+ 11. mode/cwd_rel/write/write_scope 与任务实际值一致或更严格
126
+ 12. 通过 → 执行;执行结果附 grant_status:"verified"、grant_id、used 次数
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 5. 失败码(全部为任务级 refusal,不抛异常)
132
+
133
+ ```
134
+ GRANT_MISSING 任务要求 grant 但没带(仅当任务显式声明 require_grant)
135
+ GRANT_UNSUPPORTED_AGENT 旧 agent 不认识 grant(relay 侧探测后提示)
136
+ GRANT_BAD_SIGNATURE 签名不符
137
+ GRANT_UNKNOWN_KEY key_id 找不到公钥
138
+ GRANT_EXPIRED 过期 / 未到 issued_at / TTL>10min
139
+ GRANT_WRONG_MACHINE machine_ids 不含本机
140
+ GRANT_WRONG_TASK task_id 与 offer 不一致
141
+ GRANT_ARGV_MISMATCH argv_hash 不符
142
+ GRANT_REPLAY nonce/ grant_id 已使用(超 max_uses)
143
+ GRANT_REVOKED 撤销集命中
144
+ GRANT_CONSTRAINT_VIOLATION mode/cwd/write 与任务不符
145
+ ```
146
+
147
+ 每个失败码都要有自动化负向测试(§8)。
148
+
149
+ ---
150
+
151
+ ## 6. 密钥分发与信任模型(本版本的关键设计点)
152
+
153
+ **问题**:agent 要验签,就需要操作员公钥。从哪里来?
154
+
155
+ **本版本(1.3.3-beta)方案:TOFU + 轮换链**
156
+
157
+ 1. relay 维护 operator 公钥登记:操作员用 operator token(已有的持有证明)调用登记接口,登记 `{key_id, public_key, issued_at, rotation_of?}`;relay 记录 ledger。
158
+ 2. agent 首次需要验签时向 relay 拉取公钥(`GET /v1/fleet/operators/:key_id`),**首次接受即固定(TOFU)** 存本地 state。
159
+ 3. 之后任何公钥变更必须由**当前 pinned key 签名**(轮换链:`rotation_of = 旧 key_id`,签名覆盖新公钥),agent 验旧签后更新 pinned key;链断则拒绝并告警。
160
+ 4. 撤销的公钥进入撤销集,agent 同步后拒绝。
161
+
162
+ **信任声明(写进文档与告警)**:TOFU 阶段,**被攻破的 relay 可以在 agent 首次验签时注入伪造公钥**。这是 1.3.3-beta 的已知限制;缓解与硬化:
163
+
164
+ - 短时间内:配对响应里携带 operator 公钥(pairing 时操作员在场,agent 固定),已有机器通过一条"bootstrap"指令完成 pin;
165
+ - 后续版本(M2.1):强制 pairing-time pinning + 轮换链校验 + relay 侧公钥透明日志(append-only,agent 可校验历史)。
166
+
167
+ ---
168
+
169
+ ## 7. 与 M1 的关系
170
+
171
+ - 共用:`src/maintenance/protocol.mjs` 的规范化 + Ed25519 验签(M1 放在 root runner,M2 放在 agent)。
172
+ - 差异:M1 的 request 是**固定 action**(update/restart/diag,root 权限);M2 的 grant 是**任意 argv**(agent 权限)。两者都用 operator key 签名,都不扩 allow-list。
173
+ - 组合场景:远程升级 = M1 的 update(root runner);远程"运行升级脚本并检查" = M2 的 grant(agent 权限)。两者不互相替代。
174
+
175
+ ---
176
+
177
+ ## 8. 验收
178
+
179
+ ### 正向(真实任务,必须实跑)
180
+
181
+ 1. **空 allow-list 机器执行清单外命令**:TwinsEarth(allow-list 为空)在 grant 授权下执行 `node -e "console.log(process.version)"` 成功,ledger 记录 `auth: "grant"` + grant_id。
182
+ 2. **常规命令不受影响**:同一台机器无 grant 的 `npm --version` 仍然 `COMMAND_NOT_ALLOWED`。
183
+ 3. **多机 grant**:grant 同时授权 jp-shared + TwinsEarth,两台都成功(各自 max_uses 计数独立)。
184
+ 4. **写约束**:grant 带 `write:false` 时写文件的任务被拒绝(`GRANT_CONSTRAINT_VIOLATION` 或写入被拦)。
185
+ 5. **撤销**:签发后立即撤销 → 同一 grant 再派发被 `GRANT_REVOKED` 拒绝。
186
+
187
+ ### 负向(自动化,进单测/集成)
188
+
189
+ ```
190
+ 未签名/错签/未知 key_id/过期/未来时间/TTL>10min/换机/换 task_id/改 argv/重放/
191
+ 超 max_uses/撤销/约束冲突/大小超限/旧 agent 不认识 grant
192
+ → 全部对应错误码,任务不执行,ledger 留痕
193
+ ```
194
+
195
+ ### 审计验收
196
+
197
+ - ledger 中每个 grant 任务:`grant_id / key_id / argv_hash / machine / outcome / grant_status` 可查。
198
+ - 控制端 `w2m_history` 能看到该任务属于 grant 授权(新字段),不需要翻签名原文。
199
+
200
+ ---
201
+
202
+ ## 9. 兼容与发布
203
+
204
+ - `1.3.3-beta.1` 起;beta 政策:GitHub prerelease,不入 npm。
205
+ - 旧 relay:忽略 `grant` 字段 → 任务按 allow-list 走(可能被拒);控制端检测到旧 relay 返回 `MAINTENANCE_UNSUPPORTED_RELAY`/`GRANT_UNSUPPORTED_RELAY` 提示。
206
+ - 旧 agent:收到带 grant 的 offer 不认识该字段(按现有校验丢失)→ 走 allow-list 拒绝;relay 侧可先行探测 agent 版本并给出 `GRANT_UNSUPPORTED_AGENT`。
207
+ - 不改变 task/offer 现有字段语义,只新增可选字段(`grant`、结果里的 `grant_status`)。
208
+
209
+ ---
210
+
211
+ ## 10. 交付物清单(实现阶段)
212
+
213
+ ```
214
+ src/auth/operator-key.mjs operator key 生成/登记/轮换链/TOFU pin(与 M1 共用)
215
+ src/auth/grant.mjs grant 结构、canonical、签发、验证、撤销集
216
+ src/agent/agent.mjs 验签与使用计数、撤销缓存、结果字段(核心改动)
217
+ src/relay/state.mjs grant 透传、ledger 事件、撤销集、operator 公钥登记
218
+ src/relay/server.mjs POST /v1/task 的 grant 字段、/v1/fleet/operators、撤销集接口
219
+ src/plugin/tools.mjs 控制端签发入口(展示完整 argv → 签名 → 派发 → 等待)
220
+ test/grant.test.mjs 单元:规范化/签名/失败码矩阵
221
+ test/grant-e2e.test.mjs 双机模拟 relay:验签/使用计数/重放/撤销
222
+ docs/COLLAB/M2-TASK-GRANT.md 本文
223
+ ```
224
+
225
+ ## 11. 开放问题(实现前确认)
226
+
227
+ 1. **operator 公钥的初始信任**:接受 TOFU(relay 可注入,文档声明)还是要求 pairing-time pinning 才能开工(更安全但已有机器需要一个 bootstrap 动作)?
228
+ 2. **max_uses 语义**:按机器计一次还是全局一次?多机 grant 是否允许每台一次(推荐)?
229
+ 3. **task_id 绑定**:是否允许 grant 不绑 task_id(更灵活,但同 argv 可被重复用于不同任务)?建议 beta 强制绑定。
230
+ 4. **写授权**:1.3.3-beta 是否允许 `write:true` 的 grant(进入工作区写文件)?还是本版本只做只读命令,写留给 M3 工作区模型?
231
+ 5. **Windows**:TwinsEarth 的 agent 在 Windows 上,验签与计数落盘路径需确认;是否与 Linux 同批验收?
@@ -0,0 +1,291 @@
1
+ # M3 Git 原生任务(worktree / patch / 合并队列 / MERGE_CONFLICT)设计规格
2
+
3
+ > 状态:**设计稿**。只定义工作区语义、制品契约、合并队列与冲突语义,不含实现。
4
+ > 版本:`1.3.4-beta.*`(GitHub prerelease,不入 npm)。
5
+ > 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` M3;依赖 M2(合并任务需签名授权)。
6
+
7
+ ---
8
+
9
+ ## 1. 目标与非目标
10
+
11
+ ### 目标
12
+
13
+ 1. **隔离工作区**:每个任务在独立的 `git worktree` 里执行,起始点严格是任务声明的 `base_commit`;机器上其他任务、其他机器互不影响。
14
+ 2. **可传输的改动**:任务产出是**内容寻址的制品**(patch/commit bundle + 元数据),不是"机器上的本地文件变了"。
15
+ 3. **串行合并队列**:同一项目同一目标分支的合并严格串行;合并器只做"应用到当前 head → 校验 → 推进 ref",绝不 force push、绝不静默覆盖。
16
+ 4. **冲突一等公民**:无法应用/冲突时返回结构化 `MERGE_CONFLICT`(文件、hunk、base/ours/theirs、冲突制品),由人决定下一步,而不是失败日志里的一句话。
17
+ 5. **可重放**:合并事件进 ledger;重放后分支 head 与已合并集合一致。
18
+
19
+ ### 非目标(本版本不做)
20
+
21
+ - 不做"多仓库/多分支任意图"的编排(单项目 + 单目标分支;多分支留 M4/M5)。
22
+ - 不做自动解决冲突(冲突必须显式选择解决者)。
23
+ - 不做 PR 评审 UI(M4/M5)。
24
+ - 不做远程托管差异(GitHub/GitLab API 推送)——本版本只维护 relay 侧的项目 ref 与合并产物;推送镜像另立项。
25
+ - 不替代人的代码审查;合并前测试是否强制由项目策略决定(§6.4)。
26
+
27
+ ---
28
+
29
+ ## 2. 现状与缺口
30
+
31
+ | 现状 | 缺口 |
32
+ |---|---|
33
+ | 任务 = argv + 每台机器自己的项目副本 | 没有"任务专属工作区",两次并发任务会互相踩 |
34
+ | `base_commit` 只用于**事后锚点一致性检查** | 没有"从 base_commit 开始、产出可合并增量"的语义 |
35
+ | 结果是 stdout/stderr/blob | 代码改动的结果没有结构(commit/tree/diff/patch 哈希) |
36
+ | 无合并概念 | 多台机器的改动无法合并,冲突无法表达 |
37
+
38
+ ---
39
+
40
+ ## 3. 任务种类(task kinds)
41
+
42
+ 在现有 task 上新增**可选**字段(旧任务行为不变):
43
+
44
+ ```json
45
+ {
46
+ "workspace": {
47
+ "kind": "worktree", // "copy"(默认,旧行为)| "worktree"
48
+ "repo": "w2m-fleet-demo", // 项目标识(relay project 对象,见 M4)
49
+ "base_commit": "0e89dfb…", // 必填,worktree 的起点
50
+ "target_branch": "main", // 合并目标;worktree 任务只读该值
51
+ "cleanup": "on-success", // on-success(默认)| keep(调试用)
52
+ "allow_empty": true // 无改动是否算成功(默认 true)
53
+ },
54
+ "produce": "patch" // "patch"(默认)| "commit"(保留完整提交对象)
55
+ }
56
+ ```
57
+
58
+ | kind | 行为 | 结果 |
59
+ |---|---|---|
60
+ | `copy`(默认) | 旧行为:各机器原地副本,只读语义 | stdout/stderr/blob,与 v1.3.1 相同 |
61
+ | `worktree` | 在任务专属 worktree 执行,允许写 | patch/commit 制品 + 元数据;改动只在制品里 |
62
+ | `merge`(内部) | 由合并器执行,不允许用户直接指定 | 新 head 或 `MERGE_CONFLICT` |
63
+
64
+ `worktree` 任务的写约束:
65
+
66
+ - 允许写:**仅 worktree 目录内**(`cwd_rel` 必须落在 worktree 内);
67
+ - 禁止:修改 `.git`(hooks/config 由执行器禁用)、写 worktree 之外、访问网络(除非任务显式声明,属后续 cap);
68
+ - 同一机器同时活跃的 worktree 数量上限(默认 4),超限拒绝 `WORKTREE_LIMIT`;
69
+ - 执行前后都做 `git status --porcelain` 快照,用于审计"谁改了什么"。
70
+
71
+ ---
72
+
73
+ ## 4. Worktree 生命周期(执行器侧)
74
+
75
+ ### 4.1 仓库缓存
76
+
77
+ ```
78
+ <state>/repos/<repo-key>.git bare 镜像(relay project 声明的 origin)
79
+ <state>/worktrees/<task_id>/<attempt>/ 每个任务尝试一个 worktree
80
+ ```
81
+
82
+ - 首次任务:`git clone --bare <origin> <repo-key>.git`(或从 relay 的制品种子引导)。
83
+ - 后续:`git fetch --prune` 后校验 `base_commit` 存在;不存在则 `BASE_COMMIT_UNKNOWN`。
84
+ - 网络不可用且本地已有 base_commit → 允许离线执行(结果仍是 patch,合并时再校验)。
85
+
86
+ ### 4.2 创建/销毁
87
+
88
+ ```
89
+ git -C <cache> worktree add --detach <wt> <base_commit>
90
+ # 固化隔离:
91
+ git -C <wt> config core.hooksPath /dev/null # Linux;Windows 用空目录
92
+ git -C <wt> config user.name "w2m-agent"
93
+ git -C <wt> config user.email "agent@w2m.invalid"
94
+ git -C <wt> config commit.gpgsign false
95
+ ```
96
+
97
+ 执行结束:
98
+
99
+ - 成功且有改动:提交(identity 固定)→ 生成制品 → 上传 → 按 `cleanup` 删除 worktree。
100
+ - 成功无改动:`no_change` 结果(仍上传空 patch 元数据,便于审计"确实没改")。
101
+ - 失败/超时:删除 worktree(保留日志制品),`WORKTREE_CLEANUP` 失败只告警不覆盖主结果。
102
+ - GC:残留 worktree > 7 天由 agent 启动时清理(记录清理事件)。
103
+
104
+ ### 4.3 锚点与一致性
105
+
106
+ - 结果必须带:`base_commit`、`head_commit`(若有提交)、`tree_sha`、`parent`、`files_changed[]`、`diffstat`。
107
+ - 执行器校验 `git merge-base --is-ancestor <base_commit> HEAD`(worktree 内 HEAD 就是提交链)。
108
+ - 合并器再校验一次(§6.3),防止"结果被篡改/分支被移动"。
109
+
110
+ ---
111
+
112
+ ## 5. 制品契约(patch artifact)
113
+
114
+ 结果 JSON(新增字段,旧字段保留):
115
+
116
+ ```json
117
+ {
118
+ "workspace": { "kind": "worktree", "repo": "w2m-fleet-demo", "worktree": "/…/wt", "cleaned": true },
119
+ "patch": {
120
+ "format": "git-format-patch|git-diff-binary",
121
+ "sha256": "内容哈希",
122
+ "bytes": 12345,
123
+ "output_mode": "blob|inline",
124
+ "base_commit": "…",
125
+ "head_commit": "…",
126
+ "tree": "…",
127
+ "parent": "…",
128
+ "files_changed": ["src/a.mjs", "test/a.test.mjs"],
129
+ "diffstat": { "files": 2, "insertions": 30, "deletions": 4 },
130
+ "empty": false
131
+ },
132
+ "tests": { "ran": true, "exit_code": 0, "summary": "…" }
133
+ }
134
+ ```
135
+
136
+ - 大 patch 走既有 blob 通道(内容寻址,sha 即名字);小 patch 内联。
137
+ - **patch 不是可信输入**:合并器只接受"从 base_commit 生成的 patch",且重新计算 sha 比对;不接受任意 diff。
138
+ - `format: git-format-patch` 时禁止包含 merge commit、签署签名外的元数据;`git-diff-binary` 用于二进制文件(需显式 allow)。
139
+
140
+ ---
141
+
142
+ ## 6. 合并队列(Merge Queue)
143
+
144
+ ### 6.1 队列模型
145
+
146
+ ```
147
+ project: w2m-fleet-demo
148
+ target_branch: main
149
+ queue item: { queue_id, patch_sha, base_commit, source_task_id, submitted_by, submitted_at, state }
150
+ state: queued → claimed → applying → merged | conflict | failed
151
+ ```
152
+
153
+ - 每个 `(project, target_branch)` 一个队列;**同一时刻只有一个合并器持有队列锁**(reuse lease 机制,新 lease kind `merge-slot`,TTL 10 分钟,心跳续租)。
154
+ - 合并器机器:项目配置指定(默认 relay 所在机器 / 指定 machine_id);必须通过 M2 grant 授权合并任务的命令行。
155
+ - 队列 item 可带 `depends_on`(例如依赖某个测试任务完成)。
156
+
157
+ ### 6.2 合并流程(固定顺序)
158
+
159
+ ```
160
+ 1. 取队首 item
161
+ 2. 校验:patch_sha 一致;item.base_commit 是当前 head 的祖先(否则进入 §6.3 重放策略)
162
+ 3. 在合并专用 worktree(detached at 当前 head)中 git apply --3way 或 git am
163
+ · 成功 → 进入 4
164
+ · 冲突 → 生成 MERGE_CONFLICT(§7),item 终止,不推进 head
165
+ 4. 可选验证:项目策略要求时,跑验证任务(测试/锚点),失败则 item 终止(head 不变)
166
+ 5. 推进 head:fast-forward 到新提交(或按策略生成 merge commit)
167
+ 6. ledger 事件:project.ref.updated {from, to, patch_sha, queue_id, task_id}
168
+ 7. 发布:GET project ref 的读者看到新 head;触发 M4 项目房间事件
169
+ 8. 锁释放,处理下一个 item
170
+ ```
171
+
172
+ ### 6.3 base 前进时的重放策略(三种,项目配置选一)
173
+
174
+ | 策略 | 行为 | 结果 |
175
+ |---|---|---|
176
+ | `strict`(默认) | base_commit 必须等于当前 head(完全线性) | 否则 `BASE_MISMATCH`,提交者需 rebase 重交 |
177
+ | `replay` | 若 patch 能干净应用到新 head 上则重放(`git apply --3way`),否则冲突 | 成功 = 新 merge;冲突 = `MERGE_CONFLICT` |
178
+ | `queue-base` | 合并队列内部维护"虚拟 head"(前一个 item 的后状态),后续 item 按队列顺序依次应用 | 保证提交顺序但不保证等价于最新 head |
179
+
180
+ > beta 建议:默认 `strict`(最容易验证正确性),`replay` 作为显式选项。
181
+
182
+ ### 6.4 测试门禁(可配置)
183
+
184
+ - 项目策略 `merge_requires_tests: true|false`(默认 false)。
185
+ - true 时:合并前必须有一个同 patch_sha 的测试任务 `status=ok`(由 M2 grant 授权执行),否则 item 停在 `queued`,返回 `TESTS_REQUIRED`。
186
+ - 测试通过后仍需锚点校验;不通过则 item `failed`(head 不动)。
187
+
188
+ ---
189
+
190
+ ## 7. MERGE_CONFLICT:一等结果
191
+
192
+ 冲突不是异常,而是**任务结果**:
193
+
194
+ ```json
195
+ {
196
+ "status": "conflict",
197
+ "refusal_reason": "MERGE_CONFLICT",
198
+ "merge": {
199
+ "queue_id": "…",
200
+ "patch_sha": "…",
201
+ "target_head": "…",
202
+ "files": ["src/a.mjs"],
203
+ "hunks": [{ "file": "src/a.mjs", "start": 10, "lines": 4, "side": "ours|theirs|both" }],
204
+ "artifact": { "sha256": "冲突现场 patch(带 conflict markers)", "output_mode": "blob" },
205
+ "base_commit": "…", "ours_commit": "…", "theirs_commit": "…",
206
+ "next": "由人选择解决者:重新提交到新 base / 手动解决后作为新 patch 入队"
207
+ }
208
+ }
209
+ ```
210
+
211
+ 规则:
212
+
213
+ - 冲突时**绝不推进 head**,绝不覆盖任何人的改动。
214
+ - 冲突现场(含 conflict markers 的 diff)作为 blob 保存,任何有权限的客户端可下载。
215
+ - 冲突任务的 `source_task_id` 与提交者可见;M4/M5 的审查任务可以引用它。
216
+ - 队列 item 不会自动重试冲突(避免抖动);提交者修好后作为**新 item** 入队。
217
+
218
+ ---
219
+
220
+ ## 8. 失败码
221
+
222
+ ```
223
+ WORKTREE_LIMIT 并发 worktree 超限
224
+ BASE_COMMIT_UNKNOWN 仓库里没有该 base_commit
225
+ BASE_MISMATCH strict 策略下 base 不等于当前 head
226
+ PATCH_SHA_MISMATCH 制品被篡改或损坏
227
+ PATCH_APPLY_FAILED patch 无法应用(非冲突性错误)
228
+ MERGE_CONFLICT 结构化冲突(§7)
229
+ TESTS_REQUIRED 策略要求测试但不存在通过的测试任务
230
+ TEST_FAILED_AFTER_MERGE 验证失败,未推进 head
231
+ MERGE_ABORTED 合并器异常中断(锁过期后由下一个合并器恢复)
232
+ NOT_MERGE_EXECUTOR 非指定合并器持有 merge-slot
233
+ FORCE_PUSH_REFUSED 任何试图改写历史的行为一律拒绝(红线)
234
+ ```
235
+
236
+ ---
237
+
238
+ ## 9. 与 M2 / M4 的关系
239
+
240
+ - **M2 grant**:合并任务(`git apply`/`git merge`/`git push` 到 relay ref)属于清单外命令,必须由操作员 grant 授权;`worktree` 任务的普通改动不需要 grant(写限制在隔离工作区)。
241
+ - **M4 项目房间**:`project.ref.updated` 是项目房间的事件;任务板展示 queued/merged/conflict;制品(patch/conflict)挂在项目下。
242
+ - **M5 多客户端**:分支锁(避免两个合并器同时写同一分支)由 merge-slot 保证;审查任务可作为 merge item 的前置条件。
243
+
244
+ ---
245
+
246
+ ## 10. 验收(真实任务)
247
+
248
+ ### 正向
249
+
250
+ 1. **两机并行修不同文件**:A/B 各自 worktree 任务 → 各自 patch 入队 → 合并器先 A 后 B → 最终 head 包含两者改动,测试通过,ledger 两条 `project.ref.updated`。
251
+ 2. **无改动任务**:worktree 任务不产生 diff → `no_change` 成功,不入队。
252
+ 3. **冲突**:A/B 改同一文件同一处 → 第二个 item 返回 `MERGE_CONFLICT`,文件列表正确,冲突 blob 可下载,head 不变。
253
+ 4. **base 前进**:A 提交后 B 基于旧 base 提交 → `strict` 下 B 得到 `BASE_MISMATCH`;`replay` 下若 B 的改动可干净应用则自动合并。
254
+ 5. **测试门禁**:策略开启 + 无测试任务 → `TESTS_REQUIRED`;补测试通过后自动继续合并。
255
+ 6. **重放/幂等**:同一 patch 重复入队 → 第二次被识别为已合并(`already_merged`),不产生重复提交。
256
+
257
+ ### 负向(自动化)
258
+
259
+ ```
260
+ 篡改 patch sha / 非 worktree 产出的 patch / patch 含 merge commit /
261
+ 越过 worktree 写文件 / .git 目录改动 / 非合并器推进 ref / force push /
262
+ worktree 超限 / 合并中断后锁恢复 → 全部对应错误码且 head 不变
263
+ ```
264
+
265
+ ---
266
+
267
+ ## 11. 交付物清单(实现阶段)
268
+
269
+ ```
270
+ src/agent/worktree.mjs worktree 创建/隔离/清理/GC、改动快照
271
+ src/agent/patch-artifact.mjs commit/patch 生成、sha、blob 上传、结果字段
272
+ src/relay/merge-queue.mjs 队列、merge-slot lease、item 状态机、ledger 事件
273
+ src/relay/project-ref.mjs project/branch ref 存储与 GET 接口
274
+ src/relay/merge-executor.mjs 合并执行(apply/验证/推进/冲突制品)——可在 relay 或独立机器
275
+ src/plugin/tools.mjs 派发 worktree 任务;查看队列;下载冲突制品(小改)
276
+ src/relay/server.mjs /v1/projects/:id/queue、/v1/projects/:id/ref 路由
277
+ test/worktree.test.mjs worktree 生命周期/隔离/GC
278
+ test/patch-artifact.test.mjs 制品契约/不可信输入校验
279
+ test/merge-queue.test.mjs 队列串行/重试/幂等/锁恢复
280
+ test/merge-conflict.test.mjs MERGE_CONFLICT 结构与 head 不变
281
+ test/git-native-e2e.test.mjs 双机双 worktree → 合并(本地 relay + 临时仓库)
282
+ ```
283
+
284
+ ## 12. 开放问题(实现前确认)
285
+
286
+ 1. **仓库来源**:relay project 的 origin 指向哪个仓库(GitHub 私仓?本机 bare 镜像?);推送只到 relay ref 还是也镜像到 GitHub?
287
+ 2. **合并器位置**:合并器跑在 relay 主机(jp-shared)还是指定一台独立机器?——影响 M2 grant 的机器集合与权限。
288
+ 3. **base 策略默认值**:`strict`(推荐)还是 `replay`?
289
+ 4. **测试门禁默认**:默认不开(false)还是强制开(true)?强制开需要先有测试任务模型。
290
+ 5. **分支范围**:beta 只支持一个目标分支(main)还是允许按任务指定分支?
291
+ 6. **Windows 路径**:TwinsEarth 作为 worktree 执行者时,大小写不敏感文件系统与 symlink 的约束(现有 caps 已暴露 `case_sensitive_fs:false`),是否需要显式限制某些 patch(如重命名大小写)?