@twinsearth/w2m-dsh-plugin 1.3.0 → 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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +601 -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 +59 -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 +250 -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/package.json +22 -4
- package/plugin.json +1 -1
- package/src/acceptance/evidence.mjs +79 -0
- package/src/agent/agent.mjs +64 -2
- package/src/agent/grant-gate.mjs +82 -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 +109 -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 +182 -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 +164 -1
- package/src/relay/state.mjs +128 -1
|
@@ -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)?
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# M5 多客户端原语(分支/文件锁、审查任务、通知)设计规格
|
|
2
|
+
|
|
3
|
+
> 状态:**设计稿**。只定义锁、审查、通知三个原语的模型与语义,不含实现。
|
|
4
|
+
> 版本:`1.3.6-beta.*`(GitHub prerelease,不入 npm)。
|
|
5
|
+
> 上游:`docs/MULTI-MACHINE-COLLABORATION-PLAN.md` M5;依赖 M3(合并队列)、M4(项目房间/事件)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 目标与非目标
|
|
10
|
+
|
|
11
|
+
### 目标
|
|
12
|
+
|
|
13
|
+
1. **避免互相踩**:分支锁与文件锁让两个客户端/机器在改同一处之前先协商;冲突在**发生前**暴露,而不是合并时才发现。
|
|
14
|
+
2. **审查成为一等任务**:补丁/改动可以要求审查;审查结论(批准/要求修改)进入项目账本,并可作为合并前置条件。
|
|
15
|
+
3. **通知可达且不丢**:任务分配、审查请求、锁冲突、合并结果、分支推进等事件能定向送达;离线客户端回来能补齐;按客户端维护已读水位。
|
|
16
|
+
4. **全部可审计**:锁的获取/续租/强制释放、审查结论、通知投递都进项目事件流与 ledger。
|
|
17
|
+
|
|
18
|
+
### 非目标(本版本不做)
|
|
19
|
+
|
|
20
|
+
- 不做跨项目锁、不做目录级递归锁(只到分支 + 文件路径)。
|
|
21
|
+
- 不做自动抢占/自动释放冲突的"聪明策略"——过期才可重入,强制释放必须显式且留痕。
|
|
22
|
+
- 不做外部通知渠道(邮件/Webhook/IM)——本版本只有项目房间内通知(SSE + 未读水位)。
|
|
23
|
+
- 不做多人账号体系(身份仍为 machine + operator,与 M4 一致)。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. 原语一:分支锁 / 文件锁
|
|
28
|
+
|
|
29
|
+
### 2.1 锁对象
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"lock_id": "01M6…",
|
|
34
|
+
"project_id": "…",
|
|
35
|
+
"kind": "branch|file",
|
|
36
|
+
"target": { "branch": "main" } | { "path": "src/a.mjs" },
|
|
37
|
+
"mode": "exclusive",
|
|
38
|
+
"holder": { "kind": "machine|operator", "id": "…" },
|
|
39
|
+
"reason": "改 auth 模块(任务 01M6…)",
|
|
40
|
+
"acquired_at": "…",
|
|
41
|
+
"expires_at": "…",
|
|
42
|
+
"lease_ms": 1800000,
|
|
43
|
+
"renewed_at": "…"
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- 分支锁:`(project_id, branch)` 唯一;文件锁:`(project_id, path)` 唯一。
|
|
48
|
+
- 同一 target 同一时刻只允许一个 exclusive holder;无共享锁(读不需要锁)。
|
|
49
|
+
- TTL 默认 30 分钟;持有人每 5 分钟续租(客户端心跳);过期自动释放(事件 `lock.expired`)。
|
|
50
|
+
- 持有者可以主动释放;被他人持有不可抢占(除非过期)。
|
|
51
|
+
- **强制释放**:owner 可 `force_release`,必须带 reason,事件 `lock.force_released` 记 who/why,通知原持有人。
|
|
52
|
+
|
|
53
|
+
### 2.2 强制级别(项目设置)
|
|
54
|
+
|
|
55
|
+
| 模式 | 行为 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `advisory`(默认) | 只记录与展示;获取冲突时返回 `LOCK_HELD` 但调用方可选择继续(`force: true` 需有权限,事件记录) |
|
|
58
|
+
| `enforced` | 获取失败即阻止:worktree 任务/合并提交在创建时检查锁,冲突直接拒绝(`LOCK_HELD`),不会进入队列 |
|
|
59
|
+
|
|
60
|
+
强制级别只对"声明了 target_branch / 涉及文件路径"的写任务生效(`workspace.kind=worktree` 且 `produce=patch/commit`);只读任务不需要锁。
|
|
61
|
+
|
|
62
|
+
### 2.3 与 M3 合并队列的关系
|
|
63
|
+
|
|
64
|
+
- 合并队列的 `merge-slot` 是**系统级串行化**(保证同一时刻一个合并器)。
|
|
65
|
+
- 分支锁是**用户级协调**(保证两个提交者不在同一分支上互相覆盖意图)。
|
|
66
|
+
- 合并时若目标分支被他人持有锁且模式为 `enforced`:合并项排队等待(`waiting_on: lock:<id>`),锁释放后继续;等待超过 `expires_at` 则失败 `LOCK_WAIT_TIMEOUT`。
|
|
67
|
+
- 文件锁在合并时用于**冲突预检**:队列里两个 item 的 `files_changed` 有交集且各自目标文件被 lock 覆盖 → 后一个 item 等待或提示冲突(`PATH_LOCK_CONFLICT`)。
|
|
68
|
+
|
|
69
|
+
### 2.4 API 与错误码
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
GET /v1/projects/:id/locks
|
|
73
|
+
POST /v1/projects/:id/locks {kind, target, lease_ms, reason, mode?}
|
|
74
|
+
POST /v1/projects/:id/locks/:lock_id/renew
|
|
75
|
+
DELETE /v1/projects/:id/locks/:lock_id (holder)
|
|
76
|
+
DELETE /v1/projects/:id/locks/:lock_id?force=1&reason=… (owner)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
LOCK_HELD 目标已被持有(返回 holder + expires_at)
|
|
81
|
+
LOCK_NOT_OWNER 非 holder 续租/释放
|
|
82
|
+
LOCK_EXPIRED 操作时已过期
|
|
83
|
+
LOCK_FORCE_DENIED 无 owner 权限或缺少 reason
|
|
84
|
+
LOCK_WAIT_TIMEOUT 合并等待锁超时
|
|
85
|
+
PATH_LOCK_CONFLICT 文件锁与队列内已有改动重叠
|
|
86
|
+
LOCK_LIMIT 单项目/单持有者锁数量超限(默认 20/10)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 3. 原语二:审查任务(review)
|
|
92
|
+
|
|
93
|
+
### 3.1 定位
|
|
94
|
+
|
|
95
|
+
审查是**控制面任务**(不需要目标机器执行命令):它指向一个制品/补丁,由有权限的审查者给出结论,结论进入项目事件流与 ledger,并可作为合并前置条件。
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"review_id": "01M6…",
|
|
100
|
+
"project_id": "…",
|
|
101
|
+
"target": { "kind": "patch|artifact", "sha256": "…", "source_task_id": "…", "diffstat": {…} },
|
|
102
|
+
"requested_by": { "kind": "machine|operator", "id": "…" },
|
|
103
|
+
"reviewers": [{ "kind": "machine|operator", "id": "…" }],
|
|
104
|
+
"required_approvals": 1,
|
|
105
|
+
"status": "pending|approved|changes_requested|expired|cancelled",
|
|
106
|
+
"decisions": [
|
|
107
|
+
{ "reviewer": {…}, "decision": "approve|changes_requested|comment",
|
|
108
|
+
"comment": "…", "findings": [{"file":"src/a.mjs","line":10,"severity":"high|medium|low","text":"…"}],
|
|
109
|
+
"decided_at": "…" }
|
|
110
|
+
],
|
|
111
|
+
"due_at": "…"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
规则:
|
|
116
|
+
|
|
117
|
+
- 审查者必须具有 `maintainer` 或 `owner` 角色;`worker` 不能当审查者。
|
|
118
|
+
- **禁止自审**:`requested_by` 与 reviewer 相同 → `REVIEW_SELF_NOT_ALLOWED`(owner 可 override,但事件标记 `self_review_override`)。
|
|
119
|
+
- 批准数达到 `required_approvals` → `approved`;任一人 `changes_requested` → `changes_requested`(可由提交者重新提交新 revision 触发新一轮)。
|
|
120
|
+
- 审查结论不可改(审计),改变结论需要新的 decision 记录(`REVIEW_ALREADY_DECIDED` 拒绝重复同向结论)。
|
|
121
|
+
- 过期未处理 → `expired`(可重建新审查)。
|
|
122
|
+
- 审查证据引用内容寻址制品(patch、测试结果、diffstat),审查者客户端按 sha 拉取。
|
|
123
|
+
|
|
124
|
+
### 3.2 与合并门禁
|
|
125
|
+
|
|
126
|
+
项目设置 `merge_requires_review: true` 时,M3 合并队列在应用前检查:同一 `patch_sha` 存在 `approved` 审查;否则 item 停在 `queued` 返回 `REVIEW_REQUIRED`。测试门禁(M3)与审查门禁相互独立,可同时开启。
|
|
127
|
+
|
|
128
|
+
### 3.3 API 与错误码
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
POST /v1/projects/:id/reviews 创建(requested_by 有权限)
|
|
132
|
+
GET /v1/projects/:id/reviews?status=…
|
|
133
|
+
GET /v1/projects/:id/reviews/:review_id
|
|
134
|
+
POST /v1/projects/:id/reviews/:review_id/decisions {decision, comment, findings}
|
|
135
|
+
```
|
|
136
|
+
```
|
|
137
|
+
REVIEW_PERMISSION 无 maintainer/owner 角色
|
|
138
|
+
REVIEW_SELF_NOT_ALLOWED 自审且未 override
|
|
139
|
+
REVIEW_ALREADY_DECIDED 重复同向结论
|
|
140
|
+
REVIEW_EXPIRED 过期后提交
|
|
141
|
+
REVIEW_TARGET_UNKNOWN 目标 sha 不在制品索引
|
|
142
|
+
REVIEW_REQUIRED 合并门禁挡住(M3 返回)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 4. 原语三:通知(notifications)
|
|
148
|
+
|
|
149
|
+
### 4.1 通知对象
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"notification_id": "01M6…",
|
|
154
|
+
"project_id": "…",
|
|
155
|
+
"kind": "task.assigned|review.requested|review.decided|lock.conflict|merge.conflict|merge.merged|ref.updated|mention|maintenance.result",
|
|
156
|
+
"target": { "kind": "machine|operator", "id": "…" } | { "kind": "broadcast" },
|
|
157
|
+
"subject": "短标题",
|
|
158
|
+
"refs": { "task_id": "…", "review_id": "…", "lock_id": "…", "merge_queue_id": "…", "sha256": "…" },
|
|
159
|
+
"created_at": "…",
|
|
160
|
+
"requires_action": true
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 4.2 投递与已读
|
|
165
|
+
|
|
166
|
+
- 投递路径:项目事件流 `notification.created`(SSE 实时)+ `GET /v1/projects/:id/notifications?since_ms=…`(离线补齐)。
|
|
167
|
+
- 已读水位:按客户端身份(machine_id 或 operator name)维护 `read_watermark`(ms);`GET …/notifications/unread` 返回 `since read_watermark` 的计数与摘要。
|
|
168
|
+
- 去重:同一 `(kind, refs 主键, target)` 60 秒内只产生一条。
|
|
169
|
+
- 限流:单项目 ≤ 120 条/分钟,超出合并为 `digest`(摘要一条,含 count)。
|
|
170
|
+
- 保留:通知保留 7 天或 2000 条/项目(取小);已读回执只存水位,不存逐条已读。
|
|
171
|
+
- 权限:只有项目成员能读该项目通知;非成员不泄露。
|
|
172
|
+
|
|
173
|
+
### 4.3 API
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
GET /v1/projects/:id/notifications?since_ms=…&limit=…
|
|
177
|
+
GET /v1/projects/:id/notifications/unread
|
|
178
|
+
POST /v1/projects/:id/notifications/read {watermark_ms}
|
|
179
|
+
```
|
|
180
|
+
```
|
|
181
|
+
NOTIFICATION_RATE_LIMITED 被合并为 digest(不是错误,返回 digest id)
|
|
182
|
+
NOTIFICATION_FORBIDDEN 非成员
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 5. 权限矩阵(叠加 M4 角色)
|
|
188
|
+
|
|
189
|
+
| 动作 | owner | maintainer | worker | observer |
|
|
190
|
+
|---|---|---|---|---|
|
|
191
|
+
| 获取分支/文件锁 | ✅ | ✅ | ✅ | ❌ |
|
|
192
|
+
| 强制释放锁 | ✅ | ❌ | ❌ | ❌ |
|
|
193
|
+
| 创建审查 | ✅ | ✅ | ✅(请求他人审) | ❌ |
|
|
194
|
+
| 作为审查者裁决 | ✅ | ✅ | ❌ | ❌ |
|
|
195
|
+
| 读通知 | ✅ | ✅ | ✅ | ✅ |
|
|
196
|
+
| 标记已读 | ✅ | ✅ | ✅ | ✅ |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 6. 并发与一致性要点
|
|
201
|
+
|
|
202
|
+
1. **锁获取必须是原子 CAS**:relay 单线程处理请求,`(target)` 上已有未过期 holder 则失败;并发同请求只一个成功(集成测试覆盖 10 并发)。
|
|
203
|
+
2. **锁/审查/通知状态变更都写项目事件 + ledger**;重启后从状态文件 + ledger 恢复。
|
|
204
|
+
3. **过期清理**:relay 周期扫描(复用 lease sweep 节奏),过期锁写 `lock.expired` 并通知原持有人。
|
|
205
|
+
4. **客户端离线**:锁过期后回归即可再获取;被通知的事件按 since 补齐。
|
|
206
|
+
5. **时钟**:服务端时间权威;客户端只用水位比较,不自行判定过期。
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 7. 验收(真实任务)
|
|
211
|
+
|
|
212
|
+
### 正向
|
|
213
|
+
|
|
214
|
+
1. **分支锁**:客户端 A 获取 main 锁 → B 获取返回 `LOCK_HELD`(含 holder/expires)→ A 释放后 B 成功;SSE 两侧都收到 lock 事件。
|
|
215
|
+
2. **enforced 模式**:项目切到 enforced 后,B 在无锁情况下创建 worktree 写任务被拒(`LOCK_HELD`),A 释放后成功。
|
|
216
|
+
3. **锁过期**:A 持锁不续租,TTL 到期后 B 可获取;A 收到 `lock.expired` 通知。
|
|
217
|
+
4. **强制释放**:owner 强制释放 A 的锁,带 reason;A 收到通知;事件含 who/why。
|
|
218
|
+
5. **审查通过**:A 提交 patch → 创建审查指派 B → B 批准 → 合并门禁放行。
|
|
219
|
+
6. **审查拒绝**:B 提 `changes_requested` + 行级 findings → A 客户端看到→ 重新提交新 revision。
|
|
220
|
+
7. **自审禁止**:A 指派自己 → `REVIEW_SELF_NOT_ALLOWED`。
|
|
221
|
+
8. **通知补齐**:B 离线期间发生任务分配/审查请求 → 重新连上后 unread 计数正确、事件按 since 补齐、标记已读后归零。
|
|
222
|
+
|
|
223
|
+
### 负向(自动化)
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
10 并发抢同一分支锁 → 恰好 1 成功
|
|
227
|
+
非 holder 续租/释放 → LOCK_NOT_OWNER
|
|
228
|
+
非 owner 强制释放 → LOCK_FORCE_DENIED
|
|
229
|
+
worker 审查裁决 → REVIEW_PERMISSION
|
|
230
|
+
重复 approve → REVIEW_ALREADY_DECIDED
|
|
231
|
+
非成员读通知 → NOTIFICATION_FORBIDDEN
|
|
232
|
+
通知风暴 500 条/分钟 → digest 合并且不丢事件
|
|
233
|
+
过期锁被操作 → LOCK_EXPIRED
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 8. 交付物清单(实现阶段)
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
src/relay/locks.mjs 锁的原子获取/续租/释放/过期/强制释放、限流
|
|
242
|
+
src/relay/reviews.mjs 审查对象、权限、自审、结论状态机、合并门禁查询
|
|
243
|
+
src/relay/notifications.mjs 通知生成/去重/限流/digest/未读水位、SSE 事件
|
|
244
|
+
src/relay/server.mjs /v1/projects/:id/{locks,reviews,notifications} 路由
|
|
245
|
+
src/relay/state.mjs lock/review/notification 状态持久化 + 事件
|
|
246
|
+
src/plugin/tools.mjs 锁/审查/通知的工具入口(实现时定名)+ 任务声明锁
|
|
247
|
+
src/plugin/fleet-chat-route.mjs 宿主代理上述只读/操作接口
|
|
248
|
+
lib/client.js 面板:锁列表、审查列表/裁决、通知铃铛与未读
|
|
249
|
+
test/locks.test.mjs 并发/过期/强制释放/权限
|
|
250
|
+
test/reviews.test.mjs 状态机/自审/合并门禁/权限
|
|
251
|
+
test/notifications.test.mjs 去重/限流/digest/未读水位/离线补齐
|
|
252
|
+
test/m5-e2e.test.mjs 双客户端:锁冲突→审查→合并门禁(模拟)
|
|
253
|
+
docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md 本文
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## 9. 开放问题(实现前确认)
|
|
257
|
+
|
|
258
|
+
1. **锁强制级别默认**:`advisory`(推荐,先展示再用)还是 `enforced`?
|
|
259
|
+
2. **文件锁粒度**:精确路径即可,还是需要 glob/目录模式(如 `src/**`)?
|
|
260
|
+
3. **审查者指派**:指定具体审查者(推荐)还是"具备 maintainer 角色的人皆可审"?
|
|
261
|
+
4. **必要批准数**:默认 1 还是按项目可配(1/2)?
|
|
262
|
+
5. **通知渠道**:本版本只做项目房间内通知,是否需要同时保留 fleet chat 提醒(复用 mentions)?
|
|
263
|
+
6. **锁等待策略**:合并队列遇到 enforced 锁是等待(带超时)还是直接失败让用户重试?
|