@harness-mix/cli 0.1.12 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/NOTICE +14 -0
  3. package/README.md +4 -24
  4. package/docs/multi-agent-collaboration.md +1 -1
  5. package/licenses/anthropic-codex-theme-MIT.txt +21 -0
  6. package/licenses/codex-dream-skin-MIT.txt +21 -0
  7. package/output/native-build/renderer-extension.js +703 -58
  8. package/package.json +192 -189
  9. package/scripts/collaboration-test.cjs +262 -200
  10. package/scripts/collaboration-ui-smoke.cjs +85 -77
  11. package/scripts/core-concurrency-test.cjs +23 -1
  12. package/scripts/e2e-pi-cancel.cjs +99 -0
  13. package/scripts/native-file-change-test.cjs +42 -2
  14. package/scripts/native-sidebar-test.cjs +8 -0
  15. package/scripts/native-vendor-adapters-test.cjs +67 -1
  16. package/scripts/pets-test.cjs +63 -0
  17. package/scripts/pi-cancel-race-test.cjs +102 -0
  18. package/scripts/stuck-turn-test.cjs +73 -0
  19. package/scripts/workspace-test.cjs +9 -0
  20. package/src/assets/skins/dream-skin/NOTICE.md +13 -0
  21. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/hero.webp +0 -0
  22. package/src/assets/skins/dream-skin/themes/gothic-void-crusade/theme.json +34 -0
  23. package/src/main/adapters/claude.js +9 -6
  24. package/src/main/adapters/pi-family.js +41 -8
  25. package/src/main/harness-adapter/event-normalizer.js +270 -258
  26. package/src/main/host/collaboration-mcp.cjs +8 -1
  27. package/src/main/host/collaboration-tools.js +1 -1
  28. package/src/main/host/collaboration.js +81 -11
  29. package/src/main/host/runtime.js +1420 -1341
  30. package/src/main/native/pets.js +79 -2
  31. package/src/main/native/protocol.js +1160 -1145
  32. package/src/main/native/thread-list.js +6 -1
  33. package/src/main/workspace/core-review.js +75 -0
  34. package/src/main/workspace/diff.js +5 -1
  35. package/src/main/workspace/file-changes.js +9 -4
  36. package/src/main/workspace/review-controller.js +115 -102
  37. package/src/main/workspace/review.js +90 -88
  38. package/src/native-ui/renderer-extension/dist/contract-audit.js +16637 -0
  39. package/src/native-ui/renderer-extension/dist/index.js +34853 -0
  40. package/src/native-ui/renderer-extension/dist/production.js +34263 -0
  41. package/src/native-ui/renderer-extension/dist/renderer-binding-probe.js +34254 -0
  42. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  43. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +11 -0
  44. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  45. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts +73 -0
  46. package/src/native-ui/renderer-extension/dist/types/renderer-pet-switcher.d.ts.map +1 -0
  47. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  48. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  49. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  50. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts +12 -0
  51. package/src/native-ui/renderer-extension/dist/types/settings/collaboration-page.d.ts.map +1 -0
  52. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
  53. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
  56. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  57. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +1 -1
  58. package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -1
  59. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +9 -0
  60. package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -1
  61. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -1
  63. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  64. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3181 -3171
  65. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +14 -0
  66. package/src/native-ui/renderer-extension/src/renderer-pet-switcher.ts +308 -0
  67. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  68. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +76 -21
  69. package/src/native-ui/renderer-extension/src/settings/collaboration-page.ts +139 -0
  70. package/src/native-ui/renderer-extension/src/settings/icons.ts +155 -153
  71. package/src/native-ui/renderer-extension/src/settings/localization.ts +785 -783
  72. package/src/native-ui/renderer-extension/src/settings/pages.ts +694 -681
  73. package/src/native-ui/renderer-extension/src/settings/pet-market.css +110 -1
  74. package/src/native-ui/renderer-extension/src/settings/pet-market.ts +276 -22
  75. package/src/native-ui/renderer-extension/src/settings/pets-client.ts +32 -0
  76. package/src/native-ui/renderer-extension/src/settings/shell.css +2294 -2255
  77. package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +51 -1
  78. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
  79. package/src/native-ui/renderer-extension/test/renderer-pet-switcher.test.ts +400 -0
  80. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +80 -79
  81. package/src/native-ui/renderer-extension/test/settings/pet-market.test.ts +378 -0
  82. package/src/native-ui/renderer-extension/test/settings/pets-client.test.ts +83 -0
  83. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  84. 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 = new Set(['create_agent_team', 'assign_team_task', 'get_team_state', 'update_team_task', 'send_team_message']);
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 toolCallId = `collaboration:${randomUUID()}`;
464
- const operation = job.childId ? 'sendInput' : 'spawnAgent';
465
- const emit = event => {
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
- emit({ kind: 'tool', toolCallId, title: `Agent 协作 · ${rt.adapters.get(job.agent).manifest.name}`, input: task, state: 'running', output: JSON.stringify(this.view(job)) });
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 { await this.save(); emit({ kind: 'tool', toolCallId, state: job.status === 'completed' ? 'done' : 'error', output: JSON.stringify(this.view(job)) }); }
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) {