@twinsearth/w2m-dsh-plugin 2.0.3 → 2.0.5

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 (72) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +187 -462
  5. package/bin/w2m-joint.mjs +69 -7
  6. package/codex/mcp-servers.toml +1 -1
  7. package/docs/acceptance/2.0.4/RELEASE-2040.json +22 -0
  8. package/docs/acceptance/2.0.5/RELEASE-205.json +21 -0
  9. package/docs/acceptance/2.0.5/RELEASE-2050.json +22 -0
  10. package/docs/adr/0001-collaboration-crdt.md +59 -0
  11. package/lib/client.js +2754 -2599
  12. package/package.json +2 -2
  13. package/plugin.json +1 -1
  14. package/src/agent/agent.mjs +97 -36
  15. package/src/agent/artifact-delivery.mjs +89 -0
  16. package/src/agent/caps.mjs +16 -2
  17. package/src/agent/sinks/git-sink.mjs +78 -0
  18. package/src/agent/sinks/ssh-sink.mjs +83 -0
  19. package/src/agent/sse-cursor.mjs +16 -0
  20. package/src/agent/sse-watchdog.mjs +71 -0
  21. package/src/plugin/dataplane-sink-route.mjs +113 -0
  22. package/src/plugin/fleet-chat-route.mjs +11 -0
  23. package/src/plugin/gpw.mjs +99 -15
  24. package/src/plugin/joint-dev/dispatch.mjs +5 -1
  25. package/src/plugin/joint-dev/merge-approval.mjs +115 -0
  26. package/src/plugin/joint-dev/progress.mjs +107 -0
  27. package/src/plugin/joint-dev/relay-dispatch.mjs +52 -22
  28. package/src/plugin/project-stream-route.mjs +58 -0
  29. package/src/plugin/tools-joint.mjs +6 -1
  30. package/src/plugin/tools.mjs +25 -5
  31. package/src/relay/fleet-messages.mjs +48 -24
  32. package/src/relay/project-room.mjs +47 -2
  33. package/src/relay/server.mjs +11 -5
  34. package/src/relay/state.mjs +63 -3
  35. package/src/util/chat-crypto.mjs +70 -1
  36. package/docs/COLLAB/COLLABORATION-MODEL.md +0 -240
  37. package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +0 -361
  38. package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +0 -70
  39. package/docs/COLLAB/M1-MAINTENANCE.md +0 -313
  40. package/docs/COLLAB/M2-TASK-GRANT.md +0 -231
  41. package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +0 -291
  42. package/docs/COLLAB/M4-PROJECT-ROOM.md +0 -260
  43. package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +0 -263
  44. package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +0 -231
  45. package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +0 -263
  46. package/docs/COLLAB/TASK-MODEL.md +0 -281
  47. package/docs/acceptance/1.9.10/RELEASE-1910.json +0 -21
  48. package/docs/acceptance/1.9.11/RELEASE-1911.json +0 -20
  49. package/docs/acceptance/1.9.12/RELEASE-1912.json +0 -20
  50. package/docs/acceptance/1.9.13/RELEASE-1913.json +0 -20
  51. package/docs/acceptance/1.9.14/RELEASE-1914.json +0 -20
  52. package/docs/acceptance/1.9.15/RELEASE-1915.json +0 -22
  53. package/docs/acceptance/1.9.16/RELEASE-1916.json +0 -21
  54. package/docs/acceptance/1.9.17/RELEASE-1917.json +0 -20
  55. package/docs/acceptance/1.9.19/RELEASE-1919.json +0 -21
  56. package/docs/acceptance/1.9.20/RELEASE-1920.json +0 -22
  57. package/docs/acceptance/1.9.4/RELEASE-194.json +0 -23
  58. package/docs/acceptance/1.9.5/RELEASE-195.json +0 -23
  59. package/docs/acceptance/1.9.6/RELEASE-196.json +0 -22
  60. package/docs/acceptance/1.9.7/RELEASE-197.json +0 -21
  61. package/docs/acceptance/1.9.8/RELEASE-198.json +0 -21
  62. package/docs/acceptance/1.9.9/RELEASE-199.json +0 -21
  63. package/lib/client.js.bak2 +0 -640
  64. package/src/plugin/dataflow.mjs +0 -72
  65. package/src/plugin/fleet-target.mjs.bak-arch2 +0 -812
  66. package/src/plugin/fleet-target.mjs.bak-archive +0 -812
  67. package/src/plugin/tofuture/report.mjs +0 -44
  68. package/src/plugin/tools.mjs.bak-c7 +0 -7029
  69. package/src/relay/tofuture-whitelist.mjs +0 -46
  70. package/src/util/data-plane.mjs +0 -180
  71. package/src/util/sealed-object.mjs +0 -141
  72. package/src/util/tofuture-hmac.mjs +0 -30
package/bin/w2m-joint.mjs CHANGED
@@ -17,9 +17,15 @@
17
17
  *
18
18
  * Exit codes: 0 ok, 1 refusal (named code), 2 usage error.
19
19
  */
20
- import { readFileSync } from 'node:fs';
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+
22
+ import { deviceFilePath } from '../src/agent/identity.mjs';
23
+ import { resolveStateDir } from '../src/util/cli.mjs';
21
24
 
22
25
  import { buildControlEnvelopes, buildPlan } from '../src/plugin/joint-dev/dispatch.mjs';
26
+ import { abortMerge, advanceTarget, mergePatches } from '../src/plugin/joint-dev/merge-engine.mjs';
27
+ import { consumeMergeApproval, createMergeApproval, loadMergeApproval } from '../src/plugin/joint-dev/merge-approval.mjs';
28
+ import { resolve as resolvePath } from 'node:path';
23
29
  import { negotiateFeatures, readRelayCapabilities } from '../src/plugin/relay-capabilities.mjs';
24
30
  import { dispatchPlanToRelay } from '../src/plugin/joint-dev/relay-dispatch.mjs';
25
31
  import { readOperatorToken } from '../src/plugin/operator-token.mjs';
@@ -53,8 +59,8 @@ const fail = (code, detail) => { print({ ok: false, code, detail: detail ?? null
53
59
 
54
60
  const args = parseArgs(process.argv.slice(2));
55
61
  const [command, action] = args._;
56
- const stateDir = typeof args.state === 'string' ? args.state : (process.env.W2M_JOINT_STATE ?? null);
57
- if (stateDir === null) fail('STATE_DIR_REQUIRED', 'pass --state <dir> (the joint-dev state directory)');
62
+ if (args['allow-unsigned-approval'] === true) fail('UNKNOWN_FLAG', '--allow-unsigned-approval was removed: joint approvals must be signed');
63
+ const stateDir = resolveStateDir(typeof args.state === 'string' ? args.state : (process.env.W2M_JOINT_STATE ?? null), 'joint-dev');
58
64
 
59
65
  if (command === 'plan' && action === 'create') {
60
66
  if (typeof args.steps !== 'string') fail('STEPS_REQUIRED', 'pass --steps <steps.json>');
@@ -130,7 +136,7 @@ if (command === 'status') {
130
136
  const plan = loadPlan({ stateDir, planSha: args.plan });
131
137
  if (plan.ok !== true) fail(plan.code ?? 'PLAN_NOT_FOUND');
132
138
  const approval = loadApproval({ stateDir, planSha: args.plan });
133
- const gate = assertDispatchAllowed({ stateDir, planSha: args.plan, requireSignature: args['allow-unsigned-approval'] !== true });
139
+ const gate = assertDispatchAllowed({ stateDir, planSha: args.plan, requireSignature: true });
134
140
  print({
135
141
  ok: true,
136
142
  plan_sha256: plan.plan.plan_sha256,
@@ -147,7 +153,7 @@ if (command === 'dispatch') {
147
153
  if (typeof args.plan !== 'string') fail('PLAN_REQUIRED', 'pass --plan <sha256>');
148
154
  const key = ensureOperatorKey();
149
155
  const trusted = key.ok === true ? [key.key_id] : [];
150
- const gate = assertDispatchAllowed({ stateDir, planSha: args.plan, requireSignature: args['allow-unsigned-approval'] !== true, trustedOperatorKeyIds: trusted });
156
+ const gate = assertDispatchAllowed({ stateDir, planSha: args.plan, requireSignature: true, trustedOperatorKeyIds: trusted });
151
157
  if (gate.ok !== true) fail(gate.code ?? 'APPROVAL_REQUIRED', 'dispatch refused before anything was sent');
152
158
  const relayUrl = typeof args.relay === 'string' ? args.relay : null;
153
159
  if (args['dry-run'] !== true) {
@@ -161,9 +167,23 @@ if (command === 'dispatch') {
161
167
  }
162
168
  if (token === null) fail('OPERATOR_TOKEN_MISSING', 'run w2m-operator set --file <token file>, or pass --token-file');
163
169
  // decision 2: one approval authorises exactly one dispatch.
164
- const consumed = consumeApproval({ stateDir, planSha: args.plan, approvalSha: gate.approval.approval.approval_sha256 });
170
+ const consumed = consumeApproval({ stateDir, planSha: args.plan, approvalSha: gate.approval.approval_sha256 });
165
171
  if (consumed.ok !== true) fail(consumed.code ?? 'APPROVAL_CONSUME_FAILED', consumed.detail ?? null);
166
- const sent = await dispatchPlanToRelay({ rabbitUrl: relayUrl, token, plan: gate.plan, approval: gate.approval.approval, auditFile: stateDir + '/joint-dev/audit.jsonl' });
172
+ // D2/B5: the gate is on by default in warn mode; enforce is explicit.
173
+ let deviceToken = '';
174
+ if (typeof args['device-token-file'] === 'string') {
175
+ deviceToken = readFileSync(args['device-token-file'], 'utf8').trim();
176
+ } else {
177
+ try {
178
+ const file = deviceFilePath({});
179
+ if (existsSync(file)) deviceToken = String(JSON.parse(readFileSync(file, 'utf8')).device_token ?? '');
180
+ } catch { deviceToken = ''; }
181
+ }
182
+ const expectedVersion = typeof args['expect-version'] === 'string' && args['expect-version'] !== ''
183
+ ? args['expect-version']
184
+ : JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
185
+ const versionGate = args['version-gate'] === 'off' || args['version-gate'] === 'enforce' ? args['version-gate'] : 'warn';
186
+ const sent = await dispatchPlanToRelay({ rabbitUrl: relayUrl, token, plan: gate.plan, approval: gate.approval, auditFile: stateDir + '/joint-dev/audit.jsonl', dispatchDir: stateDir + '/joint-dev/dispatches', deviceToken, expectedVersion, versionGate });
167
187
  print({ ...sent, plan_sha256: gate.plan.plan_sha256, relay: relayUrl });
168
188
  process.exit(sent.ok === true ? 0 : 1);
169
189
  }
@@ -183,6 +203,7 @@ if (command === 'dispatch') {
183
203
  relay: typeof args.relay === 'string' ? args.relay : null,
184
204
  relay_capabilities: capabilities === null ? null : { relay_version: capabilities.relay_version, features: capabilities.features },
185
205
  relay_negotiation: negotiation,
206
+ version_gate: args['version-gate'] === 'off' || args['version-gate'] === 'enforce' ? args['version-gate'] : 'warn',
186
207
  plan_sha256: gate.plan.plan_sha256,
187
208
  approval_sha256: gate.approval.approval_sha256,
188
209
  envelopes: envelopes.length,
@@ -191,5 +212,46 @@ if (command === 'dispatch') {
191
212
  process.exit(0);
192
213
  }
193
214
 
215
+ if (command === 'merge') {
216
+ const repo = typeof args.repo === 'string' ? resolvePath(args.repo) : null;
217
+ const baseCommit = typeof args.base === 'string' ? args.base : null;
218
+ const targetRef = typeof args['target-ref'] === 'string' ? args['target-ref'] : null;
219
+ const expectedOld = typeof args['expected-old'] === 'string' ? args['expected-old'] : null;
220
+ if (repo === null || baseCommit === null || targetRef === null || expectedOld === null) {
221
+ fail('MERGE_ARGS_REQUIRED', 'merge needs --repo --base --target-ref --expected-old');
222
+ }
223
+ if (action === 'approve') {
224
+ if (typeof args.head !== 'string' || typeof args.approver !== 'string') fail('MERGE_APPROVER_REQUIRED', 'merge approve needs --head <sha> --approver <name> --authority <70..100>');
225
+ const authority = Number.parseInt(String(args.authority ?? ''), 10);
226
+ if (!Number.isInteger(authority)) fail('AUTHORITY_REQUIRED', 'pass --authority <70..100>');
227
+ const ttlMin = Number.parseInt(String(args['ttl-min'] ?? '10'), 10);
228
+ const created = createMergeApproval({ stateDir, planSha: typeof args.plan === 'string' ? args.plan : null, repo, baseCommit, targetRef, expectedOld, integrationHead: args.head, approver: args.approver, authority, ttlMs: (Number.isFinite(ttlMin) ? ttlMin : 10) * 60_000 });
229
+ if (created.ok !== true) fail(created.code ?? 'MERGE_APPROVAL_FAILED', created.detail ?? null);
230
+ print({ ok: true, approval_sha256: created.approval_sha256, approval: created.approval });
231
+ process.exit(0);
232
+ }
233
+ if (typeof args.patches !== 'string') fail('MERGE_PATCHES_REQUIRED', 'pass --patches <manifest.json>');
234
+ let patches = null;
235
+ try { patches = JSON.parse(readFileSync(args.patches, 'utf8').replace(/^\uFEFF/, '')); } catch (error) { fail('MERGE_PATCHES_UNREADABLE', error instanceof Error ? error.message : String(error)); }
236
+ if (!Array.isArray(patches) || patches.length === 0) fail('MERGE_PATCHES_INVALID', 'manifest must be a non-empty JSON array of {step_id, patch}');
237
+ const merged = mergePatches({ repoDir: repo, baseCommit, patches });
238
+ if (merged.ok !== true) fail(merged.code ?? 'MERGE_FAILED', merged.detail ?? null);
239
+ const dryRun = args.apply !== true;
240
+ print({ ok: true, dry_run: dryRun, integration_head: merged.integration_head, applied: merged.applied, target_ref: targetRef, expected_old: expectedOld });
241
+ if (dryRun) { abortMerge({ repoDir: repo, worktree: merged.worktree }); process.exit(0); }
242
+ if (typeof args.approval !== 'string') { abortMerge({ repoDir: repo, worktree: merged.worktree }); fail('MERGE_APPROVAL_REQUIRED', '--apply needs --approval <merge_approval_sha256>'); }
243
+ const gate = loadMergeApproval({ stateDir, approvalSha: args.approval, repo, baseCommit, targetRef, expectedOld, integrationHead: merged.integration_head, planSha: typeof args.plan === 'string' ? args.plan : null });
244
+ if (gate.ok !== true) { abortMerge({ repoDir: repo, worktree: merged.worktree }); fail(gate.code ?? 'MERGE_APPROVAL_INVALID', gate.detail ?? null); }
245
+ const consumed = consumeMergeApproval({ stateDir, approvalSha: args.approval });
246
+ if (consumed.ok !== true) { abortMerge({ repoDir: repo, worktree: merged.worktree }); fail(consumed.code ?? 'MERGE_APPROVAL_CONSUME_FAILED', consumed.detail ?? null); }
247
+ const stamp = new Date().toISOString().replace(/[^0-9]/g, '').slice(0, 14);
248
+ const rollbackRef = 'refs/w2m/rollback/' + (typeof args.plan === 'string' ? args.plan.slice(0, 12) : 'plan') + '-' + stamp;
249
+ const advanced = advanceTarget({ repoDir: repo, targetRef, expectedOld, newCommit: merged.integration_head, rollbackRef });
250
+ abortMerge({ repoDir: repo, worktree: merged.worktree });
251
+ if (advanced.ok !== true) fail(advanced.code ?? 'MERGE_CAS_FAILED', advanced.detail ?? null);
252
+ print({ ok: true, dry_run: false, integration_head: merged.integration_head, target_ref: targetRef, rollback_ref: rollbackRef });
253
+ process.exit(0);
254
+ }
255
+
194
256
  process.stderr.write('usage: node bin/w2m-joint.mjs <plan create|list|get|approve|status|dispatch> [options]\n');
195
257
  process.exit(2);
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@2.0.3", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@2.0.5", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "2.0.4",
3
+ "case": "RELEASE-2040",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-28T12:54:03.976Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-2040-1793192043977",
11
+ "requirement": "v2.0.4:GPW 双模式(小白默认/高级手动)、默认保护文件夹、3 秒放行节流、高级模式 10 秒自动放行;README 主页重写",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "mode": "novice default; advanced manual via /api/w2m/gpw/folders set-mode",
16
+ "protected": "C:\\\\Windows, Program Files (x86) on C: and D: always denied in novice",
17
+ "throttle": "one path auto-pass per 3000ms",
18
+ "timeout": "advanced pending auto-approve 10s (was 100s)",
19
+ "tests": "gpw suites 74 pass / 0 fail incl gpw-modes 5/5; client-bundle 26/26; lint 0 errors"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "version": "2.0.5",
3
+ "case": "RELEASE-205",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-28T16:37:29.497Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-205-1793205449498",
11
+ "requirement": "v2.0.5:Relay 支持 AEAD v2 加密消息信封(enc.v=2 + aad{session_id,machine_id,message_id}),新增能力标记 chat-enc-v2,供主插件启用 encrypted mirror 发送;v1 兼容不变。",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "envelope": "v2 shape accepted and echoed verbatim (iv/tag + nested aad); v1 still accepted; unknown versions refused BAD_ENCRYPTION",
16
+ "aadBinding": "clear AAD must name the same session_id and machine_id as the message (CHAT_AAD_MISMATCH otherwise); missing aad CHAT_AAD_MISSING; bad message id CHAT_AAD_INVALID",
17
+ "feature": "healthz features includes chat-enc-v2; dispatcher can gate encrypted mirror on it",
18
+ "tests": "fleet-messages + relay-health-capabilities + relay.test 217 pass / 0 fail; targeted v2 suite 28 pass / 0 fail; eslint 0 errors"
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": "2.0.5",
3
+ "case": "RELEASE-2050",
4
+ "kind": "positive",
5
+ "machines": [
6
+ "b53a37f4-1464-4512-9259-33a7df091e3e"
7
+ ],
8
+ "executed_at": "2026-10-28T16:36:54.281Z",
9
+ "executor": "lead",
10
+ "task_id": "pre-release-2050-1793205414282",
11
+ "requirement": "v2.0.5:A7 AEAD v2 AAD(chat v2 + relay enc.v2 + capability chat-enc-v2);联合开发 P0/S1/S4/A1/A2/A3/A4/A5/A6/A8 全部切片在本工作树验证",
12
+ "result": {
13
+ "outcome": "ok",
14
+ "assertions": {
15
+ "preflight": "16 pass / 0 fail / 1 unverifiable (GREEN)",
16
+ "tests": "declared suite all pass (node --test via scripts.test)",
17
+ "lint": "0 errors",
18
+ "pack": "dist tarball byte-identical on a second pack; manifest equals tree",
19
+ "scope": "local code + tests only; no push, no tag, no npm publish, no relay deployment"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,59 @@
1
+ # ADR 0001 — 多机协作:本周期不引入 CRDT(D5)
2
+
3
+ - 状态:已接受(2026-10-28)
4
+ - 决策者:项目负责人(D5)
5
+ - 相关:D1=c、A6 项目房间、A7 聊天 AEAD、S4 数据面红线
6
+
7
+ ## 背景
8
+
9
+ “多机跨机实时协作”的讨论中出现了共享编辑/多光标/实时文档(CRDT)诉求。当前系统的协作对象是:
10
+
11
+ - 追加式的编队聊天消息(`w2m_chat`);
12
+ - 追加式的项目房间事件(board/locks/reviews/notifications);
13
+ - 以 git 为权威的代码产物(patch/commit/merge)。
14
+
15
+ 以上都是**追加式或 git 语义**,不是“多写者并发编辑同一份可变文档”。仓库同时保持 **零运行时依赖**、单文件手写 client bundle(无 bundler),以及“数据面不经 relay”的红线。
16
+
17
+ ## 决策
18
+
19
+ **本周期(v2.x)不引入任何 CRDT 实现,不新增 CRDT 依赖,不写共享编辑代码。** 共享编辑若未来立项,必须先有新的 ADR 批准,并通过独立评审。
20
+
21
+ ## 评估过的选项
22
+
23
+ | 选项 | 许可(本轮核实口径) | 体积/运行时 | 结论 |
24
+ |---|---|---|---|
25
+ | Yjs | MIT(npm 13.6.33) | 纯 JS,约 1MB+ 生态;需要 provider/transport | **未来首选**(若共享编辑立项,风险最低) |
26
+ | Automerge | MIT(npm latest 2.0.0-alpha.3) | Rust/WASM,数 MB,alpha 版本 | **备选**(需要 Git-like change DAG 证据时再评估) |
27
+ | json-joy | **AGPL-3.0-only**(多数包) | 2.2MB + 20+ 运行时依赖 | **明确排除**:与 MIT 公开插件分发不兼容,不依赖、不复制代码 |
28
+
29
+ ## 不引入的理由
30
+
31
+ 1. **问题形态不匹配**:当前协作是追加事件 + git 合并,CRDT 解决的是并发可变文档;引入它不会自动闭合 A3/A6 的任何缺口。
32
+ 2. **零依赖是发布资产**:任何 CRDT 库都会进入持有 device token / operator key / fleet key 的进程信任域,扩大 CVE 面并破坏可复现的单文件打包。
33
+ 3. **数据面红线**:共享编辑内容属于数据面,必须经用户 sink/git,不能经 relay;现有 relay 冻结,无法承载新的同步协议。
34
+ 4. **权限与审计模型未定义**:多人同时编辑需要用户级身份、逐操作授权与审计,当前只有机器级身份。
35
+ 5. **许可风险**:json-joy 为 AGPL,直接排除;Yjs/Automerge 需在立项时做人工法务复核。
36
+
37
+ ## 触发条件(满足才可重新评估)
38
+
39
+ - 出现真实的“多人同时编辑同一份文档/文件”需求,而非代码补丁协作;
40
+ - 新的 ADR 获批,且明确数据面走 sink/git 或加密 P2P,不经 relay;
41
+ - 完成用户级身份、逐操作授权、审计与回滚设计;
42
+ - 选定单一实现(Yjs 优先),完成构建管线与许可复核,并接受依赖体积/供应链评审。
43
+
44
+ ## 本周期替代方案
45
+
46
+ - 代码协作:git worktree + `git apply --3way` + CAS 更新目标分支(merge-engine,A4 CLI 已落地)。
47
+ - 产物同步:用户授权的 dir/git/ssh sink(A3)。
48
+ - 状态同步:项目房间事件 + 分页/截断信号(A6 截断已落地;持久化与客户端面板在后续切片)。
49
+ - 消息加密:聊天 AEAD v2 + AAD 会话/发送者绑定(A7)。
50
+
51
+ ## 后果
52
+
53
+ - 不新增依赖,`package.json#dependencies` 保持为空;
54
+ - 不新增 `src/util/crdt.mjs` 之类的占位模块,避免制造“已有 CRDT”的读感;
55
+ - 未来若立项,按触发条件重新走 ADR,而不是在既有事件/合并路径上静默引入。
56
+
57
+ ## 回滚/复审
58
+
59
+ 纯文档决策,无运行时代码;复审时新增一份 ADR 取代本文件,并在 CHANGELOG 记录决策变化。