@harness-mix/cli 0.1.5 → 0.1.7

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 (92) hide show
  1. package/README.md +86 -56
  2. package/docs/harness-management.md +4 -4
  3. package/docs/multi-agent-collaboration.md +9 -9
  4. package/docs/storage-verification-design.md +49 -0
  5. package/output/native-build/renderer-extension.js +4210 -2273
  6. package/package.json +11 -9
  7. package/scripts/codebuddy-migration-test.cjs +1 -1
  8. package/scripts/collaboration-test.cjs +8 -3
  9. package/scripts/collaboration-ui-smoke.cjs +40 -26
  10. package/scripts/core-runtime-test.cjs +9 -1
  11. package/scripts/integrations-test.cjs +63 -4
  12. package/scripts/integrations-ui-smoke.cjs +77 -16
  13. package/scripts/native-protocol-test.cjs +113 -2
  14. package/scripts/native-sidebar-test.cjs +7 -0
  15. package/scripts/native-skill-roots-test.cjs +90 -0
  16. package/scripts/native-ui-smoke.cjs +23 -4
  17. package/scripts/projector-test.cjs +13 -0
  18. package/scripts/storage-verification-test.cjs +78 -0
  19. package/scripts/switch-harness-test.cjs +1 -1
  20. package/scripts/test-live-mentions.cjs +152 -0
  21. package/src/main/adapters/antigravity.js +20 -17
  22. package/src/main/adapters/claude.js +41 -8
  23. package/src/main/adapters/codebuddy.js +1 -0
  24. package/src/main/adapters/codex.js +20 -2
  25. package/src/main/adapters/cursor.js +5 -1
  26. package/src/main/adapters/dsh.js +8 -1
  27. package/src/main/adapters/grok.js +4 -1
  28. package/src/main/adapters/hermes.js +8 -0
  29. package/src/main/adapters/kiro.js +2 -1
  30. package/src/main/adapters/managed-mcp.js +15 -2
  31. package/src/main/adapters/omp.js +11 -0
  32. package/src/main/adapters/openclaw.js +6 -0
  33. package/src/main/adapters/opencode.js +5 -1
  34. package/src/main/adapters/pi.js +5 -1
  35. package/src/main/adapters/qoder.js +1 -0
  36. package/src/main/adapters/trae.js +4 -0
  37. package/src/main/adapters/zcode.js +3 -0
  38. package/src/main/host/collaboration.js +59 -11
  39. package/src/main/host/core-session.js +7 -1
  40. package/src/main/host/handoff-checkpoints.js +8 -1
  41. package/src/main/host/integrations.js +164 -26
  42. package/src/main/host/runtime.js +270 -116
  43. package/src/main/host/store.js +32 -11
  44. package/src/main/host/thread-storage.js +73 -0
  45. package/src/main/host/thread-store.js +123 -0
  46. package/src/main/host/verification-gates.js +118 -0
  47. package/src/main/native/protocol.js +209 -83
  48. package/src/main/native/thread-list.js +3 -0
  49. package/src/main/protocol-core/projector.js +20 -3
  50. package/src/main/shared-contracts/event.js +2 -1
  51. package/src/main/shared-contracts/item.js +1 -0
  52. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts +2 -2
  53. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -1
  56. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
  57. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  58. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +29 -4
  59. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -1
  60. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +5 -0
  61. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +2 -1
  63. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  64. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
  65. package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
  66. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +2 -1
  67. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -1
  68. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  69. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
  70. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  71. package/src/native-ui/renderer-extension/dist/types/settings/storage-page.d.ts +20 -0
  72. package/src/native-ui/renderer-extension/dist/types/settings/storage-page.d.ts.map +1 -0
  73. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  74. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +8 -88
  75. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +150 -168
  76. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +5 -1
  77. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
  78. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +36 -3
  79. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +4 -0
  80. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +12 -0
  81. package/src/native-ui/renderer-extension/src/settings/icons.ts +6 -3
  82. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1434 -62
  83. package/src/native-ui/renderer-extension/src/settings/localization.ts +8 -4
  84. package/src/native-ui/renderer-extension/src/settings/pages.ts +17 -8
  85. package/src/native-ui/renderer-extension/src/settings/shell.css +103 -8
  86. package/src/native-ui/renderer-extension/src/settings/storage-page.ts +74 -0
  87. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +4 -1
  88. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
  89. package/src/native-ui/renderer-extension/test/settings/harness-settings.test.ts +5 -0
  90. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  91. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +50 -4
  92. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +8 -2
@@ -20,7 +20,7 @@ const CLAUDE_PERMISSION_MODES = [
20
20
  { id: "acceptEdits", label: "接受编辑", description: "允许文件编辑;其他受保护操作前询问" },
21
21
  { id: "auto", label: "自动模式", description: "由 Claude 判断权限请求" },
22
22
  { id: "dontAsk", label: "免询问", description: "跳过权限询问(非绕过检查)" },
23
- { id: "bypassPermissions", label: "绕过权限", description: "跳过全部权限检查,谨慎使用", dangerous: true },
23
+ { id: "bypassPermissions", label: "绕过权限 (YOLO)", description: "跳过全部权限检查(YOLO 模式),自动放行工具执行", dangerous: true },
24
24
  ];
25
25
 
26
26
  function summarizeInput(input) {
@@ -244,14 +244,25 @@ async function loadSdk() {
244
244
  return import("@anthropic-ai/claude-agent-sdk");
245
245
  }
246
246
 
247
+ function normalizeClaudePermissionMode(mode) {
248
+ if (mode === 'yolo' || mode === 'skip' || mode === 'dangerously-skip-permissions') {
249
+ return 'bypassPermissions';
250
+ }
251
+ return mode;
252
+ }
253
+
247
254
  /** 建立一个常驻 SDK 会话(open 与 fork 共用):构造 query、启动事件泵 */
248
- function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelId, effort, emit, collaboration, managedMcp = [], onPlanLimit }) {
255
+ function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelId, effort, emit, collaboration, managedMcp = [], onPlanLimit, isWorker = false }) {
249
256
  const input = new MessageQueue();
257
+ const normalizedPermissionMode = normalizeClaudePermissionMode(permissionMode) || (isWorker ? 'bypassPermissions' : undefined);
258
+ const isBypass = normalizedPermissionMode === 'bypassPermissions' || isWorker;
259
+
250
260
  const session = {
251
261
  nativeSessionId: resumeId || newSessionId,
252
262
  cwd,
253
263
  collaborationEnabled: !!collaboration,
254
- permissionMode,
264
+ permissionMode: normalizedPermissionMode,
265
+ isWorker,
255
266
  model: undefined,
256
267
  input,
257
268
  query: undefined,
@@ -270,10 +281,17 @@ function spawnSession(sdk, { cwd, resumeId, newSessionId, permissionMode, modelI
270
281
  ...(resumeId ? { resume: resumeId } : {}),
271
282
  ...(!resumeId && newSessionId ? { sessionId: newSessionId } : {}),
272
283
  ...(effort ? { effort } : {}),
273
- ...(permissionMode ? { permissionMode } : {}),
284
+ ...(normalizedPermissionMode ? { permissionMode: normalizedPermissionMode } : {}),
285
+ ...(isBypass ? { allowDangerouslySkipPermissions: true } : {}),
274
286
  ...(modelId ? { model: modelId } : {}),
275
287
  ...(process.env.HARNESS_MIX_CLAUDE_EXECUTABLE ? { pathToClaudeCodeExecutable: process.env.HARNESS_MIX_CLAUDE_EXECUTABLE } : {}),
276
288
  canUseTool: (toolName, toolInput, { signal, suggestions }) => {
289
+ if (toolName !== 'AskUserQuestion') {
290
+ const currentMode = normalizeClaudePermissionMode(session.permissionMode);
291
+ if (currentMode === 'bypassPermissions' || session.isWorker) {
292
+ return { behavior: 'allow', updatedInput: toolInput };
293
+ }
294
+ }
277
295
  const requestId = randomUUID();
278
296
  emit(projectApproval(requestId, toolName, toolInput, suggestions));
279
297
  return new Promise((resolve) => {
@@ -380,12 +398,16 @@ function create() {
380
398
  },
381
399
  async open({ thread, emit, collaboration, managedMcp }) {
382
400
  const sdk = await loadSdk();
401
+ const isWorker = Boolean(thread.parentThreadId);
402
+ const rawMode = thread.options?.permissionMode;
403
+ const permissionMode = normalizeClaudePermissionMode(rawMode) || (isWorker ? 'bypassPermissions' : undefined);
383
404
  return spawnSession(sdk, {
384
405
  cwd: thread.cwd,
385
406
  resumeId: thread.restore ? thread.nativeSessionId : undefined,
386
407
  newSessionId: thread.restore ? undefined : thread.nativeSessionId,
387
408
  effort: thread.options?.thinking,
388
- permissionMode: thread.options?.permissionMode,
409
+ permissionMode,
410
+ isWorker,
389
411
  modelId: thread.options?.model?.id,
390
412
  emit,
391
413
  collaboration,
@@ -412,11 +434,19 @@ function create() {
412
434
  },
413
435
 
414
436
  async cancel(session) {
437
+ for (const pending of session.pendingApprovals?.values() ?? []) {
438
+ pending.resolve({ behavior: "deny", message: "用户取消了请求", interrupt: true });
439
+ }
440
+ session.pendingApprovals?.clear();
415
441
  // 原生优雅中断;超时后由 Host 走 close 兜底
416
442
  await Promise.race([
417
443
  session.query?.interrupt().catch(() => {}),
418
- new Promise((r) => setTimeout(r, 5_000)),
444
+ new Promise((r) => setTimeout(r, 2_000)),
419
445
  ]);
446
+ if (session.state.turn) {
447
+ session.state.turn.resolve();
448
+ session.state.turn = null;
449
+ }
420
450
  },
421
451
 
422
452
  async listCommands(session) {
@@ -452,10 +482,12 @@ function create() {
452
482
  const copied = await getSessionMessages(result.sessionId, { dir: source.cwd });
453
483
  const checkpointMap = Object.fromEntries(copied.map((entry, index) => [history[index].uuid, entry.uuid]));
454
484
  // Fork 出的新原生会话立即拉起常驻进程(resume 到新 sessionId),与 open() 同路径
485
+ const isWorker = Boolean(source.parentThreadId);
455
486
  const session = spawnSession(sdk, {
456
487
  cwd: source.cwd,
457
488
  resumeId: result.sessionId,
458
489
  permissionMode: source.options?.permissionMode,
490
+ isWorker,
459
491
  emit,
460
492
  onPlanLimit,
461
493
  });
@@ -476,8 +508,9 @@ function create() {
476
508
  return session.model;
477
509
  },
478
510
  async setPermissionMode(session, mode) {
479
- session.permissionMode = mode;
480
- await session.query?.setPermissionMode(mode);
511
+ const normalized = normalizeClaudePermissionMode(mode);
512
+ session.permissionMode = normalized;
513
+ await session.query?.setPermissionMode(normalized);
481
514
  },
482
515
  async setThinkingLevel(session, level) {
483
516
  const models = await this.listModelsFor(session);
@@ -1,2 +1,3 @@
1
1
  const { nativeAcp } = require('./native-acp');
2
2
  module.exports = nativeAcp({ id: 'codebuddy', name: 'CodeBuddy', aliases: ['workbuddy', 'WorkBuddy'], args: ['--acp'] });
3
+ module.exports.manifest.integrations.skills = { global: ['.codebuddy/skills'], project: ['.codebuddy/skills'] };
@@ -401,8 +401,19 @@ function create() {
401
401
  },
402
402
 
403
403
  async cancel(session) {
404
+ for (const pending of session.pendingApprovals?.values() ?? []) {
405
+ pending.resolve({ action: 'decline' });
406
+ }
407
+ session.pendingApprovals?.clear();
404
408
  if (!session.state.nativeTurnId) return;
405
- await session.host.request('turn/interrupt', { threadId: session.nativeSessionId, turnId: session.state.nativeTurnId });
409
+ await Promise.race([
410
+ session.host.request('turn/interrupt', { threadId: session.nativeSessionId, turnId: session.state.nativeTurnId }).catch(() => {}),
411
+ new Promise((r) => setTimeout(r, 2_000)),
412
+ ]);
413
+ if (session.state.turn) {
414
+ session.state.turn.resolve();
415
+ session.state.turn = null;
416
+ }
406
417
  },
407
418
 
408
419
  async respond(session, requestId, response) {
@@ -557,5 +568,12 @@ function create() {
557
568
  };
558
569
  }
559
570
 
560
- manifest.integrations = { mcp: true, skills: { global: ['.agents/skills'], project: ['.agents/skills'] } };
571
+ // Global discovery: ~/.agents/skills is the recommended location; $CODEX_HOME/skills
572
+ // (~/.codex/skills) is deprecated upstream but still loaded for backwards compatibility.
573
+ // https://developers.openai.com/codex/skills
574
+ manifest.integrations = { mcp: true, skills: {
575
+ global: ['.agents/skills', '.codex/skills'],
576
+ project: ['.agents/skills'],
577
+ overrides: { '.codex/skills': { env: 'CODEX_HOME', suffix: 'skills' } },
578
+ } };
561
579
  module.exports = { manifest, create, projectNotification, queueRequest, usageView, modelView };
@@ -1,2 +1,6 @@
1
1
  const { nativeAcp } = require('./native-acp');
2
- module.exports = nativeAcp({ id: 'cursor-cli', name: 'Cursor', aliases: ['cursor', 'cursor-cli', 'cursorcli'], args: ['acp'] });
2
+ module.exports = nativeAcp({ id: 'cursor-cli', name: 'Cursor', aliases: ['cursor', 'cursor-cli', 'cursorcli'], args: ['acp'] });
3
+ module.exports.manifest.integrations.skills = {
4
+ global: ['.cursor/skills', '.agents/skills', '.claude/skills', '.codex/skills'],
5
+ project: ['.cursor/skills', '.agents/skills', '.claude/skills', '.codex/skills'],
6
+ };
@@ -443,5 +443,12 @@ function create() {
443
443
  return web;
444
444
  }
445
445
 
446
- manifest.integrations = { mcp: true };
446
+ manifest.integrations = { mcp: true, skills: {
447
+ global: ['.dsh/skills', '.agents/skills'],
448
+ project: ['.dsh/skills', '.agents/skills'],
449
+ overrides: {
450
+ '.dsh/skills': { env: 'DSH_HOME', suffix: 'skills' },
451
+ '.agents/skills': { env: 'DSH_AGENTS_HOME', suffix: 'skills' },
452
+ },
453
+ } };
447
454
  module.exports = { manifest, create, projectWireEvent, flattenCatalog };
@@ -275,4 +275,7 @@ async function doGrokCompact(session, userContext, hooks) {
275
275
  hooks?.emit?.({ kind: 'completed', finalAnswer: outcome !== 'cancelled' });
276
276
  }
277
277
  module.exports = { ...grokAdapter(), projectUsage, parseGrokCompactionUpdate, doGrokCompact };
278
- module.exports.manifest.integrations = { mcp: true };
278
+ module.exports.manifest.integrations = { mcp: true, skills: {
279
+ global: ['.grok/skills', '.agents/skills', '.claude/skills'],
280
+ project: ['.grok/skills', '.claude/skills'],
281
+ } };
@@ -22,3 +22,11 @@ module.exports = acpAdapter({
22
22
  usage: false,
23
23
  contextUsage: false,
24
24
  });
25
+ // ~/.hermes/skills is Hermes' single source of truth (categories become subdirectories);
26
+ // it has no documented project-scope skills directory. HERMES_HOME relocates the home.
27
+ // https://hermes-agent.nousresearch.com/docs/user-guide/features/skills
28
+ module.exports.manifest.integrations.skills = {
29
+ global: ['.hermes/skills'],
30
+ project: [],
31
+ overrides: { '.hermes/skills': { env: 'HERMES_HOME', suffix: 'skills' } },
32
+ };
@@ -1,2 +1,3 @@
1
1
  const { nativeAcp } = require('./native-acp');
2
- module.exports = nativeAcp({ id: 'kiro-cli', name: 'Kiro', aliases: ['kiro', 'kiro-cli', 'kirocli'], args: ['acp', '--agent-engine', 'v3', '--auth-method', 'cli'] });
2
+ module.exports = nativeAcp({ id: 'kiro-cli', name: 'Kiro', aliases: ['kiro', 'kiro-cli', 'kirocli'], args: ['acp', '--agent-engine', 'v3', '--auth-method', 'cli'] });
3
+ module.exports.manifest.integrations.skills = { global: ['.kiro/skills'], project: ['.kiro/skills'] };
@@ -3,8 +3,21 @@ function nativeServers(managed = [], collaboration) {
3
3
  return [...managed, ...(collaboration ? [{ name: 'harness-mix', ...collaboration }] : [])];
4
4
  }
5
5
  function acpServers(managed, collaboration) {
6
- return nativeServers(managed, collaboration).map(s => ({ name: s.name, command: s.command, args: s.args,
7
- env: Object.entries(s.env || {}).map(([name, value]) => ({ name, value })) }));
6
+ return nativeServers(managed, collaboration).map(s => {
7
+ if (s.url) {
8
+ return {
9
+ name: s.name,
10
+ url: s.url,
11
+ headers: Object.entries(s.http_headers || {}).map(([name, value]) => ({ name, value })),
12
+ };
13
+ }
14
+ return {
15
+ name: s.name,
16
+ command: s.command,
17
+ args: s.args || [],
18
+ env: Object.entries(s.env || {}).map(([name, value]) => ({ name, value })),
19
+ };
20
+ });
8
21
  }
9
22
  function namedServers(managed, collaboration) {
10
23
  return Object.fromEntries(nativeServers(managed, collaboration).map(({ name, ...server }) => [name, server]));
@@ -10,3 +10,14 @@ module.exports = piFamily({
10
10
  packageHint: 'npm i -g @oh-my-pi/pi-coding-agent 或 https://omp.sh/install',
11
11
  aliases: ['omp', 'oh-my-pi'],
12
12
  });
13
+ module.exports.manifest.integrations = {
14
+ mcp: false,
15
+ skills: {
16
+ global: ['.omp/agent/skills', '.agents/skills', '.pi/agent/skills'],
17
+ project: ['.omp/skills', '.agents/skills', '.pi/skills'],
18
+ overrides: {
19
+ '.omp/agent/skills': { env: 'OMP_CODING_AGENT_DIR', suffix: 'skills' },
20
+ '.pi/agent/skills': { env: 'PI_CODING_AGENT_DIR', suffix: 'skills' },
21
+ },
22
+ },
23
+ };
@@ -18,6 +18,12 @@ const manifest = {
18
18
  capabilities: { streaming: true, thinking: false, tools: true, approvals: true, questions: false, models: true, thinkingLevels: true, permissionModes: false, resume: true, fork: false, forkFromMessage: false, compaction: false, usage: true, contextUsage: true, attachments: true },
19
19
  };
20
20
 
21
+ manifest.integrations = { mcp: false, skills: {
22
+ global: ['.openclaw/skills', '.agents/skills'],
23
+ project: ['skills', '.agents/skills'],
24
+ overrides: { '.openclaw/skills': { env: 'OPENCLAW_STATE_DIR', suffix: 'skills' } },
25
+ } };
26
+
21
27
  const textOfContent = (result) => (result?.content ?? []).map((c) => (c?.type === "text" ? c.text : "")).filter(Boolean).join("\n");
22
28
  const stringifyArgs = (args) => {
23
29
  if (args == null) return undefined;
@@ -180,5 +180,9 @@ function create() {
180
180
  };
181
181
  return adapter;
182
182
  }
183
- manifest.integrations = { mcp: true, skills: { global: ['.config/opencode/skills'], project: ['.opencode/skills'], overrides: { '.config/opencode/skills': { env: 'XDG_CONFIG_HOME', suffix: 'opencode/skills' } } } };
183
+ manifest.integrations = { mcp: true, skills: {
184
+ global: ['.config/opencode/skills', '.claude/skills', '.agents/skills'],
185
+ project: ['.opencode/skills', '.claude/skills', '.agents/skills'],
186
+ overrides: { '.config/opencode/skills': { env: 'XDG_CONFIG_HOME', suffix: 'opencode/skills' } },
187
+ } };
184
188
  module.exports = { manifest, create, projectEvent, projectPart, describeSession };
@@ -8,4 +8,8 @@ module.exports = piFamily({
8
8
  bin: 'pi',
9
9
  packageHint: 'npm i -g @earendil-works/pi-coding-agent',
10
10
  });
11
- module.exports.manifest.integrations = { mcp: false, skills: { global: ['.pi/agent/skills'], project: ['.pi/skills'], overrides: { '.pi/agent/skills': { env: 'PI_CODING_AGENT_DIR', suffix: 'skills' } } } };
11
+ module.exports.manifest.integrations = { mcp: false, skills: {
12
+ global: ['.pi/agent/skills', '.agents/skills'],
13
+ project: ['.pi/skills', '.agents/skills'],
14
+ overrides: { '.pi/agent/skills': { env: 'PI_CODING_AGENT_DIR', suffix: 'skills' } },
15
+ } };
@@ -5,3 +5,4 @@ module.exports = nativeAcp({
5
5
  id: 'qoder', name: 'Qoder', args: ['--acp'],
6
6
  capabilities: { questions: false, thinkingLevels: false, usage: true, contextUsage: true, attachments: true, fork: false, compaction: false },
7
7
  });
8
+ module.exports.manifest.integrations.skills = { global: ['.qoder/skills'], project: ['.qoder/skills'] };
@@ -5,3 +5,7 @@ module.exports = nativeAcp({
5
5
  id: 'trae', name: 'Trae', args: [],
6
6
  capabilities: { questions: false, thinkingLevels: false, usage: true, contextUsage: true, attachments: false, fork: false, compaction: false },
7
7
  });
8
+ // Trae IDE uses .trae/skills; TraeCode CLI uses .traecli/skills. Both are registered so the
9
+ // CLI-backed (ACP) session and the IDE share one managed root set.
10
+ // https://docs.trae.cn/cli_skills
11
+ module.exports.manifest.integrations.skills = { global: ['.trae/skills', '.traecli/skills'], project: ['.trae/skills', '.traecli/skills', '.agents/skills'] };
@@ -5,3 +5,6 @@ module.exports = nativeAcp({
5
5
  id: 'zcode', name: 'ZCode', args: [],
6
6
  capabilities: { questions: false, thinkingLevels: false, usage: true, contextUsage: true, attachments: false, fork: false, compaction: false },
7
7
  });
8
+ // ZCode scans, per scope: .zcode/skills then .agents/skills (deeper workspace levels win).
9
+ // https://zcode.z.ai/en/docs/skill
10
+ module.exports.manifest.integrations.skills = { global: ['.zcode/skills', '.agents/skills'], project: ['.zcode/skills', '.agents/skills'] };
@@ -8,6 +8,22 @@ const { createWorkspace, reviewWorkspace, applyWorkspace, discardWorkspace, push
8
8
  const validators = new Map(tools.map(tool => [tool.name, z.fromJSONSchema(tool.inputSchema)]));
9
9
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
10
10
 
11
+ function defaultWorkerPermissionMode(agent) {
12
+ switch (agent) {
13
+ case 'claude':
14
+ case 'claude-code':
15
+ return 'bypassPermissions';
16
+ case 'antigravity':
17
+ case 'agy':
18
+ return 'skip';
19
+ case 'pi':
20
+ case 'omp':
21
+ return 'no-approve';
22
+ default:
23
+ return undefined;
24
+ }
25
+ }
26
+
11
27
  // A session-scoped local bridge. Native models/credentials and approvals stay in adapters.
12
28
  class Collaboration {
13
29
  constructor(runtime) {
@@ -49,6 +65,7 @@ class Collaboration {
49
65
  if (job.appliedDigest) throw new Error('此任务已应用;后续修改请创建新任务');
50
66
  const parent = this.runtime.threads.find(t => t.id === job.owner);
51
67
  if (!parent || this.runtime.threads.some(t => (this.runtime.execution.isRunning(t.id) || t.reviewPending) && [parent.cwd, job.workspace?.cwd].includes(t.cwd))) throw new Error('请等待主任务和工作区任务结算后再应用');
68
+ if (job.childId) this.runtime.verificationGates.assertSatisfied(this.runtime.getThread(job.childId), '应用子任务改动');
52
69
  job.applying = true;
53
70
  try {
54
71
  const result = await applyWorkspace(job.workspace, digest);
@@ -73,6 +90,7 @@ class Collaboration {
73
90
  const job = this.jobs.get(id);
74
91
  if (!job) throw new Error('子任务不存在');
75
92
  if (job.status === 'running') throw new Error('请等待子任务完成后再推送分支');
93
+ if (job.childId) this.runtime.verificationGates.assertSatisfied(this.runtime.getThread(job.childId), '推送子任务分支');
76
94
  return pushWorkspace(job.workspace, remote, branch);
77
95
  }
78
96
 
@@ -134,6 +152,13 @@ class Collaboration {
134
152
  if (name === 'delegate_to_agent') {
135
153
  const agent = rt.resolveHarnessId(args.agent_type);
136
154
  if (!agent || !rt.status[agent]?.available) throw new Error('Target Harness unavailable');
155
+ // Server-side enforcement: multi-agent collaboration can ONLY start when the user explicitly selected/mentioned agents.
156
+ if (!parent.activeMentions || !parent.activeMentions.length) {
157
+ throw new Error('跨 Harness 协作仅在用户显式使用 #agent 或 @agent(如 #pi、#claude)指定时允许启动。用户本轮未显式委派,不能由大模型自行决定启动跨 Harness 协作。请直接使用当前 Harness 的原生工具完成任务。');
158
+ }
159
+ if (!parent.activeMentions.includes(agent)) {
160
+ throw new Error(`用户仅显式指定了 [${parent.activeMentions.join(', ')}],不能委派给未指定的 "${agent}"。请向用户确认是否需要委派给其他 Harness。`);
161
+ }
137
162
  const jobs = [...this.jobs.values()].filter(j => j.owner === owner);
138
163
  if (jobs.filter(j => j.status === 'running').length >= 4) throw new Error('At most four concurrent subtasks; collect existing results first');
139
164
  if (jobs.filter(j => j.turnId === rt.execution.lastTurn(owner)?.id).length >= 16) throw new Error('At most sixteen subtasks per lead turn');
@@ -146,7 +171,7 @@ class Collaboration {
146
171
  if (name === 'get_delegation_status') {
147
172
  const jobs = args.task_ids.map(id => this.owned(owner, id));
148
173
  const until = Date.now() + (args.wait_ms ?? 0);
149
- while (jobs.every(j => j.status === 'running') && Date.now() < until && !this.closing) await delay(Math.min(100, until - Date.now()));
174
+ while (jobs.every(j => j.status === 'running') && Date.now() < until && !this.closing && !this.cancelling.has(owner) && rt.execution.isRunning(owner)) await delay(Math.min(100, until - Date.now()));
150
175
  return jobs.map(j => this.view(j));
151
176
  }
152
177
  const job = this.owned(owner, args.task_id);
@@ -184,9 +209,12 @@ class Collaboration {
184
209
  job.workspace = await createWorkspace(parent.cwd, job.id, job.isolation);
185
210
  await this.save();
186
211
  }
187
- if (job.status !== 'running' || this.closing) return;
188
- const child = job.childId ? rt.threads.find(t => t.id === job.childId) : await rt.createThread({ harnessId: job.agent, cwd: job.workspace.cwd, title: `${parent.title} › ${task.slice(0, 40)}`, parentThreadId: parent.id,
189
- onCreated: async thread => { job.childId = thread.id; await this.save(); } });
212
+ const workerPermMode = defaultWorkerPermissionMode(job.agent);
213
+ const child = job.childId ? rt.threads.find(t => t.id === job.childId) : await rt.createThread({
214
+ harnessId: job.agent, cwd: job.workspace.cwd, title: `${parent.title} ${task.slice(0, 40)}`, parentThreadId: parent.id,
215
+ options: { ...(workerPermMode ? { permissionMode: workerPermMode } : {}) },
216
+ onCreated: async thread => { job.childId = thread.id; await this.save(); }
217
+ });
190
218
  if (!child) throw new Error('Native child history is missing; no replacement session was created');
191
219
  job.childId = child.id;
192
220
  await this.save();
@@ -198,7 +226,15 @@ class Collaboration {
198
226
  void sending.then(() => { sendDone = true; }, error => { sendDone = true; sendError = error; });
199
227
  const until = Date.now() + 30 * 60 * 1000;
200
228
  let displayedStatus = 'running';
201
- while (job.status === 'running' && (!sendDone || rt.execution.isRunning(child.id) || child.reviewPending)) {
229
+ let turnInactiveSince = null;
230
+ while (job.status === 'running' && !this.closing && !this.cancelling.has(parent.id) && rt.execution.isRunning(parent.id)) {
231
+ const childRunning = rt.execution.isRunning(child.id) || child.reviewPending;
232
+ if (!childRunning) {
233
+ if (!turnInactiveSince) turnInactiveSince = Date.now();
234
+ if (sendDone || Date.now() - turnInactiveSince > 2000) break;
235
+ } else {
236
+ turnInactiveSince = null;
237
+ }
202
238
  const current = this.view(job).display_status;
203
239
  if (current !== displayedStatus) { displayedStatus = current; emit({ kind: 'tool', toolCallId, state: 'running', output: JSON.stringify(this.view(job)) }); }
204
240
  if (Date.now() > until) { await rt.cancel(child.id); throw new Error('Subtask timed out after 30 minutes'); }
@@ -227,16 +263,26 @@ class Collaboration {
227
263
  if (job.status !== 'running') return;
228
264
  job.status = this.closing ? 'interrupted' : 'cancelled';
229
265
  job.cancelling = true;
230
- try { if (job.childId) await this.runtime.cancel(job.childId); }
231
- finally { job.cancelling = false; await this.save(); }
266
+ try {
267
+ if (job.childId) {
268
+ await Promise.race([
269
+ this.runtime.cancel(job.childId),
270
+ new Promise(r => setTimeout(r, 3_000)),
271
+ ]).catch(() => {});
272
+ }
273
+ } finally { job.cancelling = false; await this.save(); }
232
274
  }
233
275
 
234
276
  async cancelOwner(owner) {
235
277
  const jobs = [...this.jobs.values()].filter(j => j.owner === owner && j.status === 'running');
236
278
  if (!jobs.length) return;
237
279
  this.cancelling.add(owner);
238
- try { await Promise.all(jobs.map(j => this.cancel(j))); }
239
- finally { this.cancelling.delete(owner); }
280
+ try {
281
+ await Promise.race([
282
+ Promise.all(jobs.map(j => this.cancel(j))),
283
+ new Promise(r => setTimeout(r, 5_000)),
284
+ ]).catch(() => {});
285
+ } finally { this.cancelling.delete(owner); }
240
286
  }
241
287
  isParticipant(thread, owner) { return thread.id === owner || [...this.jobs.values()].some(j => j.owner === owner && j.childId === thread.id); }
242
288
  async close() {
@@ -254,11 +300,13 @@ function mentionedAgents(text, runtime) {
254
300
  // Ignore code and email/package addresses; explicit links survive draft copy/paste.
255
301
  const prose = text.replace(/```[\s\S]*?```|`[^`\n]*`/g, '');
256
302
  const ids = new Set();
257
- for (const match of prose.matchAll(/\[[^\]\n]+\]\(harness-mix:\/\/agent\/([\w-]+)\)|(?:^|[\s,。;:])@([\w-]+)(?=$|[\s,。;:])/g)) {
303
+ // CJK ideographs (\u4e00-\u9fff) and fullwidth/halfwidth forms are valid word boundaries,
304
+ // so #agent works in Chinese prose (for example 帮我#pi做这个). @ remains native Codex syntax.
305
+ for (const match of prose.matchAll(/\[[^\]\n]+\]\(harness-mix:\/\/agent\/([\w-]+)\)|(?:^|[\s\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff,。;:、!?""''()【】])#([\w-]+)(?=$|[\s\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff,。;:、!?""''()【】])/g)) {
258
306
  const id = runtime.resolveHarnessId(match[1] || match[2]);
259
307
  if (id) ids.add(id);
260
308
  }
261
309
  return [...ids];
262
310
  }
263
311
 
264
- module.exports = { Collaboration, mentionedAgents };
312
+ module.exports = { Collaboration, mentionedAgents, defaultWorkerPermissionMode };
@@ -17,6 +17,9 @@ class CoreSession {
17
17
  const turns = this.core.turns.turnsForThread(thread.id);
18
18
  if (turns.length) this.lastTurns.set(thread.id, turns.at(-1).id);
19
19
  this.sync(thread);
20
+ // The authoritative state now lives in ProtocolCore. Retaining the loaded
21
+ // checkpoint would keep a second complete copy of every item in memory.
22
+ delete thread.coreState;
20
23
  }
21
24
  lastTurn(id) { return this.core.getTurn(this.lastTurns.get(id)); }
22
25
  isRunning(id) { return Boolean(active(this.lastTurn(id))); }
@@ -73,7 +76,10 @@ class CoreSession {
73
76
  delete message.items;
74
77
  thread.tools.push(...items.filter(i => i.type === 'tool_call').map(i => ({ ...i, id: i.nativeRef?.toolCallId ?? i.id, messageId: message.id, at: i.createdAt, endedAt: i.updatedAt })));
75
78
  }
76
- thread.coreState = { version: 1, thread: structuredClone(coreThread), turns: structuredClone(this.core.turns.turnsForThread(thread.id)), items: structuredClone([...this.core.projector.items.values()].filter(i => i.threadId === thread.id)) };
79
+ }
80
+ checkpoint(thread) {
81
+ const coreThread = this.core.getThread(thread.id);
82
+ return coreThread ? { version: 1, thread: structuredClone(coreThread), turns: structuredClone(this.core.turns.turnsForThread(thread.id)), items: structuredClone([...this.core.projector.items.values()].filter(i => i.threadId === thread.id)) } : null;
77
83
  }
78
84
  snapshot() { return this.core.snapshot(); }
79
85
  }
@@ -42,6 +42,7 @@ function conversationSnapshot(thread) {
42
42
  }
43
43
 
44
44
  function evidenceKind(item) {
45
+ if (item.type === 'verification_report') return 'verification';
45
46
  const title = String(item.title || '').toLowerCase();
46
47
  if (/test|vitest|jest|pytest|测试/.test(title)) return 'test';
47
48
  if (/build|compile|tsc|构建|编译/.test(title)) return 'build';
@@ -51,7 +52,13 @@ function evidenceKind(item) {
51
52
 
52
53
  function evidenceSnapshot(thread) {
53
54
  const items = (thread.messages || []).flatMap(message => message.coreItems || []);
54
- return items.filter(item => item.type === 'tool_call' && item.state !== 'running').slice(-LIMITS.evidence).map(item => {
55
+ return items.filter(item => (item.type === 'tool_call' && item.state !== 'running') || item.type === 'verification_report').slice(-LIMITS.evidence).map(item => {
56
+ if (item.type === 'verification_report') {
57
+ const report = redact(item.report ?? {});
58
+ const content = { kind: 'verification', title: 'Verification gate', state: report.status ?? 'unknown', inputExcerpt: report.mode ?? '', outputExcerpt: JSON.stringify(report).slice(0, LIMITS.excerpt), createdAt: item.updatedAt };
59
+ const contentDigest = digest(content);
60
+ return { evidenceId: `evidence_${contentDigest.slice(0, 24)}`, ...content, contentDigest };
61
+ }
55
62
  const content = {
56
63
  kind: evidenceKind(item),
57
64
  sourceHarnessId: thread.harnessId,