@twinsearth/w2m-dsh-plugin 2.0.4 → 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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +8 -0
- package/bin/w2m-joint.mjs +69 -7
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.0.5/RELEASE-205.json +21 -0
- package/docs/acceptance/2.0.5/RELEASE-2050.json +22 -0
- package/docs/adr/0001-collaboration-crdt.md +59 -0
- package/lib/client.js +146 -8
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/agent/agent.mjs +97 -36
- package/src/agent/artifact-delivery.mjs +89 -0
- package/src/agent/caps.mjs +16 -2
- package/src/agent/sinks/git-sink.mjs +78 -0
- package/src/agent/sinks/ssh-sink.mjs +83 -0
- package/src/agent/sse-cursor.mjs +16 -0
- package/src/agent/sse-watchdog.mjs +71 -0
- package/src/plugin/dataplane-sink-route.mjs +113 -0
- package/src/plugin/fleet-chat-route.mjs +11 -0
- package/src/plugin/joint-dev/dispatch.mjs +5 -1
- package/src/plugin/joint-dev/merge-approval.mjs +115 -0
- package/src/plugin/joint-dev/progress.mjs +107 -0
- package/src/plugin/joint-dev/relay-dispatch.mjs +52 -22
- package/src/plugin/project-stream-route.mjs +58 -0
- package/src/plugin/tools-joint.mjs +6 -1
- package/src/plugin/tools.mjs +25 -5
- package/src/relay/fleet-messages.mjs +48 -24
- package/src/relay/project-room.mjs +47 -2
- package/src/relay/server.mjs +11 -5
- package/src/relay/state.mjs +63 -3
- package/src/util/chat-crypto.mjs +70 -1
- package/docs/COLLAB/COLLABORATION-MODEL.md +0 -240
- package/docs/COLLAB/EXECUTION-AUTH-MODEL.md +0 -361
- package/docs/COLLAB/M1-ACCEPTANCE-RUNBOOK.md +0 -70
- package/docs/COLLAB/M1-MAINTENANCE.md +0 -313
- package/docs/COLLAB/M2-TASK-GRANT.md +0 -231
- package/docs/COLLAB/M3-GIT-NATIVE-TASKS.md +0 -291
- package/docs/COLLAB/M4-PROJECT-ROOM.md +0 -260
- package/docs/COLLAB/M5-MULTI-CLIENT-PRIMITIVES.md +0 -263
- package/docs/COLLAB/M6-CROSS-NETWORK-RELIABILITY.md +0 -231
- package/docs/COLLAB/M7-METRICS-ACCEPTANCE.md +0 -263
- package/docs/COLLAB/TASK-MODEL.md +0 -281
- package/docs/acceptance/1.9.10/RELEASE-1910.json +0 -21
- package/docs/acceptance/1.9.11/RELEASE-1911.json +0 -20
- package/docs/acceptance/1.9.12/RELEASE-1912.json +0 -20
- package/docs/acceptance/1.9.13/RELEASE-1913.json +0 -20
- package/docs/acceptance/1.9.14/RELEASE-1914.json +0 -20
- package/docs/acceptance/1.9.15/RELEASE-1915.json +0 -22
- package/docs/acceptance/1.9.16/RELEASE-1916.json +0 -21
- package/docs/acceptance/1.9.17/RELEASE-1917.json +0 -20
- package/docs/acceptance/1.9.19/RELEASE-1919.json +0 -21
- package/docs/acceptance/1.9.20/RELEASE-1920.json +0 -22
- package/docs/acceptance/1.9.4/RELEASE-194.json +0 -23
- package/docs/acceptance/1.9.5/RELEASE-195.json +0 -23
- package/docs/acceptance/1.9.6/RELEASE-196.json +0 -22
- package/docs/acceptance/1.9.7/RELEASE-197.json +0 -21
- package/docs/acceptance/1.9.8/RELEASE-198.json +0 -21
- package/docs/acceptance/1.9.9/RELEASE-199.json +0 -21
- package/lib/client.js.bak2 +0 -640
- package/src/plugin/dataflow.mjs +0 -72
- package/src/plugin/fleet-target.mjs.bak-arch2 +0 -812
- package/src/plugin/fleet-target.mjs.bak-archive +0 -812
- package/src/plugin/tofuture/report.mjs +0 -44
- package/src/plugin/tools.mjs.bak-c7 +0 -7029
- package/src/relay/tofuture-whitelist.mjs +0 -46
- package/src/util/data-plane.mjs +0 -180
- package/src/util/sealed-object.mjs +0 -141
- package/src/util/tofuture-hmac.mjs +0 -30
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
|
|
8
|
-
"version": "2.0.
|
|
8
|
+
"version": "2.0.5"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "w2m-fleet",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
|
|
15
|
-
"version": "2.0.
|
|
15
|
+
"version": "2.0.5",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "2.0.
|
|
29
|
+
"version": "2.0.5"
|
|
30
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w2m-fleet",
|
|
3
3
|
"description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [2.0.5] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
- Relay 支持 AEAD v2 加密消息信封(A7 收尾):`enc.v=2` 携带 `aad:{session_id,machine_id,message_id}`,
|
|
13
|
+
relay 校验明文 AAD 与消息的 session/machine 一致后原样存储与回放;v1 信封保持兼容。
|
|
14
|
+
- 新增能力标记 `chat-enc-v2`(`/healthz.features`),主插件据此启用 encrypted mirror 发送。
|
|
15
|
+
- 新增错误码:`CHAT_AAD_MISSING` / `CHAT_AAD_MISMATCH` / `CHAT_AAD_INVALID`。
|
|
16
|
+
- 测试:fleet-messages AEAD v2 用例、能力协商断言(v1 兼容与未知版本拒绝保持)。
|
|
17
|
+
|
|
10
18
|
## [2.0.4] - 2026-10-28
|
|
11
19
|
|
|
12
20
|
- GPW 双策略模式:小白模式(默认)只保留黑名单强制拒绝,其余工具调用放行;高级模式需手动切换,保持四色策略。
|
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
|
-
|
|
57
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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);
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -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 记录决策变化。
|
package/lib/client.js
CHANGED
|
@@ -889,6 +889,67 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
889
889
|
if (result.ok) setTokenState(result.body);
|
|
890
890
|
}, []);
|
|
891
891
|
React.useEffect(() => { loadToken(); }, [loadToken]);
|
|
892
|
+
const [sinkKind, setSinkKind] = React.useState('dir');
|
|
893
|
+
const [sinkRoot, setSinkRoot] = React.useState('');
|
|
894
|
+
const [sinkState, setSinkState] = React.useState(null);
|
|
895
|
+
const loadSink = React.useCallback(async () => {
|
|
896
|
+
const result = await call('/api/w2m/dataplane-sink', { method: 'GET' });
|
|
897
|
+
if (result.ok) setSinkState(result.body);
|
|
898
|
+
else if (result.routeMissing !== true) setError(result.error ?? 'sink status failed');
|
|
899
|
+
}, []);
|
|
900
|
+
React.useEffect(() => { loadSink(); }, [loadSink]);
|
|
901
|
+
const [projectRoomId, setProjectRoomId] = React.useState('');
|
|
902
|
+
const [projectEvents, setProjectEvents] = React.useState([]);
|
|
903
|
+
const [projectStreaming, setProjectStreaming] = React.useState(false);
|
|
904
|
+
const projectAbortRef = React.useRef(null);
|
|
905
|
+
const stopProjectStream = () => { projectAbortRef.current?.abort(); projectAbortRef.current = null; setProjectStreaming(false); };
|
|
906
|
+
React.useEffect(() => () => { projectAbortRef.current?.abort(); }, []);
|
|
907
|
+
const startProjectStream = async () => {
|
|
908
|
+
const projectId = projectRoomId.trim();
|
|
909
|
+
if (projectId === '') return;
|
|
910
|
+
stopProjectStream();
|
|
911
|
+
const controller = new AbortController();
|
|
912
|
+
projectAbortRef.current = controller;
|
|
913
|
+
setProjectEvents([]);
|
|
914
|
+
setProjectStreaming(true);
|
|
915
|
+
try {
|
|
916
|
+
const response = await fetch('/api/w2m/project-stream?project_id=' + encodeURIComponent(projectId) + '&since_seq=0', { credentials: 'same-origin', signal: controller.signal });
|
|
917
|
+
if (response.ok !== true || response.body === null) { setError('项目流不可用(HTTP ' + response.status + ')'); return; }
|
|
918
|
+
const reader = response.body.getReader();
|
|
919
|
+
const decoder = new TextDecoder();
|
|
920
|
+
let buffer = '';
|
|
921
|
+
for (;;) {
|
|
922
|
+
const { done, value } = await reader.read();
|
|
923
|
+
if (done) break;
|
|
924
|
+
buffer += decoder.decode(value, { stream: true });
|
|
925
|
+
let split = buffer.indexOf('\n\n');
|
|
926
|
+
while (split >= 0) {
|
|
927
|
+
const block = buffer.slice(0, split);
|
|
928
|
+
buffer = buffer.slice(split + 2);
|
|
929
|
+
const dataLine = block.split('\n').find((line) => line.startsWith('data: '));
|
|
930
|
+
if (dataLine !== undefined) {
|
|
931
|
+
try { setProjectEvents((prev) => [...prev.slice(-99), JSON.parse(dataLine.slice(6))]); } catch { /* ignore malformed frame */ }
|
|
932
|
+
}
|
|
933
|
+
split = buffer.indexOf('\n\n');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (error?.name !== 'AbortError') setError('项目流中断');
|
|
938
|
+
} finally {
|
|
939
|
+
if (projectAbortRef.current === controller) { projectAbortRef.current = null; setProjectStreaming(false); }
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
const saveSink = async () => {
|
|
943
|
+
if (sinkRoot.trim() === '') return;
|
|
944
|
+
setBusy(true);
|
|
945
|
+
try {
|
|
946
|
+
const payload = sinkKind === 'ssh'
|
|
947
|
+
? { kind: 'ssh', host: sinkRoot.split(':')[0].trim(), user: (sinkRoot.split(':')[1] ?? '').trim(), dir: (sinkRoot.split(':')[2] ?? '').trim() }
|
|
948
|
+
: { kind: sinkKind, root: sinkRoot.trim() };
|
|
949
|
+
const result = await call('/api/w2m/dataplane-sink', { method: 'POST', body: JSON.stringify(payload) });
|
|
950
|
+
if (result.ok) { setSinkRoot(''); await loadSink(); } else setError(result.error ?? 'sink save failed');
|
|
951
|
+
} finally { setBusy(false); }
|
|
952
|
+
};
|
|
892
953
|
const saveToken = async () => {
|
|
893
954
|
if (tokenInput.trim() === '') return;
|
|
894
955
|
setBusy(true);
|
|
@@ -940,6 +1001,30 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
940
1001
|
]),
|
|
941
1002
|
h('div', { key: 'st', className: 'w2mGpw_adminSub' }, tokenState?.configured === true ? ('已配置 · ' + String(tokenState.source ?? '') + ' · ' + String(tokenState.fingerprint ?? '').slice(0, 12)) : '未配置 operator token(保存后写入 0600 文件,只显示指纹)'),
|
|
942
1003
|
]),
|
|
1004
|
+
h('div', { key: 'sink', className: 'w2mGpw_adminCard' }, [
|
|
1005
|
+
h('div', { key: 'row', className: 'w2mGpw_adminToolbar' }, [
|
|
1006
|
+
h('select', { key: 'k', className: 'w2mGpw_adminInput', value: sinkKind, onChange: (event) => setSinkKind(event.target.value) }, [
|
|
1007
|
+
h('option', { key: 'd', value: 'dir' }, '本地目录'),
|
|
1008
|
+
h('option', { key: 'g', value: 'lan-git' }, 'git 仓库'),
|
|
1009
|
+
h('option', { key: 's', value: 'ssh' }, 'ssh'),
|
|
1010
|
+
]),
|
|
1011
|
+
h('input', { key: 'p', className: 'w2mGpw_adminInput', value: sinkRoot, placeholder: sinkKind === 'ssh' ? 'host:user:绝对目录' : (sinkKind === 'lan-git' ? 'git 仓库绝对路径' : '产物目录绝对路径'), onChange: (event) => setSinkRoot(event.target.value) }),
|
|
1012
|
+
h('button', { key: 'a', type: 'button', className: 'w2mGpw_adminBtn', disabled: busy, onClick: saveSink }, '授权并保存'),
|
|
1013
|
+
]),
|
|
1014
|
+
h('div', { key: 'st', className: 'w2mGpw_adminSub' }, sinkState?.configured === true
|
|
1015
|
+
? ('已授权 · ' + String(sinkState.kind ?? '') + ' · ' + String(sinkState.root ?? sinkState.host ?? '') + ' · 凭据不写入配置')
|
|
1016
|
+
: '未配置数据面 sink:产物只保留本地并标记 ARTIFACT_NOT_DELIVERED(工具参数不能完成授权)'),
|
|
1017
|
+
]),
|
|
1018
|
+
h('div', { key: 'projectroom', className: 'w2mGpw_adminCard' }, [
|
|
1019
|
+
h('div', { key: 'row', className: 'w2mGpw_adminToolbar' }, [
|
|
1020
|
+
h('input', { key: 'p', className: 'w2mGpw_adminInput', value: projectRoomId, placeholder: '项目 id(只读订阅项目事件)', onChange: (event) => setProjectRoomId(event.target.value) }),
|
|
1021
|
+
h('button', { key: 'start', type: 'button', className: 'w2mGpw_adminBtn', disabled: projectStreaming, onClick: startProjectStream }, '订阅'),
|
|
1022
|
+
h('button', { key: 'stop', type: 'button', className: 'w2mGpw_adminBtn', disabled: !projectStreaming, onClick: stopProjectStream }, '停止'),
|
|
1023
|
+
]),
|
|
1024
|
+
h('div', { key: 'ev', className: 'w2mGpw_adminSub' }, projectEvents.length === 0
|
|
1025
|
+
? (projectStreaming ? '已连接,等待事件…' : '未订阅(只读;截断时 relay 会先发 PROJECT_REPLAY_TRUNCATED notice)')
|
|
1026
|
+
: projectEvents.slice(-8).map((event) => String(event.seq ?? '?') + ' · ' + String(event.type ?? event.code ?? 'event')).join(' | ')),
|
|
1027
|
+
]),
|
|
943
1028
|
h('div', { key: 'toolbar', className: 'w2mGpw_adminCard' }, [
|
|
944
1029
|
h('div', { key: 'row', className: 'w2mGpw_adminToolbar' }, [
|
|
945
1030
|
h('input', { key: 'q', className: 'w2mGpw_adminInput', value: query, placeholder: '搜索路径', onChange: (event) => setQuery(event.target.value) }),
|
|
@@ -1142,23 +1227,68 @@ function clickLoadEarlier() {
|
|
|
1142
1227
|
try { el.click(); return true; } catch { return false; }
|
|
1143
1228
|
}
|
|
1144
1229
|
const waitFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
1230
|
+
/**
|
|
1231
|
+
* The history loader shows "加载中..." while a page is in flight. Clicking "加载更早" again in that
|
|
1232
|
+
* window is what made the top button look like it was being interrupted/blocked: every loop pressed
|
|
1233
|
+
* the row while the previous press was still loading, so nothing advanced.
|
|
1234
|
+
*/
|
|
1235
|
+
function isLoadingEarlier() {
|
|
1236
|
+
if (typeof document === 'undefined') return false;
|
|
1237
|
+
for (const root of searchRoots()) {
|
|
1238
|
+
let nodes = [];
|
|
1239
|
+
try { nodes = root.querySelectorAll ? [...root.querySelectorAll('button, [role="button"], a, span, div')] : []; } catch { continue; }
|
|
1240
|
+
for (const el of nodes) {
|
|
1241
|
+
const text = String(el.textContent ?? '').trim();
|
|
1242
|
+
if (text === '' || text.length > 24) continue;
|
|
1243
|
+
if (/^加载中|^loading|加载中\.\.\./i.test(text)) return true;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
/** Wait for an in-flight "load earlier" page to finish (or time out), then continue. */
|
|
1249
|
+
async function waitForLoadSettled(timeoutMs = 6000) {
|
|
1250
|
+
let waited = 0;
|
|
1251
|
+
while (waited < timeoutMs) {
|
|
1252
|
+
if (!isLoadingEarlier()) return true;
|
|
1253
|
+
await waitFor(100);
|
|
1254
|
+
waited += 100;
|
|
1255
|
+
}
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1145
1258
|
async function scrollChatTurns() {
|
|
1146
1259
|
const surface = findScrollSurface();
|
|
1147
1260
|
const viewport = typeof window !== 'undefined' ? window.innerHeight : 800;
|
|
1148
1261
|
const step = Math.round(Math.max(240, (surface !== null ? surface.clientHeight : viewport) * 1.8));
|
|
1149
|
-
if (surface !== null && surface.scrollTop <= 8
|
|
1262
|
+
if (surface !== null && surface.scrollTop <= 8) {
|
|
1263
|
+
if (isLoadingEarlier()) await waitForLoadSettled();
|
|
1264
|
+
else if (clickLoadEarlier()) await waitForLoadSettled();
|
|
1265
|
+
}
|
|
1150
1266
|
const after = surface ?? findScrollSurface();
|
|
1151
1267
|
if (after !== null) after.scrollTop = Math.max(0, after.scrollTop - step);
|
|
1152
1268
|
else if (typeof window !== 'undefined') window.scrollBy({ top: -step, behavior: 'smooth' });
|
|
1153
1269
|
}
|
|
1154
1270
|
async function scrollChatTop() {
|
|
1155
|
-
// Load history until the "load earlier" row is gone,
|
|
1156
|
-
for
|
|
1271
|
+
// Load history until the "load earlier" row is gone, but never press it while a page is loading:
|
|
1272
|
+
// each press waits for the loader to settle and for the surface to actually grow, and two pages
|
|
1273
|
+
// that add nothing end the loop instead of spinning. Only then is the surface pinned to the top.
|
|
1274
|
+
let stagnant = 0;
|
|
1275
|
+
let lastHeight = -1;
|
|
1276
|
+
for (let i = 0; i < 40; i += 1) {
|
|
1277
|
+
if (isLoadingEarlier()) {
|
|
1278
|
+
await waitForLoadSettled();
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1157
1281
|
const surface = findScrollSurface();
|
|
1158
1282
|
if (surface !== null) surface.scrollTop = 0;
|
|
1283
|
+
const before = surface !== null ? surface.scrollHeight : -1;
|
|
1159
1284
|
if (!clickLoadEarlier()) break;
|
|
1160
|
-
await
|
|
1285
|
+
await waitForLoadSettled();
|
|
1286
|
+
const after = findScrollSurface() !== null ? findScrollSurface().scrollHeight : -1;
|
|
1287
|
+
if (after <= before || after === lastHeight) stagnant += 1; else stagnant = 0;
|
|
1288
|
+
lastHeight = after;
|
|
1289
|
+
if (stagnant >= 2) break;
|
|
1161
1290
|
}
|
|
1291
|
+
await waitForLoadSettled();
|
|
1162
1292
|
for (let i = 0; i < 8; i += 1) {
|
|
1163
1293
|
const surface = findScrollSurface();
|
|
1164
1294
|
if (surface !== null) surface.scrollTop = 0;
|
|
@@ -1249,14 +1379,22 @@ function ConversationSearch() {
|
|
|
1249
1379
|
const [cursor, setCursor] = React.useState(-1);
|
|
1250
1380
|
const inputRef = React.useRef(null);
|
|
1251
1381
|
React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
|
|
1252
|
-
|
|
1253
|
-
|
|
1382
|
+
// v2.0.5: wait until 3s pass without new input before walking the conversation, so typing does
|
|
1383
|
+
// not trigger a full-tree highlight pass on every keystroke (the reported input jank).
|
|
1384
|
+
const paintNow = () => {
|
|
1254
1385
|
const total = paintSearchMarks(query);
|
|
1255
1386
|
setCount(total);
|
|
1256
1387
|
setCursor(total > 0 ? 0 : -1);
|
|
1257
1388
|
const first = document.querySelector('mark.w2mSearchMark');
|
|
1258
1389
|
if (first && first.scrollIntoView) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1259
|
-
|
|
1390
|
+
};
|
|
1391
|
+
React.useEffect(() => {
|
|
1392
|
+
if (!open) { clearSearchMarks(); setCount(0); setCursor(-1); }
|
|
1393
|
+
}, [open]);
|
|
1394
|
+
React.useEffect(() => {
|
|
1395
|
+
if (!open) return undefined;
|
|
1396
|
+
const timer = setTimeout(paintNow, 3000);
|
|
1397
|
+
return () => clearTimeout(timer);
|
|
1260
1398
|
}, [open, query]);
|
|
1261
1399
|
const step = (backwards) => {
|
|
1262
1400
|
const marks = [...document.querySelectorAll('mark.w2mSearchMark')];
|
|
@@ -1286,7 +1424,7 @@ function ConversationSearch() {
|
|
|
1286
1424
|
placeholder: '搜索本对话全部内容',
|
|
1287
1425
|
onChange: (event) => setQuery(event.target.value),
|
|
1288
1426
|
onKeyDown: (event) => {
|
|
1289
|
-
if (event.key === 'Enter') { event.preventDefault(); step(event.shiftKey); }
|
|
1427
|
+
if (event.key === 'Enter') { event.preventDefault(); paintNow(); step(event.shiftKey); }
|
|
1290
1428
|
if (event.key === 'Escape') { setOpen(false); setQuery(''); }
|
|
1291
1429
|
},
|
|
1292
1430
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"pack": "node scripts/pack.mjs",
|
|
65
65
|
"reachability": "node scripts/reachability-check.mjs",
|
|
66
66
|
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
67
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/
|
|
67
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.test.mjs test/joint-cli.test.mjs test/joint-command-hash.test.mjs test/joint-dispatch-audit.test.mjs test/tools-cancel-auth.test.mjs test/fleet-mirror.test.mjs test/sink-git-ssh.test.mjs test/agent-sse-watchdog.test.mjs test/version-gate.test.mjs test/joint-merge-cli.test.mjs test/joint-progress.test.mjs test/project-room-truncation.test.mjs test/chat-crypto-aad.test.mjs test/agent-artifact-delivery.test.mjs test/dataplane-sink-route.test.mjs test/artifact-sink-e2e.test.mjs test/project-room-persistence.test.mjs test/project-room-disk-persistence.test.mjs test/project-stream-route.test.mjs",
|
|
68
68
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
69
69
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
70
70
|
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.5",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|