@harness-mix/cli 0.2.0 → 0.2.2
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 +17 -0
- package/NOTICE +14 -0
- package/README.md +4 -24
- package/docs/multi-agent-collaboration.md +1 -1
- package/licenses/anthropic-codex-theme-MIT.txt +21 -0
- package/licenses/codex-dream-skin-MIT.txt +21 -0
- package/output/native-build/renderer-extension.js +703 -58
- package/package.json +192 -189
- package/scripts/collaboration-test.cjs +262 -200
- package/scripts/collaboration-ui-smoke.cjs +85 -77
- package/scripts/core-concurrency-test.cjs +23 -1
- package/scripts/e2e-pi-cancel.cjs +99 -0
- package/scripts/native-file-change-test.cjs +42 -2
- package/scripts/native-sidebar-test.cjs +8 -0
- package/scripts/native-vendor-adapters-test.cjs +67 -1
- package/scripts/pets-test.cjs +63 -0
- package/scripts/pi-cancel-race-test.cjs +102 -0
- package/scripts/stuck-turn-test.cjs +73 -0
- package/scripts/workspace-test.cjs +9 -0
- package/src/assets/skins/dream-skin/NOTICE.md +13 -0
- package/src/assets/skins/dream-skin/themes/gothic-void-crusade/hero.webp +0 -0
- package/src/assets/skins/dream-skin/themes/gothic-void-crusade/theme.json +34 -0
- package/src/main/adapters/claude.js +9 -6
- package/src/main/adapters/pi-family.js +41 -8
- package/src/main/harness-adapter/event-normalizer.js +270 -258
- package/src/main/host/collaboration-mcp.cjs +8 -1
- package/src/main/host/collaboration-tools.js +1 -1
- package/src/main/host/collaboration.js +81 -11
- package/src/main/host/runtime.js +1462 -1341
- package/src/main/native/pets.js +79 -2
- package/src/main/native/protocol.js +1164 -1145
- package/src/main/native/thread-list.js +6 -1
- package/src/main/workspace/core-review.js +75 -0
- package/src/main/workspace/diff.js +5 -1
- package/src/main/workspace/file-changes.js +9 -4
- package/src/main/workspace/review-controller.js +115 -102
- package/src/main/workspace/review.js +90 -88
- package/src/native-ui/renderer-extension/dist/contract-audit.js +16637 -0
- package/src/native-ui/renderer-extension/dist/index.js +34853 -0
- package/src/native-ui/renderer-extension/dist/production.js +34263 -0
- package/src/native-ui/renderer-extension/dist/renderer-binding-probe.js +34254 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +11 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts +73 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts +12 -0
- package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +9 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3181 -3171
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +14 -0
- package/src/native-ui/renderer-extension/src/renderer-pet-switcher.ts +308 -0
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
- package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +76 -21
- package/src/native-ui/renderer-extension/src/settings/collaboration-page.ts +139 -0
- package/src/native-ui/renderer-extension/src/settings/icons.ts +155 -153
- package/src/native-ui/renderer-extension/src/settings/localization.ts +785 -783
- package/src/native-ui/renderer-extension/src/settings/pages.ts +694 -681
- package/src/native-ui/renderer-extension/src/settings/pet-market.css +110 -1
- package/src/native-ui/renderer-extension/src/settings/pet-market.ts +276 -22
- package/src/native-ui/renderer-extension/src/settings/pets-client.ts +32 -0
- package/src/native-ui/renderer-extension/src/settings/shell.css +2294 -2255
- package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +51 -1
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
- package/src/native-ui/renderer-extension/test/renderer-pet-switcher.test.ts +400 -0
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +80 -79
- package/src/native-ui/renderer-extension/test/settings/pet-market.test.ts +378 -0
- package/src/native-ui/renderer-extension/test/settings/pets-client.test.ts +83 -0
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
- package/src/native-ui/renderer-extension/test/settings/skin-runtime.test.ts +15 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const http = require('node:http');
|
|
2
2
|
const { randomUUID } = require('node:crypto');
|
|
3
|
+
const fs = require('node:fs');
|
|
3
4
|
const path = require('node:path');
|
|
4
5
|
const { tools } = require('./collaboration-tools');
|
|
5
6
|
const { z } = require('zod');
|
|
@@ -10,6 +11,12 @@ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
10
11
|
const MAX_CONCURRENT_SUBTASKS = 6;
|
|
11
12
|
const MAX_SUBTASKS_PER_TURN = 16;
|
|
12
13
|
|
|
14
|
+
// Settings → Collaboration 开关:两者默认开启。collaboration 关闭时不再注入协作
|
|
15
|
+
// MCP、不解析 # 提及、拒绝一切协作工具调用;agentTeam 关闭时保留一次性委派,
|
|
16
|
+
// 但隐藏并拒绝 create_agent_team 等团队工具。
|
|
17
|
+
const DEFAULT_PREFERENCES = Object.freeze({ collaboration: true, agentTeam: true });
|
|
18
|
+
const TEAM_TOOL_NAMES = new Set(['create_agent_team', 'assign_team_task', 'get_team_state', 'update_team_task', 'send_team_message']);
|
|
19
|
+
|
|
13
20
|
// Dependency depth of each task: the longest chain of prerequisites, used to
|
|
14
21
|
// lay the team board out in lanes.
|
|
15
22
|
// Missing dependency ids are ignored (the runtime already rejects unknown ids at
|
|
@@ -79,10 +86,40 @@ class Collaboration {
|
|
|
79
86
|
this.store = new Store(path.join(runtime.store.directory, 'collaboration'));
|
|
80
87
|
this.teamStore = new Store(path.join(runtime.store.directory, 'collaboration'), 'teams.json');
|
|
81
88
|
this.teams = new Map();
|
|
89
|
+
this.prefFile = path.join(runtime.store.directory, 'collaboration', 'preferences.json');
|
|
90
|
+
this.prefs = { ...DEFAULT_PREFERENCES };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async loadPreferences() {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(await fs.promises.readFile(this.prefFile, 'utf8'));
|
|
96
|
+
// 只认显式的 false:缺失字段/旧文件一律回落到默认开启
|
|
97
|
+
this.prefs = {
|
|
98
|
+
collaboration: parsed?.collaboration !== false,
|
|
99
|
+
agentTeam: parsed?.agentTeam !== false,
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error.code !== 'ENOENT') throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getPreferences() {
|
|
107
|
+
return { ...this.prefs };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async setPreferences(patch = {}) {
|
|
111
|
+
await this.initialize();
|
|
112
|
+
if (typeof patch.collaboration === 'boolean') this.prefs.collaboration = patch.collaboration;
|
|
113
|
+
if (typeof patch.agentTeam === 'boolean') this.prefs.agentTeam = patch.agentTeam;
|
|
114
|
+
await fs.promises.mkdir(path.dirname(this.prefFile), { recursive: true });
|
|
115
|
+
const tmp = `${this.prefFile}.${process.pid}.${randomUUID()}.tmp`;
|
|
116
|
+
await fs.promises.writeFile(tmp, JSON.stringify(this.prefs, null, 2));
|
|
117
|
+
await fs.promises.rename(tmp, this.prefFile);
|
|
118
|
+
return this.getPreferences();
|
|
82
119
|
}
|
|
83
120
|
|
|
84
121
|
async initialize() {
|
|
85
|
-
if (!this.loading) this.loading = Promise.all([this.store.load(), this.teamStore.load()]).then(async ([rows, teams]) => {
|
|
122
|
+
if (!this.loading) this.loading = this.loadPreferences().then(() => Promise.all([this.store.load(), this.teamStore.load()])).then(async ([rows, teams]) => {
|
|
86
123
|
for (const row of rows) {
|
|
87
124
|
if (!row.id || !row.owner || !row.agent) throw new Error('Invalid collaboration history');
|
|
88
125
|
this.jobs.set(row.id, { ...row, ...(row.status === 'running' ? { status: 'interrupted', error: 'Host restarted; resume this native session explicitly.' } : {}) });
|
|
@@ -191,9 +228,12 @@ class Collaboration {
|
|
|
191
228
|
|
|
192
229
|
emitTeam(team, action) {
|
|
193
230
|
if (!this.runtime.execution.isRunning(team.owner)) return;
|
|
231
|
+
// 团队全部完成时把团队卡片结算为 done——否则它会作为未终态 tool_call
|
|
232
|
+
// 一直挂到回合结束,被投影成「执行中」。完成后若再 reopen,会以 running 复更。
|
|
233
|
+
const done = team.status === 'completed';
|
|
194
234
|
this.runtime.emitCollaboration(team.owner, {
|
|
195
235
|
kind: 'tool', toolCallId: `agent-team:${team.id}`, title: `Agent Team · ${team.name}`,
|
|
196
|
-
state: 'running', input: team.goal, output: JSON.stringify({ action, ...this.teamView(team) }),
|
|
236
|
+
state: done ? 'done' : 'running', input: team.goal, output: JSON.stringify({ action, ...this.teamView(team) }),
|
|
197
237
|
});
|
|
198
238
|
}
|
|
199
239
|
|
|
@@ -250,7 +290,7 @@ class Collaboration {
|
|
|
250
290
|
let key = this.keys.get(thread.id);
|
|
251
291
|
if (!key) { key = randomUUID(); this.keys.set(thread.id, key); }
|
|
252
292
|
return { command: process.execPath, args: [path.join(__dirname, 'collaboration-mcp.cjs')],
|
|
253
|
-
env: { HARNESS_MIX_COLLAB_URL: `http://127.0.0.1:${this.server.address().port}`, HARNESS_MIX_COLLAB_KEY: key } };
|
|
293
|
+
env: { HARNESS_MIX_COLLAB_URL: `http://127.0.0.1:${this.server.address().port}`, HARNESS_MIX_COLLAB_KEY: key, HARNESS_MIX_COLLAB_TEAM: this.prefs.agentTeam ? '1' : '0' } };
|
|
254
294
|
}
|
|
255
295
|
|
|
256
296
|
async handle(req, res) {
|
|
@@ -360,13 +400,19 @@ class Collaboration {
|
|
|
360
400
|
await this.initialize();
|
|
361
401
|
if (this.closing) throw new Error('Host is closing');
|
|
362
402
|
if (!validators.has(name)) throw new Error('Unknown collaboration tool');
|
|
403
|
+
if (!this.prefs.collaboration) {
|
|
404
|
+
throw new Error('多 Agent 协作已在设置中停用(设置 → 协作)。Multi-Agent collaboration is disabled in Settings → Collaboration.');
|
|
405
|
+
}
|
|
363
406
|
args = validators.get(name).parse(args);
|
|
364
407
|
const rt = this.runtime;
|
|
365
|
-
const teamTools =
|
|
408
|
+
const teamTools = TEAM_TOOL_NAMES;
|
|
366
409
|
const participant = this.participant(principal, args.team_id);
|
|
367
410
|
const owner = participant?.team.owner ?? principal;
|
|
368
411
|
const parent = rt.threads.find(t => t.id === owner);
|
|
369
412
|
if (teamTools.has(name)) {
|
|
413
|
+
if (name === 'create_agent_team' && !this.prefs.agentTeam) {
|
|
414
|
+
throw new Error('Agent Team 已在设置中停用(设置 → 协作)。Agent Team is disabled in Settings → Collaboration; one-shot delegation remains available.');
|
|
415
|
+
}
|
|
370
416
|
if (!rt.execution.isRunning(principal) || (principal === owner && this.cancelling.has(owner))) throw new Error('Collaboration turn is no longer active');
|
|
371
417
|
return this.teamCall(principal, name, args);
|
|
372
418
|
}
|
|
@@ -407,8 +453,16 @@ class Collaboration {
|
|
|
407
453
|
const jobs = [...this.jobs.values()].filter(j => j.owner === owner);
|
|
408
454
|
if (jobs.filter(j => j.status === 'running').length >= MAX_CONCURRENT_SUBTASKS) throw new Error('At most six concurrent subtasks; collect existing results first');
|
|
409
455
|
if (jobs.filter(j => j.turnId === rt.execution.lastTurn(owner)?.id).length >= MAX_SUBTASKS_PER_TURN) throw new Error('At most sixteen subtasks per lead turn');
|
|
456
|
+
// Risk-aware default: while another session outside this collaboration group is
|
|
457
|
+
// actively running in the lead directory, a shared workspace would let both sides
|
|
458
|
+
// silently overwrite each other — start new workers isolated ('auto' isolates Git
|
|
459
|
+
// projects into a worktree and falls back to shared only outside Git). A teammate's
|
|
460
|
+
// inherited workspace and an explicit isolation argument still win over the default.
|
|
461
|
+
const externalActive = rt.threads.some(t => t.id !== owner && t.parentThreadId !== owner && !this.isParticipant(t, owner)
|
|
462
|
+
&& String(t.cwd).toLowerCase() === String(parent.cwd).toLowerCase()
|
|
463
|
+
&& (rt.execution.isRunning(t.id) || t.reviewPending));
|
|
410
464
|
const job = { id: randomUUID(), owner, agent, turnId: rt.execution.lastTurn(owner).id, status: 'running', task: args.task,
|
|
411
|
-
isolation: previousMemberJob?.isolation ?? args.isolation ?? 'shared',
|
|
465
|
+
isolation: previousMemberJob?.isolation ?? args.isolation ?? (externalActive ? 'auto' : 'shared'),
|
|
412
466
|
...(previousMemberJob?.workspace ? { workspace: previousMemberJob.workspace } : {}),
|
|
413
467
|
...(team ? { teamId: team.id, memberId: member.id, teamTaskId: teamTask.id, ...(member.childId ? { childId: member.childId } : {}) } : {}) };
|
|
414
468
|
this.jobs.set(job.id, job);
|
|
@@ -460,18 +514,25 @@ class Collaboration {
|
|
|
460
514
|
async run(parent, job, task) {
|
|
461
515
|
const rt = this.runtime;
|
|
462
516
|
const turnId = job.turnId;
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
517
|
+
const title = `Agent 协作 · ${rt.adapters.get(job.agent).manifest.name}`;
|
|
518
|
+
// 「创建智能体」(spawnAgent) 与「执行中」(sendInput) 是两个独立的投影 item:
|
|
519
|
+
// spawn 在子会话就绪后立即结算为 done,否则 Desktop 原生协作卡片会在整个
|
|
520
|
+
// 执行期间一直停留在「创建中 N 个智能体」。
|
|
521
|
+
const spawnCallId = `collaboration:${randomUUID()}`;
|
|
522
|
+
const workCallId = `collaboration:${randomUUID()}`;
|
|
523
|
+
const emit = (event, operation, toolCallId) => {
|
|
466
524
|
if (rt.execution.lastTurn(parent.id)?.id === turnId) rt.emitCollaboration(parent.id, { ...event,
|
|
467
525
|
collaboration: { ...this.view(job), operation } });
|
|
468
526
|
};
|
|
527
|
+
// 已有子会话的后续输入(message_agent / resume / 团队持久成员)不涉及创建。
|
|
528
|
+
let spawnSettled = !!job.childId;
|
|
469
529
|
try {
|
|
470
530
|
if (!job.workspace) {
|
|
471
531
|
job.workspace = await createWorkspace(parent.cwd, job.id, job.isolation);
|
|
472
532
|
await this.save();
|
|
473
533
|
}
|
|
474
534
|
const workerPermMode = defaultWorkerPermissionMode(job.agent);
|
|
535
|
+
if (!spawnSettled) emit({ kind: 'tool', toolCallId: spawnCallId, title, input: task, state: 'running', output: JSON.stringify(this.view(job)) }, 'spawnAgent', spawnCallId);
|
|
475
536
|
const child = job.childId ? rt.threads.find(t => t.id === job.childId) : await rt.createThread({
|
|
476
537
|
harnessId: job.agent, cwd: job.workspace.cwd, title: `${parent.title} › ${task.slice(0, 40)}`, parentThreadId: parent.id,
|
|
477
538
|
options: { ...(workerPermMode ? { permissionMode: workerPermMode } : {}) },
|
|
@@ -486,7 +547,11 @@ class Collaboration {
|
|
|
486
547
|
if (!child) throw new Error('Native child history is missing; no replacement session was created');
|
|
487
548
|
job.childId = child.id;
|
|
488
549
|
await this.save();
|
|
489
|
-
|
|
550
|
+
if (!spawnSettled) {
|
|
551
|
+
emit({ kind: 'tool', toolCallId: spawnCallId, title, input: task, state: 'done', output: JSON.stringify(this.view(job)) }, 'spawnAgent', spawnCallId);
|
|
552
|
+
spawnSettled = true;
|
|
553
|
+
}
|
|
554
|
+
emit({ kind: 'tool', toolCallId: workCallId, title, input: task, state: 'running', output: JSON.stringify(this.view(job)) }, 'sendInput', workCallId);
|
|
490
555
|
if (job.status !== 'running' || this.closing || !rt.execution.isRunning(parent.id)) { job.status = 'cancelled'; return; }
|
|
491
556
|
// Child native file events remain visible; only the lead snapshots the shared workspace.
|
|
492
557
|
const sending = rt.send(child.id, task, { collaborationOf: parent.id, isolated: job.workspace.mode === 'worktree' });
|
|
@@ -504,7 +569,7 @@ class Collaboration {
|
|
|
504
569
|
turnInactiveSince = null;
|
|
505
570
|
}
|
|
506
571
|
const current = this.view(job).display_status;
|
|
507
|
-
if (current !== displayedStatus) { displayedStatus = current; emit({ kind: 'tool', toolCallId, state: 'running', output: JSON.stringify(this.view(job)) }); }
|
|
572
|
+
if (current !== displayedStatus) { displayedStatus = current; emit({ kind: 'tool', toolCallId: workCallId, state: 'running', output: JSON.stringify(this.view(job)) }, 'sendInput', workCallId); }
|
|
508
573
|
if (Date.now() > until) { await rt.cancel(child.id); throw new Error('Subtask timed out after 30 minutes'); }
|
|
509
574
|
await delay(100);
|
|
510
575
|
}
|
|
@@ -549,7 +614,12 @@ class Collaboration {
|
|
|
549
614
|
if (team) { this.refreshTeamStatus(team); await this.publishTeam(team, 'task_failed'); }
|
|
550
615
|
}
|
|
551
616
|
}
|
|
552
|
-
finally {
|
|
617
|
+
finally {
|
|
618
|
+
await this.save();
|
|
619
|
+
// 创建阶段失败(如原生会话启动报错)时,把仍在「创建中」的 spawn 卡片结算为错误。
|
|
620
|
+
if (!spawnSettled) emit({ kind: 'tool', toolCallId: spawnCallId, title, input: task, state: 'error', output: JSON.stringify(this.view(job)) }, 'spawnAgent', spawnCallId);
|
|
621
|
+
emit({ kind: 'tool', toolCallId: workCallId, title, state: job.status === 'completed' ? 'done' : 'error', output: JSON.stringify(this.view(job)) }, 'sendInput', workCallId);
|
|
622
|
+
}
|
|
553
623
|
}
|
|
554
624
|
|
|
555
625
|
async cancel(job) {
|