@harness-mix/cli 0.2.3 → 0.2.4
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/CHANGELOG.md +13 -0
- package/README.md +469 -467
- package/output/native-build/desktop-controller.mjs +1 -1
- package/output/native-build/renderer-extension.js +23 -4
- package/package.json +11 -9
- package/scripts/antigravity-adapter-test.cjs +647 -626
- package/scripts/codex-adapter-test.cjs +162 -127
- package/scripts/collaboration-test.cjs +274 -262
- package/scripts/jsonl-stdin-test.cjs +40 -31
- package/scripts/kiro-cursor-adapters-test.cjs +124 -100
- package/scripts/native-acp-depth-test.cjs +30 -5
- package/scripts/native-update-apply-test.cjs +269 -215
- package/scripts/native-update.cjs +78 -0
- package/scripts/native-vendor-adapters-test.cjs +196 -154
- package/scripts/salvage-rollout-writes.cjs +72 -0
- package/scripts/zcode-adapter-test.cjs +329 -0
- package/scripts/zcode-live-probe.cjs +66 -0
- package/src/main/adapters/antigravity.js +1428 -1418
- package/src/main/adapters/codex.js +656 -649
- package/src/main/adapters/native-acp-command.js +51 -48
- package/src/main/adapters/native-acp.js +47 -12
- package/src/main/adapters/qoder.js +12 -8
- package/src/main/adapters/zcode.js +921 -10
- package/src/main/host/collaboration.js +723 -715
- package/src/main/host/jsonl.js +130 -120
- package/src/main/native/config.js +9 -9
- package/src/main/native/launcher.js +252 -237
- package/src/main/native/process-utils.js +157 -57
- package/src/main/native/protocol.js +1221 -1187
- package/src/main/native/update-state.js +123 -110
- package/src/main/native/updater.js +460 -394
- package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
|
@@ -1,262 +1,274 @@
|
|
|
1
|
-
const assert = require('node:assert/strict');
|
|
2
|
-
const fs = require('node:fs/promises');
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const { HostRuntime } = require('../src/main/host/runtime');
|
|
6
|
-
const { mentionedAgents, teamTaskDepths, teamPhase, teamProgress } = require('../src/main/host/collaboration');
|
|
7
|
-
const { JsonlProcess } = require('../src/main/host/jsonl');
|
|
8
|
-
const wait = async fn => { for (let i = 0; i < 300; i++) { if (fn()) return; await new Promise(r => setTimeout(r, 10)); } throw new Error('Timed out'); };
|
|
9
|
-
|
|
10
|
-
async function main() {
|
|
11
|
-
const root = await fs.mkdtemp(path.resolve('output/collaboration-'));
|
|
12
|
-
const rt = new HostRuntime({ dataDirectory: path.join(root, 'data') });
|
|
13
|
-
await rt.store.load();
|
|
14
|
-
let connection, teamConnection, active = 0, maximum = 0;
|
|
15
|
-
const pending = new Map();
|
|
16
|
-
let leadPrompt = '';
|
|
17
|
-
const lead = { manifest: { id: 'lead', name: 'Lead', capabilities: { collaborationTools: true } },
|
|
18
|
-
async open(input) { connection = input.collaboration; return { emit: input.emit, collaborationEnabled: true }; },
|
|
19
|
-
async send(s, text) { leadPrompt = text; }, async cancel() {}, async close() {} };
|
|
20
|
-
const worker = { manifest: { id: 'worker', name: 'Worker', aliases: ['w'], capabilities: { collaborationTools: true } },
|
|
21
|
-
async open(input) {
|
|
22
|
-
if (input.collaboration) teamConnection = input.collaboration;
|
|
23
|
-
else assert.equal(input.collaboration, undefined);
|
|
24
|
-
return { id: input.thread.id, emit: input.emit, collaborationEnabled: !!input.collaboration };
|
|
25
|
-
},
|
|
26
|
-
async send(s, text) { active++; maximum = Math.max(maximum, active); pending.set(s.id, { s, text }); },
|
|
27
|
-
async cancel(s) { if (pending.delete(s.id)) active--; }, async close() {} };
|
|
28
|
-
const reviewer = { ...worker, manifest: { id: 'reviewer', name: 'Reviewer', capabilities: { collaborationTools: true } } };
|
|
29
|
-
rt.adapters.set('lead', lead); rt.status.lead = { available: true };
|
|
30
|
-
rt.adapters.set('worker', worker); rt.status.worker = { available: true };
|
|
31
|
-
rt.adapters.set('reviewer', reviewer); rt.status.reviewer = { available: true };
|
|
32
|
-
const parent = await rt.createThread({ harnessId: 'lead', cwd: root });
|
|
33
|
-
rt.collaboration.jobs.set('interrupted-fixture', { id: 'interrupted-fixture', owner: parent.id, agent: 'worker', childId: 'old-child', status: 'interrupted' });
|
|
34
|
-
const ownerConnection = connection;
|
|
35
|
-
const transport = new JsonlProcess(connection.command, connection.args, { env: { ...process.env, ...connection.env } }, {});
|
|
36
|
-
const call = (name, args) => rt.collaboration.call(parent.id, name, args);
|
|
37
|
-
const finish = (id, answer) => { const { s } = pending.get(id); pending.delete(id); active--; s.emit({ kind: 'text-delta', text: answer }); s.emit({ kind: 'completed', finalAnswer: true }); };
|
|
38
|
-
try {
|
|
39
|
-
assert.deepEqual(mentionedAgents('ask #w and [Worker](harness-mix://agent/worker) `#lead` issue#lead #worker/foo', rt), ['worker']);
|
|
40
|
-
assert.deepEqual(mentionedAgents('leave @w to native Codex mentions', rt), []);
|
|
41
|
-
assert.deepEqual(mentionedAgents('创建 Agent Team:Worker 负责开发,Reviewer 负责审查。', rt), ['worker', 'reviewer'], '纯文本 Harness 名称可在明确团队语境中授权');
|
|
42
|
-
assert.deepEqual(mentionedAgents('讨论 Worker 和 Reviewer 的界面显示', rt), [], '普通产品讨论不得误启动跨 Harness 协作');
|
|
43
|
-
assert.deepEqual(mentionedAgents('组建团队,但不要从 `Worker` 或 ```Reviewer``` 调度', rt), [], '代码区域中的名称不得作为授权');
|
|
44
|
-
assert.deepEqual(mentionedAgents('让 issue-worker 参与团队', rt), [], '较长标识符内的 Harness 子串不得误匹配');
|
|
45
|
-
const depthFixture = [{ id: 'a', dependsOn: [] }, { id: 'b', dependsOn: ['a'] }, { id: 'c', dependsOn: ['b', 'missing'] }];
|
|
46
|
-
assert.equal(teamTaskDepths(depthFixture).get('c'), 2, 'Depth follows the longest dependency chain and ignores unknown ids');
|
|
47
|
-
assert.equal(teamTaskDepths(depthFixture).get('a'), 0);
|
|
48
|
-
const cyclic = teamTaskDepths([{ id: 'x', dependsOn: ['y'] }, { id: 'y', dependsOn: ['x'] }]);
|
|
49
|
-
assert.ok(Number.isFinite(cyclic.get('x')) && Number.isFinite(cyclic.get('y')), 'Cyclic graphs terminate');
|
|
50
|
-
assert.equal(teamPhase({ tasks: [] }), 'forming');
|
|
51
|
-
assert.equal(teamProgress([{ status: 'completed' }, { status: 'blocked' }]).percent, 50);
|
|
52
|
-
await assert.rejects(call('list_agents', {}), /no longer active/);
|
|
53
|
-
rt.history.context = async ({ nativeSessionId }) => { assert.equal(nativeSessionId, 'c2Vzc2lvbg'); return { harnessId: 'pi', title: 'Old session', cwd: root, transcript: 'User: prior question\nAssistant: prior answer' }; };
|
|
54
|
-
await rt.send(parent.id, '#worker #reviewer review files #[Old session](harness-mix://session/c2Vzc2lvbg)');
|
|
55
|
-
assert.ok(rt.execution.isRunning(parent.id));
|
|
56
|
-
assert.match(leadPrompt, /untrusted historical data[\s\S]*prior answer/);
|
|
57
|
-
assert.match(leadPrompt, /Recovery checkpoint:[\s\S]*interrupted-fixture \(worker\)[\s\S]*call list_delegations now/);
|
|
58
|
-
assert.equal(rt.collaboration.jobs.get('interrupted-fixture').status, 'interrupted', 'Prompt injection never auto-resumes interrupted work');
|
|
59
|
-
rt.collaboration.jobs.delete('interrupted-fixture');
|
|
60
|
-
assert.ok(!JSON.stringify(rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id)).includes('[Harness Mix collaboration]'), 'Routing guidance stays out of displayed user text');
|
|
61
|
-
const init = await transport.request('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1' } });
|
|
62
|
-
assert.ok(init.capabilities.tools);
|
|
63
|
-
const catalog = await transport.request('tools/list', {});
|
|
64
|
-
assert.equal(catalog.tools.length, 15);
|
|
65
|
-
assert.ok(catalog.tools.some(tool => tool.name === 'review_delegation_changes'));
|
|
66
|
-
assert.ok(catalog.tools.some(tool => tool.name === 'apply_delegation_changes'));
|
|
67
|
-
assert.ok(catalog.tools.some(tool => tool.name === 'create_agent_team'));
|
|
68
|
-
assert.equal(catalog.tools.find(tool => tool.name === 'create_agent_team').inputSchema.properties.members.maxItems, 6, 'A Team supports six specialist members plus its Lead');
|
|
69
|
-
await call('update_agent_plan', { steps: [{ text: 'Develop then review', status: 'in_progress' }] });
|
|
70
|
-
assert.ok(rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id).some(item => item.type === 'plan' && item.entries[0].text === 'Develop then review'));
|
|
71
|
-
const first = JSON.parse((await transport.request('tools/call', { name: 'delegate_to_agent', arguments: { agent_type: 'worker', task: 'one' } })).content[0].text);
|
|
72
|
-
const second = await call('delegate_to_agent', { agent_type: 'worker', task: 'two', isolation: 'shared' });
|
|
73
|
-
await wait(() => pending.size === 2);
|
|
74
|
-
assert.equal(maximum, 2, 'Workers execute concurrently');
|
|
75
|
-
const jobs = [...rt.collaboration.jobs.values()];
|
|
76
|
-
for (const job of jobs) {
|
|
77
|
-
const child = rt.threads.find(t => t.id === job.childId);
|
|
78
|
-
assert.equal(child.parentThreadId, parent.id);
|
|
79
|
-
assert.equal(child.cwd, root, 'Default worker shares the lead workspace');
|
|
80
|
-
const assistant = child.messages.find(m => m.role === 'assistant');
|
|
81
|
-
assert.equal(assistant.reviewId, undefined);
|
|
82
|
-
assert.equal(assistant.reviewOwnerThreadId, parent.id);
|
|
83
|
-
}
|
|
84
|
-
const waitingChild = rt.threads.find(t => t.id === jobs[0].childId);
|
|
85
|
-
waitingChild && pending.get(waitingChild.id).s.emit({ kind: 'approval', requestId: 'native-approval', method: 'confirm', title: 'Allow test?' });
|
|
86
|
-
assert.equal(rt.collaboration.view(jobs[0]).display_status, 'waiting_approval');
|
|
87
|
-
await wait(() => rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id).some(item => item.type === 'tool_call' && String(item.output).includes('waiting_approval')));
|
|
88
|
-
pending.get(jobs[0].childId).s.emit({ kind: 'interaction-responded', requestId: 'native-approval' });
|
|
89
|
-
finish(jobs[0].childId, 'first-result');
|
|
90
|
-
await wait(() => jobs[0].status === 'completed');
|
|
91
|
-
const before = Date.now();
|
|
92
|
-
const partial = await call('get_delegation_status', { task_ids: [first.task_id, second.task_id], wait_ms: 3000 });
|
|
93
|
-
assert.equal(partial[1].status, 'running');
|
|
94
|
-
assert.ok(Date.now() - before < 1000, 'Fan-out collection returns when any result is ready');
|
|
95
|
-
finish(jobs[1].childId, 'second-result');
|
|
96
|
-
await wait(() => jobs[1].status === 'completed');
|
|
97
|
-
const results = await call('get_delegation_status', { task_ids: [first.task_id, second.task_id], wait_ms: 3000 });
|
|
98
|
-
assert.deepEqual(results.map(r => r.status), ['completed', 'completed']);
|
|
99
|
-
assert.deepEqual(results.map(r => r.result), ['first-result', 'second-result']);
|
|
100
|
-
const cards = rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id).filter(item => item.collaboration);
|
|
101
|
-
const { projectItem } = require('../src/main/native/protocol');
|
|
102
|
-
const spawnCard = cards.find(item => item.collaboration.operation === 'spawnAgent' && item.collaboration.task_id === jobs[0].id);
|
|
103
|
-
const workCard = cards.find(item => item.collaboration.operation === 'sendInput' && item.collaboration.task_id === jobs[0].id);
|
|
104
|
-
assert.ok(spawnCard && workCard, 'Delegation projects separate spawn and execution cards');
|
|
105
|
-
assert.equal(projectItem(spawnCard).type, 'collabAgentToolCall');
|
|
106
|
-
assert.equal(projectItem(spawnCard).status, 'completed', 'Spawn card settles once the agent session exists (no stuck 创建中)');
|
|
107
|
-
assert.deepEqual(projectItem(spawnCard).receiverThreadIds, [jobs[0].childId]);
|
|
108
|
-
assert.equal(projectItem(workCard).status, 'completed');
|
|
109
|
-
assert.equal(projectItem(workCard).agentsStates[jobs[0].childId].message, 'first-result');
|
|
110
|
-
assert.ok(rt.execution.isRunning(parent.id), 'Tool results do not end the native lead turn');
|
|
111
|
-
await call('message_agent', { task_id: first.task_id, task: 'follow-up' });
|
|
112
|
-
await wait(() => pending.size === 1);
|
|
113
|
-
finish(jobs[0].childId, 'follow-up-result');
|
|
114
|
-
assert.equal((await call('get_delegation_status', { task_ids: [first.task_id], wait_ms: 3000 }))[0].result, 'follow-up-result');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
assert.equal(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
assert.equal(
|
|
145
|
-
assert.equal(
|
|
146
|
-
assert.equal(
|
|
147
|
-
|
|
148
|
-
assert.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
assert.equal(
|
|
159
|
-
assert.equal(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
await
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
assert.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
assert.equal(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
await rt.collaboration.
|
|
175
|
-
const
|
|
176
|
-
assert.equal(
|
|
177
|
-
assert.equal(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
await
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
assert.
|
|
192
|
-
assert.
|
|
193
|
-
await
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
await
|
|
199
|
-
|
|
200
|
-
await
|
|
201
|
-
assert.
|
|
202
|
-
await
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
assert.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs/promises');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { HostRuntime } = require('../src/main/host/runtime');
|
|
6
|
+
const { mentionedAgents, teamTaskDepths, teamPhase, teamProgress } = require('../src/main/host/collaboration');
|
|
7
|
+
const { JsonlProcess } = require('../src/main/host/jsonl');
|
|
8
|
+
const wait = async fn => { for (let i = 0; i < 300; i++) { if (fn()) return; await new Promise(r => setTimeout(r, 10)); } throw new Error('Timed out'); };
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const root = await fs.mkdtemp(path.resolve('output/collaboration-'));
|
|
12
|
+
const rt = new HostRuntime({ dataDirectory: path.join(root, 'data') });
|
|
13
|
+
await rt.store.load();
|
|
14
|
+
let connection, teamConnection, active = 0, maximum = 0;
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
let leadPrompt = '';
|
|
17
|
+
const lead = { manifest: { id: 'lead', name: 'Lead', capabilities: { collaborationTools: true } },
|
|
18
|
+
async open(input) { connection = input.collaboration; return { emit: input.emit, collaborationEnabled: true }; },
|
|
19
|
+
async send(s, text) { leadPrompt = text; }, async cancel() {}, async close() {} };
|
|
20
|
+
const worker = { manifest: { id: 'worker', name: 'Worker', aliases: ['w'], capabilities: { collaborationTools: true } },
|
|
21
|
+
async open(input) {
|
|
22
|
+
if (input.collaboration) teamConnection = input.collaboration;
|
|
23
|
+
else assert.equal(input.collaboration, undefined);
|
|
24
|
+
return { id: input.thread.id, emit: input.emit, collaborationEnabled: !!input.collaboration };
|
|
25
|
+
},
|
|
26
|
+
async send(s, text) { active++; maximum = Math.max(maximum, active); pending.set(s.id, { s, text }); },
|
|
27
|
+
async cancel(s) { if (pending.delete(s.id)) active--; }, async close() {} };
|
|
28
|
+
const reviewer = { ...worker, manifest: { id: 'reviewer', name: 'Reviewer', capabilities: { collaborationTools: true } } };
|
|
29
|
+
rt.adapters.set('lead', lead); rt.status.lead = { available: true };
|
|
30
|
+
rt.adapters.set('worker', worker); rt.status.worker = { available: true };
|
|
31
|
+
rt.adapters.set('reviewer', reviewer); rt.status.reviewer = { available: true };
|
|
32
|
+
const parent = await rt.createThread({ harnessId: 'lead', cwd: root });
|
|
33
|
+
rt.collaboration.jobs.set('interrupted-fixture', { id: 'interrupted-fixture', owner: parent.id, agent: 'worker', childId: 'old-child', status: 'interrupted' });
|
|
34
|
+
const ownerConnection = connection;
|
|
35
|
+
const transport = new JsonlProcess(connection.command, connection.args, { env: { ...process.env, ...connection.env } }, {});
|
|
36
|
+
const call = (name, args) => rt.collaboration.call(parent.id, name, args);
|
|
37
|
+
const finish = (id, answer) => { const { s } = pending.get(id); pending.delete(id); active--; s.emit({ kind: 'text-delta', text: answer }); s.emit({ kind: 'completed', finalAnswer: true }); };
|
|
38
|
+
try {
|
|
39
|
+
assert.deepEqual(mentionedAgents('ask #w and [Worker](harness-mix://agent/worker) `#lead` issue#lead #worker/foo', rt), ['worker']);
|
|
40
|
+
assert.deepEqual(mentionedAgents('leave @w to native Codex mentions', rt), []);
|
|
41
|
+
assert.deepEqual(mentionedAgents('创建 Agent Team:Worker 负责开发,Reviewer 负责审查。', rt), ['worker', 'reviewer'], '纯文本 Harness 名称可在明确团队语境中授权');
|
|
42
|
+
assert.deepEqual(mentionedAgents('讨论 Worker 和 Reviewer 的界面显示', rt), [], '普通产品讨论不得误启动跨 Harness 协作');
|
|
43
|
+
assert.deepEqual(mentionedAgents('组建团队,但不要从 `Worker` 或 ```Reviewer``` 调度', rt), [], '代码区域中的名称不得作为授权');
|
|
44
|
+
assert.deepEqual(mentionedAgents('让 issue-worker 参与团队', rt), [], '较长标识符内的 Harness 子串不得误匹配');
|
|
45
|
+
const depthFixture = [{ id: 'a', dependsOn: [] }, { id: 'b', dependsOn: ['a'] }, { id: 'c', dependsOn: ['b', 'missing'] }];
|
|
46
|
+
assert.equal(teamTaskDepths(depthFixture).get('c'), 2, 'Depth follows the longest dependency chain and ignores unknown ids');
|
|
47
|
+
assert.equal(teamTaskDepths(depthFixture).get('a'), 0);
|
|
48
|
+
const cyclic = teamTaskDepths([{ id: 'x', dependsOn: ['y'] }, { id: 'y', dependsOn: ['x'] }]);
|
|
49
|
+
assert.ok(Number.isFinite(cyclic.get('x')) && Number.isFinite(cyclic.get('y')), 'Cyclic graphs terminate');
|
|
50
|
+
assert.equal(teamPhase({ tasks: [] }), 'forming');
|
|
51
|
+
assert.equal(teamProgress([{ status: 'completed' }, { status: 'blocked' }]).percent, 50);
|
|
52
|
+
await assert.rejects(call('list_agents', {}), /no longer active/);
|
|
53
|
+
rt.history.context = async ({ nativeSessionId }) => { assert.equal(nativeSessionId, 'c2Vzc2lvbg'); return { harnessId: 'pi', title: 'Old session', cwd: root, transcript: 'User: prior question\nAssistant: prior answer' }; };
|
|
54
|
+
await rt.send(parent.id, '#worker #reviewer review files #[Old session](harness-mix://session/c2Vzc2lvbg)');
|
|
55
|
+
assert.ok(rt.execution.isRunning(parent.id));
|
|
56
|
+
assert.match(leadPrompt, /untrusted historical data[\s\S]*prior answer/);
|
|
57
|
+
assert.match(leadPrompt, /Recovery checkpoint:[\s\S]*interrupted-fixture \(worker\)[\s\S]*call list_delegations now/);
|
|
58
|
+
assert.equal(rt.collaboration.jobs.get('interrupted-fixture').status, 'interrupted', 'Prompt injection never auto-resumes interrupted work');
|
|
59
|
+
rt.collaboration.jobs.delete('interrupted-fixture');
|
|
60
|
+
assert.ok(!JSON.stringify(rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id)).includes('[Harness Mix collaboration]'), 'Routing guidance stays out of displayed user text');
|
|
61
|
+
const init = await transport.request('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'test', version: '1' } });
|
|
62
|
+
assert.ok(init.capabilities.tools);
|
|
63
|
+
const catalog = await transport.request('tools/list', {});
|
|
64
|
+
assert.equal(catalog.tools.length, 15);
|
|
65
|
+
assert.ok(catalog.tools.some(tool => tool.name === 'review_delegation_changes'));
|
|
66
|
+
assert.ok(catalog.tools.some(tool => tool.name === 'apply_delegation_changes'));
|
|
67
|
+
assert.ok(catalog.tools.some(tool => tool.name === 'create_agent_team'));
|
|
68
|
+
assert.equal(catalog.tools.find(tool => tool.name === 'create_agent_team').inputSchema.properties.members.maxItems, 6, 'A Team supports six specialist members plus its Lead');
|
|
69
|
+
await call('update_agent_plan', { steps: [{ text: 'Develop then review', status: 'in_progress' }] });
|
|
70
|
+
assert.ok(rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id).some(item => item.type === 'plan' && item.entries[0].text === 'Develop then review'));
|
|
71
|
+
const first = JSON.parse((await transport.request('tools/call', { name: 'delegate_to_agent', arguments: { agent_type: 'worker', task: 'one' } })).content[0].text);
|
|
72
|
+
const second = await call('delegate_to_agent', { agent_type: 'worker', task: 'two', isolation: 'shared' });
|
|
73
|
+
await wait(() => pending.size === 2);
|
|
74
|
+
assert.equal(maximum, 2, 'Workers execute concurrently');
|
|
75
|
+
const jobs = [...rt.collaboration.jobs.values()];
|
|
76
|
+
for (const job of jobs) {
|
|
77
|
+
const child = rt.threads.find(t => t.id === job.childId);
|
|
78
|
+
assert.equal(child.parentThreadId, parent.id);
|
|
79
|
+
assert.equal(child.cwd, root, 'Default worker shares the lead workspace');
|
|
80
|
+
const assistant = child.messages.find(m => m.role === 'assistant');
|
|
81
|
+
assert.equal(assistant.reviewId, undefined);
|
|
82
|
+
assert.equal(assistant.reviewOwnerThreadId, parent.id);
|
|
83
|
+
}
|
|
84
|
+
const waitingChild = rt.threads.find(t => t.id === jobs[0].childId);
|
|
85
|
+
waitingChild && pending.get(waitingChild.id).s.emit({ kind: 'approval', requestId: 'native-approval', method: 'confirm', title: 'Allow test?' });
|
|
86
|
+
assert.equal(rt.collaboration.view(jobs[0]).display_status, 'waiting_approval');
|
|
87
|
+
await wait(() => rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id).some(item => item.type === 'tool_call' && String(item.output).includes('waiting_approval')));
|
|
88
|
+
pending.get(jobs[0].childId).s.emit({ kind: 'interaction-responded', requestId: 'native-approval' });
|
|
89
|
+
finish(jobs[0].childId, 'first-result');
|
|
90
|
+
await wait(() => jobs[0].status === 'completed');
|
|
91
|
+
const before = Date.now();
|
|
92
|
+
const partial = await call('get_delegation_status', { task_ids: [first.task_id, second.task_id], wait_ms: 3000 });
|
|
93
|
+
assert.equal(partial[1].status, 'running');
|
|
94
|
+
assert.ok(Date.now() - before < 1000, 'Fan-out collection returns when any result is ready');
|
|
95
|
+
finish(jobs[1].childId, 'second-result');
|
|
96
|
+
await wait(() => jobs[1].status === 'completed');
|
|
97
|
+
const results = await call('get_delegation_status', { task_ids: [first.task_id, second.task_id], wait_ms: 3000 });
|
|
98
|
+
assert.deepEqual(results.map(r => r.status), ['completed', 'completed']);
|
|
99
|
+
assert.deepEqual(results.map(r => r.result), ['first-result', 'second-result']);
|
|
100
|
+
const cards = rt.core.getItemsForTurn(rt.execution.lastTurn(parent.id).id).filter(item => item.collaboration);
|
|
101
|
+
const { projectItem } = require('../src/main/native/protocol');
|
|
102
|
+
const spawnCard = cards.find(item => item.collaboration.operation === 'spawnAgent' && item.collaboration.task_id === jobs[0].id);
|
|
103
|
+
const workCard = cards.find(item => item.collaboration.operation === 'sendInput' && item.collaboration.task_id === jobs[0].id);
|
|
104
|
+
assert.ok(spawnCard && workCard, 'Delegation projects separate spawn and execution cards');
|
|
105
|
+
assert.equal(projectItem(spawnCard).type, 'collabAgentToolCall');
|
|
106
|
+
assert.equal(projectItem(spawnCard).status, 'completed', 'Spawn card settles once the agent session exists (no stuck 创建中)');
|
|
107
|
+
assert.deepEqual(projectItem(spawnCard).receiverThreadIds, [jobs[0].childId]);
|
|
108
|
+
assert.equal(projectItem(workCard).status, 'completed');
|
|
109
|
+
assert.equal(projectItem(workCard).agentsStates[jobs[0].childId].message, 'first-result');
|
|
110
|
+
assert.ok(rt.execution.isRunning(parent.id), 'Tool results do not end the native lead turn');
|
|
111
|
+
await call('message_agent', { task_id: first.task_id, task: 'follow-up' });
|
|
112
|
+
await wait(() => pending.size === 1);
|
|
113
|
+
finish(jobs[0].childId, 'follow-up-result');
|
|
114
|
+
assert.equal((await call('get_delegation_status', { task_ids: [first.task_id], wait_ms: 3000 }))[0].result, 'follow-up-result');
|
|
115
|
+
// 回归:apply 的同目录并发守卫不得把正在等待 MCP 工具结果的 lead 回合自身计为并发
|
|
116
|
+
// (此前条件恒真,apply_delegation_changes 不可能成功);第三方会话占用同目录时仍必须拦截。
|
|
117
|
+
rt.collaboration.jobs.set('apply-guard-job', { id: 'apply-guard-job', owner: parent.id, agent: 'worker', status: 'completed',
|
|
118
|
+
workspace: { mode: 'worktree', cwd: path.join(root, 'guard-wt'), root: path.join(root, 'guard-wt'), source: root, branch: 'guard', baseTree: 't', baseCommit: 'c' } });
|
|
119
|
+
await assert.rejects(rt.collaboration.apply('apply-guard-job', '0'.repeat(64)),
|
|
120
|
+
error => !/结算后再应用/.test(error.message), '运行中的 lead 自身不得触发同目录并发守卫');
|
|
121
|
+
const guardIntruder = await rt.createThread({ harnessId: 'worker', cwd: root });
|
|
122
|
+
const intruderTurn = rt.send(guardIntruder.id, '占用目录');
|
|
123
|
+
await wait(() => rt.execution.isRunning(guardIntruder.id));
|
|
124
|
+
await assert.rejects(rt.collaboration.apply('apply-guard-job', '0'.repeat(64)), /结算后再应用/, '第三方同目录运行会话仍被守卫拦截');
|
|
125
|
+
await rt.cancel(guardIntruder.id);
|
|
126
|
+
await intruderTurn;
|
|
127
|
+
const team = await call('create_agent_team', { name: 'Release team', goal: 'Ship a verified change', members: [{ name: 'Builder', role: 'Implement and coordinate', agent_type: 'worker' }, { name: 'Reviewer', role: 'Independently verify', agent_type: 'reviewer' }, { name: 'Frontend', role: 'Integrate the native UI', agent_type: 'worker' }, { name: 'QA', role: 'Run the regression matrix', agent_type: 'reviewer' }, { name: 'Docs', role: 'Document the delivery', agent_type: 'worker' }, { name: 'Release', role: 'Verify Git and final Diff', agent_type: 'reviewer' }] });
|
|
128
|
+
assert.equal(team.lead.agent, 'lead');
|
|
129
|
+
assert.match(team.lead.role, /Lead/);
|
|
130
|
+
assert.equal(team.members.length, 6);
|
|
131
|
+
const teamTask = (await call('assign_team_task', { team_id: team.team_id, title: 'Build', description: 'Implement the change', assignee: team.members[0].id })).task;
|
|
132
|
+
const dependentAssign = await call('assign_team_task', { team_id: team.team_id, title: 'Verify', description: 'Verify the result', assignee: team.members[1].id, depends_on: [teamTask.id] });
|
|
133
|
+
const dependent = dependentAssign.task;
|
|
134
|
+
assert.equal(dependent.status, 'blocked');
|
|
135
|
+
assert.equal(dependentAssign.team.phase, 'waiting', 'Assigned work with nothing in flight reads as waiting');
|
|
136
|
+
assert.deepEqual(dependentAssign.team.progress, { total: 2, pending: 1, blocked: 1, in_progress: 0, completed: 0, failed: 0, interrupted: 0, percent: 0 });
|
|
137
|
+
assert.equal(dependentAssign.team.tasks.find(entry => entry.id === dependent.id).depth, 1, 'Dependent task sits one dependency lane deep');
|
|
138
|
+
assert.equal(dependentAssign.team.tasks.find(entry => entry.id === teamTask.id).depth, 0);
|
|
139
|
+
const teamJob = await call('delegate_to_agent', { agent_type: 'worker', task: 'Implement as Builder', team_id: team.team_id, member_id: team.members[0].id, team_task_id: teamTask.id, isolation: 'shared' });
|
|
140
|
+
await wait(() => pending.size === 1 && teamConnection);
|
|
141
|
+
const teammateTransport = new JsonlProcess(teamConnection.command, teamConnection.args, { env: { ...process.env, ...teamConnection.env } }, {});
|
|
142
|
+
await teammateTransport.request('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'teammate-test', version: '1' } });
|
|
143
|
+
const teammateState = JSON.parse((await teammateTransport.request('tools/call', { name: 'get_team_state', arguments: { team_id: team.team_id } })).content[0].text);
|
|
144
|
+
assert.equal(teammateState.members[0].display_status, 'working');
|
|
145
|
+
assert.equal(teammateState.phase, 'running', 'In-flight work reads as running');
|
|
146
|
+
assert.equal(teammateState.progress.in_progress, 1);
|
|
147
|
+
await teammateTransport.request('tools/call', { name: 'send_team_message', arguments: { team_id: team.team_id, to: 'lead', task_id: teamTask.id, kind: 'review-request', message: 'Implementation is ready for verification.' } });
|
|
148
|
+
await assert.rejects(rt.collaboration.call(parent.id, 'send_team_message', { team_id: team.team_id, to: 'lead', message: 'bad kind', kind: 'bogus' }), /invalid_value|Invalid/, 'Message kind is a closed enum');
|
|
149
|
+
const teamChild = rt.collaboration.jobs.get(teamJob.task_id).childId;
|
|
150
|
+
await assert.rejects(rt.collaboration.call(teamChild, 'assign_team_task', { team_id: team.team_id, title: 'Escalate', description: 'Must be lead-owned', assignee: team.members[0].id }), /Only the Team Lead/);
|
|
151
|
+
await assert.rejects(rt.collaboration.call(teamChild, 'delegate_to_agent', { agent_type: 'worker', task: 'recursive' }), /Only lead/);
|
|
152
|
+
finish(teamChild, 'team-result');
|
|
153
|
+
await wait(() => rt.collaboration.jobs.get(teamJob.task_id).status === 'completed');
|
|
154
|
+
const settledTeam = await call('get_team_state', { team_id: team.team_id });
|
|
155
|
+
assert.equal(settledTeam.tasks.find(entry => entry.id === teamTask.id).status, 'completed');
|
|
156
|
+
assert.equal(settledTeam.tasks.find(entry => entry.id === dependent.id).status, 'pending');
|
|
157
|
+
assert.equal(settledTeam.messages[0].fromName, 'Builder');
|
|
158
|
+
assert.equal(settledTeam.messages[0].kind, 'review-request', 'Mailbox messages keep their typed kind');
|
|
159
|
+
assert.equal(settledTeam.phase, 'waiting', 'Build settled but dependent review is not delegated yet');
|
|
160
|
+
assert.equal(settledTeam.progress.completed, 1);
|
|
161
|
+
assert.equal(settledTeam.members[0].child_thread_id ?? settledTeam.members[0].childId, teamChild);
|
|
162
|
+
const verifyJob = await call('delegate_to_agent', { agent_type: 'reviewer', task: 'Verify independently as Reviewer', team_id: team.team_id, member_id: team.members[1].id, team_task_id: dependent.id, isolation: 'shared' });
|
|
163
|
+
await wait(() => rt.collaboration.jobs.get(verifyJob.task_id).childId && pending.has(rt.collaboration.jobs.get(verifyJob.task_id).childId));
|
|
164
|
+
const reviewerChild = rt.collaboration.jobs.get(verifyJob.task_id).childId;
|
|
165
|
+
assert.notEqual(reviewerChild, teamChild, 'Different Harness teammates own different native sessions');
|
|
166
|
+
finish(reviewerChild, 'verification-result');
|
|
167
|
+
await wait(() => rt.collaboration.jobs.get(verifyJob.task_id).status === 'completed');
|
|
168
|
+
const polish = (await call('assign_team_task', { team_id: team.team_id, title: 'Polish', description: 'Address final details', assignee: team.members[0].id, depends_on: [dependent.id] })).task;
|
|
169
|
+
const polishJob = await call('delegate_to_agent', { agent_type: 'worker', task: 'Polish in the persistent Builder session', team_id: team.team_id, member_id: team.members[0].id, team_task_id: polish.id, isolation: 'worktree' });
|
|
170
|
+
assert.equal(polishJob.child_thread_id, teamChild, 'A teammate keeps one native session across team tasks');
|
|
171
|
+
assert.equal(polishJob.workspace.cwd, rt.collaboration.jobs.get(teamJob.task_id).workspace.cwd, 'A teammate keeps its existing workspace across team tasks');
|
|
172
|
+
await wait(() => pending.has(teamChild));
|
|
173
|
+
finish(teamChild, 'polish-result');
|
|
174
|
+
await wait(() => rt.collaboration.jobs.get(polishJob.task_id).status === 'completed');
|
|
175
|
+
const inspectedTeam = await rt.collaboration.inspectTeam(parent.id, team.team_id);
|
|
176
|
+
assert.equal(inspectedTeam.team.team_id, team.team_id);
|
|
177
|
+
assert.equal(inspectedTeam.team.phase, 'completed', 'All team tasks settled');
|
|
178
|
+
assert.equal(inspectedTeam.team.progress.percent, 100);
|
|
179
|
+
assert.ok(inspectedTeam.snapshots.length >= 8, 'Team lifecycle is preserved as replayable snapshots');
|
|
180
|
+
assert.ok(inspectedTeam.snapshots.some(entry => entry.action === 'member_session_ready'));
|
|
181
|
+
assert.ok(inspectedTeam.snapshots.some(entry => entry.action === 'task_settled'));
|
|
182
|
+
assert.equal((await rt.collaboration.inspectTeam(teamChild, team.team_id)).team.team_id, team.team_id, 'Members may inspect their own shared team state');
|
|
183
|
+
const { NativeProtocol } = require('../src/main/native/protocol');
|
|
184
|
+
const protocolTeam = await new NativeProtocol(rt, () => {}).request('harnessmix/thread/team/inspect', { threadId: parent.id, teamId: team.team_id });
|
|
185
|
+
assert.equal(protocolTeam.snapshots.at(-1).team.team_id, team.team_id, 'Renderer protocol exposes the persisted Team timeline');
|
|
186
|
+
await rt.collaboration.saveTeams();
|
|
187
|
+
const persistedTeams = JSON.parse(await fs.readFile(path.join(root, 'data', 'collaboration', 'teams.json'), 'utf8'));
|
|
188
|
+
assert.equal(persistedTeams[0].id, team.team_id);
|
|
189
|
+
assert.equal(persistedTeams[0].history.length, inspectedTeam.snapshots.length);
|
|
190
|
+
teammateTransport.stop();
|
|
191
|
+
await assert.rejects(call('get_delegation_status', { task_ids: ['foreign-task'] }), /Unknown task/);
|
|
192
|
+
await assert.rejects(rt.collaboration.call(jobs[0].childId, 'delegate_to_agent', { agent_type: 'worker', task: 'recursive' }), /Only lead/);
|
|
193
|
+
await assert.rejects(call('delegate_to_agent', { agent_type: 'worker', task: 'x', unexpected: true }));
|
|
194
|
+
const savedMentions = parent.activeMentions;
|
|
195
|
+
delete parent.activeMentions;
|
|
196
|
+
await assert.rejects(call('delegate_to_agent', { agent_type: 'worker', task: 'unauthorized' }), /用户本轮未显式委派/);
|
|
197
|
+
parent.activeMentions = savedMentions;
|
|
198
|
+
const denied = await fetch(ownerConnection.env.HARNESS_MIX_COLLAB_URL, { method: 'POST', headers: { Authorization: 'Bearer invalid' }, body: '{}' });
|
|
199
|
+
assert.equal(denied.status, 403);
|
|
200
|
+
const outsider = await rt.createThread({ harnessId: 'worker', cwd: root });
|
|
201
|
+
await assert.rejects(rt.collaboration.inspectTeam(outsider.id, team.team_id), /not a team participant/);
|
|
202
|
+
const noTeam = await rt.collaboration.inspectTeam(outsider.id);
|
|
203
|
+
assert.equal(noTeam.team, null, 'Inspecting a non-team thread without a teamId answers benignly');
|
|
204
|
+
assert.deepEqual(noTeam.snapshots, []);
|
|
205
|
+
await rt.send(outsider.id, 'unrelated');
|
|
206
|
+
assert.equal(outsider.messages.at(-1).concurrent, true, 'Independent same-workspace turns are marked concurrent');
|
|
207
|
+
await rt.cancel(outsider.id); await wait(() => !pending.has(outsider.id));
|
|
208
|
+
for (let i = 0; i < 6; i++) await call('delegate_to_agent', { agent_type: 'worker', task: 'cancel me', isolation: 'shared' });
|
|
209
|
+
await wait(() => pending.size === 6);
|
|
210
|
+
await assert.rejects(call('delegate_to_agent', { agent_type: 'worker', task: 'too many' }), /six concurrent/);
|
|
211
|
+
await rt.cancel(parent.id);
|
|
212
|
+
await wait(() => !pending.size);
|
|
213
|
+
assert.equal(rt.execution.lastTurn(parent.id).status, 'cancelled');
|
|
214
|
+
await assert.rejects(call('list_agents', {}), /no longer active/);
|
|
215
|
+
|
|
216
|
+
// Settings → Collaboration 开关:默认开启;关闭后服务端硬拒绝,并持久化到磁盘
|
|
217
|
+
const prefsRoot = await fs.mkdtemp(path.resolve('output/collaboration-prefs-'));
|
|
218
|
+
const prefsRt = new HostRuntime({ dataDirectory: path.join(prefsRoot, 'data') });
|
|
219
|
+
try {
|
|
220
|
+
await prefsRt.collaboration.initialize();
|
|
221
|
+
assert.deepEqual(prefsRt.collaboration.getPreferences(), { collaboration: true, agentTeam: true }, 'Both collaboration switches default to on');
|
|
222
|
+
await prefsRt.collaboration.setPreferences({ collaboration: false });
|
|
223
|
+
await assert.rejects(prefsRt.collaboration.call('any-thread', 'list_agents', {}), /停用.*disabled/, 'Collaboration tools are hard-rejected while the switch is off');
|
|
224
|
+
await prefsRt.collaboration.setPreferences({ collaboration: true, agentTeam: false });
|
|
225
|
+
await assert.rejects(
|
|
226
|
+
prefsRt.collaboration.call('any-thread', 'create_agent_team', { name: 't', goal: 'g', members: [{ name: 'A', role: 'r', agent_type: 'worker' }] }),
|
|
227
|
+
/Agent Team 已在设置中停用/,
|
|
228
|
+
'create_agent_team is rejected while the Agent Team switch is off, before any turn-state check',
|
|
229
|
+
);
|
|
230
|
+
const reloaded = new HostRuntime({ dataDirectory: path.join(prefsRoot, 'data') });
|
|
231
|
+
await reloaded.collaboration.initialize();
|
|
232
|
+
assert.deepEqual(reloaded.collaboration.getPreferences(), { collaboration: true, agentTeam: false }, 'Preferences survive a Host restart');
|
|
233
|
+
await reloaded.close();
|
|
234
|
+
const onAgain = await prefsRt.collaboration.setPreferences({ agentTeam: true });
|
|
235
|
+
assert.deepEqual(onAgain, { collaboration: true, agentTeam: true });
|
|
236
|
+
} finally { await prefsRt.close(); }
|
|
237
|
+
|
|
238
|
+
// 同目录外部并发(另一个独立会话正在运行)→ 用户收到 toast 警告,
|
|
239
|
+
// 且新委派的 worker 默认升级为隔离模式(auto);非 Git 目录下 auto 回落 shared。
|
|
240
|
+
// 用系统临时目录:output/ 位于本仓库内,auto 在仓库内会真实创建 worktree。
|
|
241
|
+
const isoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'harness-mix-iso-'));
|
|
242
|
+
const isoRt = new HostRuntime({ dataDirectory: path.join(isoRoot, 'data') });
|
|
243
|
+
try {
|
|
244
|
+
await isoRt.store.load();
|
|
245
|
+
const isoToasts = [];
|
|
246
|
+
isoRt.subscribe(event => { if (event.type === 'toast') isoToasts.push(event); });
|
|
247
|
+
const isoPending = new Map();
|
|
248
|
+
const isoLead = { manifest: { id: 'lead', name: 'Lead', capabilities: { collaborationTools: true } },
|
|
249
|
+
async open(input) { return { emit: input.emit, collaborationEnabled: true }; },
|
|
250
|
+
async send() {}, async cancel() {}, async close() {} };
|
|
251
|
+
const isoWorker = { manifest: { id: 'worker', name: 'Worker', capabilities: { collaborationTools: true } },
|
|
252
|
+
async open(input) { return { id: input.thread.id, emit: input.emit, collaborationEnabled: !!input.collaboration }; },
|
|
253
|
+
async send(s) { isoPending.set(s.id, s); }, async cancel() {}, async close() {} };
|
|
254
|
+
isoRt.adapters.set('lead', isoLead); isoRt.status.lead = { available: true };
|
|
255
|
+
isoRt.adapters.set('worker', isoWorker); isoRt.status.worker = { available: true };
|
|
256
|
+
const isoParent = await isoRt.createThread({ harnessId: 'lead', cwd: isoRoot });
|
|
257
|
+
await isoRt.send(isoParent.id, '#worker build something');
|
|
258
|
+
const isoOutsider = await isoRt.createThread({ harnessId: 'worker', cwd: isoRoot });
|
|
259
|
+
await isoRt.send(isoOutsider.id, 'other session');
|
|
260
|
+
await wait(() => isoPending.has(isoOutsider.id));
|
|
261
|
+
assert.equal(isoOutsider.messages.at(-1).concurrent, true, 'Concurrent turn is still marked');
|
|
262
|
+
assert.ok(isoToasts.some(t => t.threadId === isoOutsider.id && /同一目录/.test(t.text)), 'Concurrent same-directory sessions warn the user');
|
|
263
|
+
const isoJob = await isoRt.collaboration.call(isoParent.id, 'delegate_to_agent', { agent_type: 'worker', task: 'auto-isolated under concurrency' });
|
|
264
|
+
assert.equal(isoRt.collaboration.jobs.get(isoJob.task_id).isolation, 'auto', 'A new worker defaults to isolated mode while another session runs in the same directory');
|
|
265
|
+
await wait(() => isoRt.collaboration.jobs.get(isoJob.task_id).workspace);
|
|
266
|
+
assert.equal(isoRt.collaboration.jobs.get(isoJob.task_id).workspace.mode, 'shared', 'Auto falls back to shared outside Git projects');
|
|
267
|
+
await isoRt.cancel(isoOutsider.id);
|
|
268
|
+
const quietJob = await isoRt.collaboration.call(isoParent.id, 'delegate_to_agent', { agent_type: 'worker', task: 'shared again once quiet' });
|
|
269
|
+
assert.equal(isoRt.collaboration.jobs.get(quietJob.task_id).isolation, 'shared', 'Default returns to shared once the directory is quiet');
|
|
270
|
+
} finally { await isoRt.close(); }
|
|
271
|
+
console.log('PASS: real MCP stdio → authenticated Host → parallel native-session adapters → results/follow-up/cancellation, ownership and shared review');
|
|
272
|
+
} finally { transport.stop(); await rt.close(); }
|
|
273
|
+
}
|
|
274
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|