@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +709 -0
- package/bin/w2m-grant.mjs +98 -0
- package/bin/w2m-maintenance.mjs +112 -0
- package/codex/mcp-servers.toml +1 -1
- package/deploy/maintenance/bootstrap.ps1 +30 -0
- package/deploy/maintenance/bootstrap.sh +61 -0
- package/deploy/server/w2m-maintenance.path +10 -0
- package/deploy/server/w2m-maintenance.service +20 -0
- package/deploy/windows/w2m-maintenance.xml +31 -0
- package/docs/COLLAB/COLLABORATION-MODEL.md +240 -0
- package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +361 -0
- package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +70 -0
- package/docs/COLLAB/M1-MAINTENANCE.md +313 -0
- package/docs/COLLAB/M2-TASK-GRANT.md +231 -0
- package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +291 -0
- package/docs/COLLAB/M4-PROJECT-ROOM.md +260 -0
- package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +263 -0
- package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +231 -0
- package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +263 -0
- package/docs/COLLAB/TASK-MODEL.md +281 -0
- package/docs/MULTI-MACHINE-COLLABORATION-PLAN.md +186 -0
- package/docs/RELEASE-STABLE.md +193 -0
- package/docs/acceptance/1.3.10-beta.10/M1-LIVE-DIAG.json +27 -0
- package/docs/acceptance/1.3.10-beta.10/M1-LIVE-POSTUPDATE-DIAG.json +18 -0
- package/docs/acceptance/1.3.10-beta.10/M1-LIVE-RESTART.json +24 -0
- package/docs/acceptance/1.3.10-beta.10/M1-LIVE-UPDATE.json +24 -0
- package/docs/acceptance/1.3.11/M1-LIVE-DIAG.json +26 -0
- package/docs/acceptance/1.3.12-beta.1/M1-LIVE-DIAG.json +26 -0
- package/docs/acceptance/1.4.0/M1-LIVE-MAINTENANCE.json +20 -0
- package/docs/acceptance/1.4.0/REGRESSION-1.4.0.json +81 -0
- package/package.json +26 -5
- package/plugin.json +1 -1
- package/src/acceptance/evidence.mjs +79 -0
- package/src/agent/agent.mjs +141 -3
- package/src/agent/grant-gate.mjs +89 -0
- package/src/agent/maintenance-ferry.mjs +111 -0
- package/src/agent/outbox.mjs +138 -0
- package/src/agent/patch-artifact.mjs +53 -0
- package/src/agent/worktree.mjs +118 -0
- package/src/maintenance/actions.mjs +203 -0
- package/src/maintenance/grant.mjs +139 -0
- package/src/maintenance/operator-key.mjs +151 -0
- package/src/maintenance/protocol.mjs +204 -0
- package/src/maintenance/runner.mjs +189 -0
- package/src/plugin/tools.mjs +1 -1
- package/src/relay/blob-sessions.mjs +148 -0
- package/src/relay/merge-queue.mjs +137 -0
- package/src/relay/project-locks.mjs +179 -0
- package/src/relay/project-notifications.mjs +184 -0
- package/src/relay/project-refs.mjs +110 -0
- package/src/relay/project-reviews.mjs +161 -0
- package/src/relay/project-room.mjs +163 -0
- package/src/relay/reliability-metrics.mjs +180 -0
- package/src/relay/server.mjs +382 -1
- package/src/relay/state.mjs +308 -1
|
@@ -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(如重命名大小写)?
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# M4 项目房间(relay project 对象 / 快照+增量)设计规格
|
|
2
|
+
|
|
3
|
+
> 状态:**设计稿**。只定义项目对象模型、API、事件与权限,不含实现。
|
|
4
|
+
> 版本:`1.3.5-beta.*`(GitHub prerelease,不入 npm)。
|
|
5
|
+
> 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` M4;依赖 M3(项目 ref/制品)与既有 memory/聊天/决策能力。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 目标与非目标
|
|
10
|
+
|
|
11
|
+
### 目标
|
|
12
|
+
|
|
13
|
+
1. **共享对象**:relay 上有一个 `project`,承载成员、任务板、制品、memory 文档、决策、git ref;多客户端看到的**同一份事实**。
|
|
14
|
+
2. **快照 + 增量**:客户端先取快照(带 `last_event_seq`),再按 seq 拉增量事件;断线重连不丢事件、不重复应用。
|
|
15
|
+
3. **多客户端**:不同机器/不同 DSH 实例打开同一 project,看到相同任务板与制品;客户端只做视图,不做本地权威状态。
|
|
16
|
+
4. **权限矩阵**:读/派发/合并/写 memory/决策/管理成员按角色判定(复用既有 role 模型与 `mayAct`)。
|
|
17
|
+
5. **兼容旧行为**:没有显式 project 的任务自动归入隐式 default project;旧客户端/旧 relay 不受影响。
|
|
18
|
+
|
|
19
|
+
### 非目标(本版本不做)
|
|
20
|
+
|
|
21
|
+
- 不做人类账号体系(成员维度先按 machine_id 与 operator 身份;账号/多人体系后续)。
|
|
22
|
+
- 不做跨 project 的成员继承/组织层级(单层 project)。
|
|
23
|
+
- 不做完整 UI(本版本先做面板的任务板/制品/决策只读视图 + 基础操作;富交互后续)。
|
|
24
|
+
- 不做事件永久存储(保留窗口 + 快照兜底;永久审计走 ledger)。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. 现状与缺口
|
|
29
|
+
|
|
30
|
+
| 现有 | 缺口 |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `GET /v1/tasks` 全局任务列表 | 没有"属于哪个项目"的归属;多项目混在一起 |
|
|
33
|
+
| `fleet-messages` 会话消息 | 消息不是共享状态,不能作为任务板/制品/决策的事实源 |
|
|
34
|
+
| memory CAS 文档(fleet 级) | 没有 project 命名空间,多项目决策互相污染 |
|
|
35
|
+
| council 决策 trail | 决策散在 memory 行里,没有结构化的项目决策账本 |
|
|
36
|
+
| blob 制品 | 没有目录/索引(只能按 sha 取,不知道"是什么、属于哪个任务") |
|
|
37
|
+
| 多 DSH 实例 | 各看各的 session;没有"项目"这个聚合根 |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. 项目对象模型
|
|
42
|
+
|
|
43
|
+
### 3.1 静态结构
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"project_id": "01M6…(ULID)",
|
|
48
|
+
"name": "w2m-fleet-demo",
|
|
49
|
+
"slug": "w2m-fleet-demo",
|
|
50
|
+
"created_at": "2026-10-21T13:00:00Z",
|
|
51
|
+
"created_by": "operator|machine_id",
|
|
52
|
+
"origin": { "kind": "git|none", "url": "https://…/repo.git", "default_branch": "main" },
|
|
53
|
+
"members": [
|
|
54
|
+
{ "kind": "machine", "machine_id": "1c14d7d2-…", "role": "owner|maintainer|worker|observer", "joined_at": "…" },
|
|
55
|
+
{ "kind": "operator", "name": "TwinsEarth", "role": "owner", "key_id": "…" }
|
|
56
|
+
],
|
|
57
|
+
"settings": {
|
|
58
|
+
"merge_requires_tests": false,
|
|
59
|
+
"event_retention": 5000
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
角色沿用既有模型(`owner / maintainer / worker / observer`),权限矩阵见 §6。
|
|
65
|
+
|
|
66
|
+
### 3.2 挂载的子对象
|
|
67
|
+
|
|
68
|
+
| 子对象 | 来源 | 说明 |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| 任务 | 现有 task(新增 `project_id` 归属) | 看板列由 task 的派生 status 决定(waiting/active/done/failed) |
|
|
71
|
+
| 制品 | blob 通道 + 新增索引 | `{sha256, kind: patch|conflict|diag|report|other, label, task_id, created_by, size, created_at}` |
|
|
72
|
+
| memory 文档 | 现有 CAS,key 变为 `project:<project_id>:<doc>` | 每个项目独立命名空间;fleet 级文档保留 |
|
|
73
|
+
| 决策 | council 决策记录 | 结构化 `{decision_id, council_id, question, choice, dissent, ballots, decided_by, decided_at}` |
|
|
74
|
+
| git ref | M3 `project.ref` | 分支 head、合并来源 |
|
|
75
|
+
| 事件流 | 本设计新增 | 追加式,seq 单调递增,快照后按 seq 增量 |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 4. 快照 + 增量协议
|
|
80
|
+
|
|
81
|
+
### 4.1 快照
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
GET /v1/projects/:id
|
|
85
|
+
→ {
|
|
86
|
+
protocol_version, project, members,
|
|
87
|
+
board: { waiting:[taskSummary…], active:[…], done:[…], failed:[…] },
|
|
88
|
+
artifacts: [{sha256, kind, label, task_id, size, created_at}…],
|
|
89
|
+
memory: [{doc, revision, updated_at, last_records: [...]}…],
|
|
90
|
+
decisions: [{decision_id, question, choice, decided_at}…],
|
|
91
|
+
refs: [{branch, head, updated_at}…],
|
|
92
|
+
last_event_seq: 1234,
|
|
93
|
+
server_time
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- 快照默认限制条数(board 每列 ≤50,artifacts ≤200,memory 文档索引不分页但只给元数据),超出用 `?full=1` 或分页参数显式拉取。
|
|
98
|
+
- 快照必须与 `last_event_seq` **同一个一致性点**(relay 单线程/加锁构造,不允许"快照取自 t1、seq 取自 t2")。
|
|
99
|
+
|
|
100
|
+
### 4.2 增量事件
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
GET /v1/projects/:id/events?since_seq=1234[&limit=200]
|
|
104
|
+
→ { events: [{ seq, type, at, actor, payload }…], next_seq, truncated }
|
|
105
|
+
SSE: GET /v1/projects/:id/stream?since_seq=1234 (断线重连带 since_seq)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
事件类型(首版):
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
project.created
|
|
112
|
+
member.added / member.removed / member.role_changed
|
|
113
|
+
task.linked {task_id, project_id}
|
|
114
|
+
task.status_changed {task_id, status, outcome}
|
|
115
|
+
artifact.added {sha256, kind, label, task_id}
|
|
116
|
+
memory.updated {doc, revision}
|
|
117
|
+
decision.recorded {decision_id, choice}
|
|
118
|
+
ref.updated {branch, from, to, queue_id}
|
|
119
|
+
settings.changed {key, value}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
规则:
|
|
123
|
+
|
|
124
|
+
- `seq` 全局单调(relay 级),客户端按 project 过滤;`next_seq` 用于续拉。
|
|
125
|
+
- 客户端应用规则:只应用 `seq > 本地已应用最大 seq`;发现 `since_seq` 过旧(事件已过保留窗口)→ `truncated=true` → **重新拉快照**,绝不静默跳过。
|
|
126
|
+
- 事件不可变;同一 seq 重放幂等(客户端按 seq 去重)。
|
|
127
|
+
- 保留窗口默认 5000 事件/项目;ledger 仍是长期审计(事件可从 ledger 重建)。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 5. API 面(relay 路由)
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
GET /v1/projects 列出项目(按成员权限过滤)
|
|
135
|
+
POST /v1/projects 创建项目(owner 或 operator)
|
|
136
|
+
GET /v1/projects/:id 快照
|
|
137
|
+
PATCH /v1/projects/:id 设置/改名(owner/maintainer)
|
|
138
|
+
GET /v1/projects/:id/events 增量(since_seq)
|
|
139
|
+
GET /v1/projects/:id/stream SSE 增量
|
|
140
|
+
POST /v1/projects/:id/members 加成员/改角色(owner)
|
|
141
|
+
DELETE /v1/projects/:id/members/:member 移除(owner)
|
|
142
|
+
POST /v1/projects/:id/artifacts 登记制品(元数据 + 已存在 blob sha)
|
|
143
|
+
GET /v1/projects/:id/artifacts/:sha 取制品元数据(内容走 blob 通道)
|
|
144
|
+
POST /v1/projects/:id/memory/:doc 追加记录(CAS,现有语义 + 项目命名空间)
|
|
145
|
+
GET /v1/projects/:id/memory/:doc 读文档
|
|
146
|
+
POST /v1/projects/:id/decisions 记录决策(council 决策落账本)
|
|
147
|
+
GET /v1/projects/:id/refs 分支 head
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
任务归属:现有 `POST /v1/task` 新增可选 `project_id`;未提供时归入隐式 `default` 项目(惰性创建,行为与今天一致)。
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 6. 权限矩阵(复用 role 模型)
|
|
155
|
+
|
|
156
|
+
| 动作 | owner | maintainer | worker | observer |
|
|
157
|
+
|---|---|---|---|---|
|
|
158
|
+
| 读快照/事件/制品 | ✅ | ✅ | ✅ | ✅ |
|
|
159
|
+
| 派发任务 | ✅ | ✅ | ✅ | ❌ |
|
|
160
|
+
| 登记制品 | ✅ | ✅ | ✅ | ❌ |
|
|
161
|
+
| 写 memory / 记录决策 | ✅ | ✅ | ❌ | ❌ |
|
|
162
|
+
| 合并(M3 merge-slot) | ✅ | ✅ | ❌ | ❌ |
|
|
163
|
+
| 管理成员/设置 | ✅ | 部分(设置) | ❌ | ❌ |
|
|
164
|
+
| 创建项目 | ✅ | ❌ | ❌ | ❌ |
|
|
165
|
+
|
|
166
|
+
拒绝时返回结构化错误(`reason_code` + `required_role` + `current_role`),与既有 chat 权限错误同风格。
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 7. 客户端(DSH 插件侧)
|
|
171
|
+
|
|
172
|
+
### 7.1 数据获取
|
|
173
|
+
|
|
174
|
+
- 宿主路由代理(沿用 fleet-chat 的 fetch 路由机制):`/api/w2m/project/:id`(快照)、`/api/w2m/project/:id/events`(增量/SSE)。
|
|
175
|
+
- 插件工具:项目房间工具(创建/列出/读取/选择当前项目、登记制品、写决策)——**实现时确定名称**。
|
|
176
|
+
- 客户端只缓存"已应用最大 seq"与视图数据;不把项目状态写入本地权威存储。
|
|
177
|
+
|
|
178
|
+
### 7.2 面板视图(本版本)
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
项目房间面板:
|
|
182
|
+
· 任务板四列(waiting/active/done/failed)+ 计数(复用 M4 之前的看板计数)
|
|
183
|
+
· 制品列表(sha 短码、kind、label、所属任务、大小、下载)
|
|
184
|
+
· 决策列表(问题、选择、异议、时间)
|
|
185
|
+
· memory 文档索引 + 最近记录(点击进文档)
|
|
186
|
+
· refs(分支 head,来自 M3)
|
|
187
|
+
· 连接状态(快照 seq / 增量 seq / 断线重连)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
不做写操作的富交互(创建任务、合并按钮放后续版本);本版本提供"选择当前项目 + 只读看板/制品/决策"。
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 8. 持久化与重放
|
|
195
|
+
|
|
196
|
+
- relay 状态目录新增 `projects.json`(项目 + 成员 + 设置,原子写)与 `project-events.jsonl`(事件流,截断保留窗口)。
|
|
197
|
+
- 启动时:加载 projects.json;事件流按 seq 校验连续性;损坏行跳过并记录告警(与现有 ledger 行为一致)。
|
|
198
|
+
- ledger 中新增 `project.*` 事件;**事件流可从 ledger 重建**(一致性检查:重建 seq 与事件流一致)。
|
|
199
|
+
- 制品内容仍在 blob 存储(8 MiB 上限/256 个);制品索引不复制内容。
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 9. 兼容与迁移
|
|
204
|
+
|
|
205
|
+
- 旧客户端:`GET /v1/tasks` 行为不变;新字段被忽略。
|
|
206
|
+
- 旧 relay:新客户端请求 `/v1/projects` 得到 404 → 提示 `PROJECT_ROOM_UNSUPPORTED_RELAY`,不回退到错误状态。
|
|
207
|
+
- 既有任务:启动时惰性归入 `default` 项目(不重写 ledger;项目快照包含 default 项目下的历史任务摘要)。
|
|
208
|
+
- 既有 fleet memory:保留为 fleet 级文档;项目决策新写 `project:<id>:decisions`。
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 10. 验收(真实任务)
|
|
213
|
+
|
|
214
|
+
### 正向
|
|
215
|
+
|
|
216
|
+
1. **两客户端同一任务板**:客户端 A 创建项目并派发任务 → 客户端 B(另一台机器)打开同一项目,3 秒内看到该任务;任务状态变化同步。
|
|
217
|
+
2. **制品可见**:A 的 worktree 任务产出 patch(M3)→ 制品登记 → B 可看到并下载(sha 校验一致)。
|
|
218
|
+
3. **决策共享**:A 记录一条 council 决策 → B 的决策列表出现(含 dissent)。
|
|
219
|
+
4. **增量不丢**:B 断线 2 分钟后重连(带 since_seq)→ 期间事件全部补齐;若超出保留窗口 → 明确提示并重拉快照。
|
|
220
|
+
5. **重启恢复**:relay 重启后,项目快照、成员、事件 seq 连续(与 ledger 一致)。
|
|
221
|
+
|
|
222
|
+
### 负向(自动化)
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
未授权成员读/写 → 403 + reason_code
|
|
226
|
+
observer 派发任务/登记制品 → 拒绝
|
|
227
|
+
非成员访问项目 → 404 或 403(不泄露存在性)
|
|
228
|
+
事件 seq 缺口 → truncated → 强制快照
|
|
229
|
+
同一 seq 重复应用 → 幂等
|
|
230
|
+
项目不存在创建任务 → default 项目
|
|
231
|
+
事件流损坏行 → 跳过告警,不阻塞启动
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 11. 交付物清单(实现阶段)
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
src/relay/project-room.mjs 项目对象/成员/设置/权限、快照一致性点
|
|
240
|
+
src/relay/project-events.mjs 事件流(seq、保留窗口、SSE、重建校验)
|
|
241
|
+
src/relay/project-artifacts.mjs 制品索引(挂 blob)
|
|
242
|
+
src/relay/server.mjs /v1/projects* 路由、任务 project_id 归属
|
|
243
|
+
src/relay/state.mjs project.* ledger 事件、重启加载
|
|
244
|
+
src/plugin/tools.mjs 项目房间工具(实现时定名)+ 任务带 project_id
|
|
245
|
+
src/plugin/fleet-chat-route.mjs 宿主代理 /api/w2m/project/*
|
|
246
|
+
lib/client.js 项目房间面板(只读看板/制品/决策/refs/连接状态)
|
|
247
|
+
test/project-room.test.mjs 模型/权限/快照一致性
|
|
248
|
+
test/project-events.test.mjs seq/增量/截断/幂等/SSE 重连
|
|
249
|
+
test/project-room-e2e.test.mjs 双客户端共享(模拟两设备)
|
|
250
|
+
docs/COLLAB/M4-PROJECT-ROOM.md 本文
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## 12. 开放问题(实现前确认)
|
|
254
|
+
|
|
255
|
+
1. **项目身份**:由操作员显式创建(推荐),还是从 git origin URL 自动派生?
|
|
256
|
+
2. **成员维度**:本版本只支持 machine + operator 身份,还是需要先引入"人"的账号模型?
|
|
257
|
+
3. **面板范围**:本版本只读看板/制品/决策,还是同时提供"创建任务/登记制品"的交互?
|
|
258
|
+
4. **事件保留**:5000 条/项目够不够?超出后只靠快照是否可接受?
|
|
259
|
+
5. **跨项目**:一个任务能否属于多个项目(如共享库)?建议 beta 单项目。
|
|
260
|
+
6. **默认项目**:隐式 `default` 项目命名与可见性(所有成员可见 vs 仅 owner)?
|