@harness-mix/cli 0.1.5 → 0.1.6

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 (60) hide show
  1. package/README.md +1 -1
  2. package/docs/harness-management.md +4 -4
  3. package/docs/multi-agent-collaboration.md +9 -9
  4. package/output/native-build/renderer-extension.js +1959 -435
  5. package/package.json +9 -8
  6. package/scripts/collaboration-test.cjs +8 -3
  7. package/scripts/collaboration-ui-smoke.cjs +40 -26
  8. package/scripts/integrations-test.cjs +63 -4
  9. package/scripts/integrations-ui-smoke.cjs +77 -16
  10. package/scripts/native-protocol-test.cjs +102 -1
  11. package/scripts/native-sidebar-test.cjs +7 -0
  12. package/scripts/native-skill-roots-test.cjs +90 -0
  13. package/scripts/test-live-mentions.cjs +152 -0
  14. package/src/main/adapters/antigravity.js +20 -17
  15. package/src/main/adapters/claude.js +41 -8
  16. package/src/main/adapters/codebuddy.js +1 -0
  17. package/src/main/adapters/codex.js +20 -2
  18. package/src/main/adapters/cursor.js +5 -1
  19. package/src/main/adapters/dsh.js +8 -1
  20. package/src/main/adapters/grok.js +4 -1
  21. package/src/main/adapters/hermes.js +8 -0
  22. package/src/main/adapters/kiro.js +2 -1
  23. package/src/main/adapters/managed-mcp.js +15 -2
  24. package/src/main/adapters/omp.js +11 -0
  25. package/src/main/adapters/openclaw.js +6 -0
  26. package/src/main/adapters/opencode.js +5 -1
  27. package/src/main/adapters/pi.js +5 -1
  28. package/src/main/adapters/qoder.js +1 -0
  29. package/src/main/adapters/trae.js +4 -0
  30. package/src/main/adapters/zcode.js +3 -0
  31. package/src/main/host/collaboration.js +57 -11
  32. package/src/main/host/integrations.js +164 -26
  33. package/src/main/host/runtime.js +126 -86
  34. package/src/main/native/protocol.js +189 -78
  35. package/src/main/native/thread-list.js +3 -0
  36. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  37. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  38. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -1
  39. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
  40. package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
  41. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +29 -4
  42. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -1
  43. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +2 -1
  44. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -1
  45. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  46. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +1 -1
  47. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  48. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -2
  50. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +149 -171
  51. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +5 -1
  52. package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
  53. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +36 -3
  54. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1434 -62
  55. package/src/native-ui/renderer-extension/src/settings/localization.ts +4 -2
  56. package/src/native-ui/renderer-extension/src/settings/pages.ts +5 -3
  57. package/src/native-ui/renderer-extension/src/settings/shell.css +96 -6
  58. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +4 -1
  59. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  60. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +5 -2
@@ -0,0 +1,90 @@
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 { ParityObserver } = require('./support/parity-observer.cjs');
6
+ const { HostRuntime } = require('../src/main/host/runtime');
7
+
8
+ // 用户可见契约:打开原生会话时,先为每个 Harness 创建缺失的原生技能根目录,
9
+ // 这样刚安装、从未运行过的 Harness 也能立即被 Harness Mix 发现并安装技能。
10
+ // 单个目录创建失败只提示、不阻断会话;项目作用域不可用时退化为全局根目录。
11
+
12
+ const skills = { global: ['.test-harness/skills'], project: ['.agents/skills'] };
13
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
14
+ async function until(fn) { for (let i = 0; i < 200; i++) { if (fn()) return; await sleep(10); } throw new Error('timed out'); }
15
+
16
+ function stubAdapter(id, onOpen) {
17
+ return {
18
+ manifest: { id, name: 'Test Harness', capabilities: {}, integrations: { mcp: false, skills: JSON.parse(JSON.stringify(skills)) } },
19
+ async open(input) { onOpen?.(input); return {}; },
20
+ async send() {}, async cancel() {}, async respond() {}, async close() {},
21
+ };
22
+ }
23
+
24
+ async function main() {
25
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'hm-skill-roots-'));
26
+ const workspace = path.join(directory, 'workspace');
27
+ const home = path.join(directory, 'home');
28
+ await fs.mkdir(workspace); await fs.mkdir(home);
29
+
30
+ const rt = new HostRuntime({ observer: new ParityObserver(), dataDirectory: directory });
31
+ await rt.store.load();
32
+ rt.integrations.home = home;
33
+ rt.integrations.environment = {};
34
+
35
+ // 1) 缺失的技能目录在会话打开时被创建(全局 + 项目)。
36
+ let opened = 0, emit = null;
37
+ const adapter = stubAdapter('test-harness', input => { opened++; emit = input.emit; });
38
+ rt.adapters.set(adapter.manifest.id, adapter);
39
+ rt.status[adapter.manifest.id] = { available: true };
40
+ const thread = await rt.createThread({ harnessId: adapter.manifest.id, cwd: workspace });
41
+ const toasts = [];
42
+ rt.subscribe(event => { if (event.type === 'toast') toasts.push(event); });
43
+ await rt.send(thread.id, 'hello');
44
+ assert.equal(opened, 1, 'native session opened');
45
+ for (const root of [path.join(home, '.test-harness', 'skills'), path.join(workspace, '.agents', 'skills')]) {
46
+ assert.equal((await fs.stat(root)).isDirectory(), true, `${root} created on launch`);
47
+ }
48
+ assert.deepEqual(toasts, [], 'a successful launch reports no skill-root failure');
49
+ emit({ kind: 'completed', finalAnswer: true });
50
+ await until(() => thread.status === 'ready' && !thread.reviewPending);
51
+
52
+ // 2) 已存在的根目录被复用,重复打开不会失败。
53
+ await fs.mkdir(path.join(home, '.test-harness', 'skills', 'existing'), { recursive: true });
54
+ await rt.send(thread.id, 'again');
55
+ assert.equal(opened, 1, 'same thread reuses the native session');
56
+ emit({ kind: 'completed', finalAnswer: true });
57
+ await until(() => thread.status === 'ready' && !thread.reviewPending);
58
+ assert.equal((await fs.stat(path.join(home, '.test-harness', 'skills', 'existing'))).isDirectory(), true, 'existing skill content untouched');
59
+
60
+ // 3) 无法创建的根目录只提示并跳过,会话照常打开。
61
+ const blockedHome = path.join(directory, 'blocked-home');
62
+ await fs.mkdir(path.join(blockedHome, '.test-harness'), { recursive: true });
63
+ await fs.writeFile(path.join(blockedHome, '.test-harness', 'skills'), 'not a directory');
64
+ const blockedWorkspace = path.join(directory, 'blocked-workspace');
65
+ await fs.mkdir(blockedWorkspace);
66
+ const blocked = new HostRuntime({ observer: new ParityObserver(), dataDirectory: path.join(directory, 'blocked-data') });
67
+ await blocked.store.load();
68
+ blocked.integrations.home = blockedHome;
69
+ blocked.integrations.environment = {};
70
+ let blockedOpened = 0;
71
+ const blockedAdapter = stubAdapter('test-harness', () => { blockedOpened++; });
72
+ blocked.adapters.set(blockedAdapter.manifest.id, blockedAdapter);
73
+ blocked.status[blockedAdapter.manifest.id] = { available: true };
74
+ const blockedThread = await blocked.createThread({ harnessId: blockedAdapter.manifest.id, cwd: blockedWorkspace });
75
+ const blockedToasts = [];
76
+ console.log('DBG blocked.integrations.home =', blocked.integrations.home);
77
+ console.log('DBG global root stat type =', (await fs.lstat(path.join(blockedHome, '.test-harness', 'skills'))).isFile() ? 'FILE' : 'other');
78
+ blocked.subscribe(event => { if (event.type === 'toast') blockedToasts.push(event); });
79
+ await blocked.send(blockedThread.id, 'hello');
80
+ assert.equal(blockedOpened, 1, 'session still opens when a skill root cannot be created');
81
+ assert.equal(blocked.sessions.has(blockedThread.id), true, 'native session attached despite the uncreatable root');
82
+ assert.equal(blockedThread.error, undefined, 'no session error from the uncreatable root');
83
+ assert.equal((await fs.stat(path.join(blockedWorkspace, '.agents', 'skills'))).isDirectory(), true, 'the remaining root is still created');
84
+ console.log('TOASTS:', JSON.stringify(blockedToasts.map(t => ({ type: t.type, level: t.level, text: String(t.text).slice(0, 140) }))));
85
+ assert.equal(blockedToasts.filter(event => /原生技能目录准备失败/.test(event.text)).length, 1, 'the skipped root is reported once');
86
+
87
+ console.log('native skill roots: created on session open, reused when present, and never fatal PASS');
88
+ await fs.rm(directory, { recursive: true, force: true });
89
+ }
90
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,152 @@
1
+ const http = require('node:http');
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+
5
+ function getJson(url) {
6
+ return new Promise((resolve, reject) => {
7
+ http.get(url, res => {
8
+ let data = '';
9
+ res.on('data', c => (data += c));
10
+ res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } });
11
+ }).on('error', reject);
12
+ });
13
+ }
14
+
15
+ async function main() {
16
+ const port = process.argv[2] || '57989';
17
+ const targets = await getJson(`http://127.0.0.1:${port}/json/list`);
18
+ const page = targets.find(t => t.type === 'page' && t.url === 'app://-/index.html');
19
+ if (!page) throw new Error('main page not found');
20
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
21
+ await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
22
+
23
+ let id = 0;
24
+ const pending = new Map();
25
+ ws.onmessage = ev => {
26
+ const msg = JSON.parse(ev.data);
27
+ if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); }
28
+ };
29
+ const send = (method, params) => new Promise(res => {
30
+ const i = ++id;
31
+ pending.set(i, res);
32
+ ws.send(JSON.stringify({ id: i, method, params }));
33
+ });
34
+
35
+ const evaluate = async expr => {
36
+ const result = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
37
+ if (result.result?.exceptionDetails) {
38
+ throw new Error(JSON.stringify(result.result.exceptionDetails));
39
+ }
40
+ return result.result?.result?.value;
41
+ };
42
+
43
+ console.log('Waiting for probe to mount...');
44
+ await evaluate(`(async () => {
45
+ for (let i = 0; i < 50; i++) {
46
+ if (window.__codexhostRendererBindingProbeV1 && document.querySelector('.ProseMirror')) break;
47
+ await new Promise(r => setTimeout(r, 100));
48
+ }
49
+ })()`);
50
+
51
+ const directClientTest = await evaluate(`(async () => {
52
+ try {
53
+ const probe = window.__codexhostRendererBindingProbeV1;
54
+ return {
55
+ hasProbe: !!probe,
56
+ probeKeys: probe ? Object.keys(probe) : []
57
+ };
58
+ } catch (e) {
59
+ return { error: e.message, stack: e.stack };
60
+ }
61
+ })()`);
62
+ console.log('Direct client test:', JSON.stringify(directClientTest, null, 2));
63
+
64
+ console.log('Focusing editor and positioning caret at end...');
65
+ await evaluate(`(() => {
66
+ const editor = document.querySelector('.ProseMirror');
67
+ if (!editor) return;
68
+ editor.focus();
69
+ const p = editor.querySelector('p') || editor;
70
+ const sel = window.getSelection();
71
+ const range = document.createRange();
72
+ range.selectNodeContents(p);
73
+ range.collapse(false);
74
+ sel?.removeAllRanges();
75
+ sel?.addRange(range);
76
+ })()`);
77
+
78
+ // Send input via CDP
79
+ console.log('Sending CDP input for #...');
80
+ await send('Input.insertText', { text: '#' });
81
+ await new Promise(r => setTimeout(r, 600));
82
+
83
+ const menuInfo = await evaluate(`(() => {
84
+ const menu = document.querySelector('[data-harness-mix-mentions]');
85
+ const tabs = [...document.querySelectorAll('[data-harness-mix-mentions] [role="tab"]')].map(t => t.textContent.trim());
86
+ const options = [...document.querySelectorAll('[data-harness-mix-mentions] [role="option"]')].map(opt => {
87
+ const name = opt.querySelector('span[style*="font-weight:600"]')?.textContent || opt.innerText;
88
+ const desc = opt.querySelector('span[style*="font-size:11px"]')?.textContent || '';
89
+ return { name, desc, disabled: opt.hasAttribute('disabled') };
90
+ });
91
+ return {
92
+ lastMentionDebug: window.__lastMentionDebug,
93
+ menuHidden: menu ? menu.hidden : true,
94
+ menuDisplay: menu ? menu.style.display : null,
95
+ menuRect: menu ? { left: menu.style.left, top: menu.style.top, bottom: menu.style.bottom } : null,
96
+ tabs,
97
+ optionCount: options.length,
98
+ sampleOptions: options.slice(0, 8)
99
+ };
100
+ })()`);
101
+ console.log('Menu state after typing #:', JSON.stringify(menuInfo, null, 2));
102
+
103
+ // Capture screenshot with popup open
104
+ const shot1 = await send('Page.captureScreenshot', { format: 'png' });
105
+ const data1 = shot1.result?.data || shot1.data;
106
+ if (data1) {
107
+ await fs.mkdir('output', { recursive: true });
108
+ await fs.writeFile(path.resolve('output/live-hash-mentions.png'), Buffer.from(data1, 'base64'));
109
+ console.log('Saved output/live-hash-mentions.png');
110
+ }
111
+
112
+ // 2. Select an agent (e.g. click first option) and verify badge styling
113
+ const state2 = await evaluate(`(async () => {
114
+ const firstOption = document.querySelector('[data-harness-mix-mentions] [role="option"]:not([disabled])');
115
+ if (firstOption) {
116
+ firstOption.click();
117
+ await new Promise(r => setTimeout(r, 200));
118
+ }
119
+ const badges = [...document.querySelectorAll('[data-harness-mix-selected-mentions] [data-harness-mix-mention-badge]')].map(b => ({
120
+ text: b.textContent.trim(),
121
+ title: b.title,
122
+ color: b.style.color
123
+ }));
124
+ const containerHasAttr = document.querySelector('[data-harness-mix-has-mentions="true"]') !== null;
125
+ return { badges, containerHasAttr };
126
+ })()`);
127
+ console.log('State after selecting agent:', JSON.stringify(state2, null, 2));
128
+
129
+ // Capture screenshot with badge in input box
130
+ const shot2 = await send('Page.captureScreenshot', { format: 'png' });
131
+ const data2 = shot2.result?.data || shot2.data;
132
+ if (data2) {
133
+ await fs.writeFile(path.resolve('output/live-hash-selected-badge.png'), Buffer.from(data2, 'base64'));
134
+ console.log('Saved output/live-hash-selected-badge.png');
135
+ }
136
+
137
+ // 3. Clear badge so we leave the UI clean
138
+ await evaluate(`(() => {
139
+ const badge = document.querySelector('[data-harness-mix-selected-mentions] [data-harness-mix-mention-badge]');
140
+ if (badge) badge.click();
141
+ const editor = document.querySelector('[contenteditable="true"], .ProseMirror, textarea');
142
+ if (editor && !editor.value) editor.textContent = '';
143
+ })()`);
144
+
145
+ ws.close();
146
+ console.log('Done!');
147
+ }
148
+
149
+ main().catch(err => {
150
+ console.error(err);
151
+ process.exit(1);
152
+ });
@@ -639,21 +639,21 @@ class QuestionBridge {
639
639
  CODEXHOST_AGY_QUESTION_CLIENT: `"${clientPath.replaceAll('\\', '/')}"`,
640
640
  };
641
641
 
642
- if (options.collaboration || options.managedMcp?.length) {
642
+ if (options.collaboration || options.managedMcp?.length) {
643
643
  const pluginDir = path.join(dir, '.agents', 'plugins', 'harness-mix');
644
644
  const plainPluginDir = path.join(dir, 'plugins', 'harness-mix');
645
645
  await fs.promises.mkdir(pluginDir, { recursive: true });
646
646
  await fs.promises.mkdir(plainPluginDir, { recursive: true });
647
647
  const pluginManifest = JSON.stringify({ name: 'harness-mix-collaboration', description: 'Harness Mix Multi-agent Collaboration' }, null, 2);
648
648
  const mcpConfig = JSON.stringify({
649
- mcpServers: require('./managed-mcp').namedServers(options.managedMcp, options.collaboration),
649
+ mcpServers: require('./managed-mcp').namedServers(options.managedMcp, options.collaboration),
650
650
  }, null, 2);
651
651
  await fs.promises.writeFile(path.join(pluginDir, 'plugin.json'), pluginManifest, 'utf8');
652
652
  await fs.promises.writeFile(path.join(pluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
653
653
  await fs.promises.writeFile(path.join(plainPluginDir, 'plugin.json'), pluginManifest, 'utf8');
654
654
  await fs.promises.writeFile(path.join(plainPluginDir, 'mcp_config.json'), mcpConfig, 'utf8');
655
655
  await fs.promises.writeFile(path.join(dir, '.agents', 'mcp_config.json'), mcpConfig, 'utf8');
656
- Object.assign(bridge.environment, options.collaboration?.env || {});
656
+ Object.assign(bridge.environment, options.collaboration?.env || {});
657
657
  }
658
658
 
659
659
  return bridge;
@@ -817,7 +817,7 @@ function create(emit, options = {}) {
817
817
  });
818
818
  },
819
819
 
820
- async open({ thread, emit: emitEvent, diagnostic, collaboration, managedMcp = [] }) {
820
+ async open({ thread, emit: emitEvent, diagnostic, collaboration, managedMcp = [] }) {
821
821
  // Do not start a second agy process while a real turn may be starting.
822
822
  // Quota is loaded only by the explicit account/usage refresh path below.
823
823
  await restoreLegacyClipboardAttachments(thread);
@@ -830,8 +830,8 @@ function create(emit, options = {}) {
830
830
  model,
831
831
  thinkingLevel,
832
832
  permissionMode,
833
- collaboration: collaboration || null,
834
- managedMcp,
833
+ collaboration: collaboration || null,
834
+ managedMcp,
835
835
  collaborationEnabled: !!collaboration,
836
836
  activeTurn: null,
837
837
  bridge: null,
@@ -853,15 +853,12 @@ function create(emit, options = {}) {
853
853
  const bridge = await QuestionBridge.create({
854
854
  approvals,
855
855
  emit: emitEvent,
856
- collaboration: session.collaboration,
857
- managedMcp: session.managedMcp,
856
+ collaboration: session.collaboration,
857
+ managedMcp: session.managedMcp,
858
858
  });
859
859
  session.bridge = bridge;
860
860
 
861
861
  let effectivePrompt = prompt ?? '';
862
- if (session.collaborationEnabled) {
863
- effectivePrompt = `[多 Agent 协作系统已就绪。你作为主协调者,可调用已注入的 MCP 工具:delegate_to_agent(agent_type, task) 向其他已注册的 Harness(如 @pi、@claude-code、@codex、@deepseek-harness 等)委派子任务,使用 get_delegation_status(task_ids) 等待结果,使用 update_agent_plan(steps) 设定与更新分步计划。]\n\n${effectivePrompt}`;
864
- }
865
862
  const { imageEntries, extraDirs } = await prepareImageAttachments(attachments?.images, session.cwd);
866
863
  if (imageEntries.length) {
867
864
  const imageNotice = [
@@ -1211,15 +1208,18 @@ function create(emit, options = {}) {
1211
1208
  },
1212
1209
 
1213
1210
  async cancel(session) {
1214
- if (session.activeTurn?.child) {
1215
- void terminateTree(session.activeTurn.child.pid);
1211
+ if (session.activeTurn) {
1212
+ session.activeTurn.completed = true;
1213
+ if (session.activeTurn.child) {
1214
+ void terminateTree(session.activeTurn.child.pid);
1215
+ }
1216
+ session.activeTurn.resolve?.();
1217
+ session.activeTurn = null;
1216
1218
  }
1217
1219
  if (session.bridge) {
1218
1220
  await session.bridge.dispose().catch(() => {});
1219
1221
  session.bridge = null;
1220
1222
  }
1221
- session.activeTurn?.resolve?.();
1222
- session.activeTurn = null;
1223
1223
  },
1224
1224
 
1225
1225
  async respond(session, requestId, response) {
@@ -1345,8 +1345,11 @@ function create(emit, options = {}) {
1345
1345
  };
1346
1346
  }
1347
1347
 
1348
- manifest.integrations = { mcp: true };
1349
- module.exports = {
1348
+ // https://antigravity.google/docs/skills/#where-skills-live
1349
+ // Single assignment: an earlier declaration used to be silently overwritten here,
1350
+ // which dropped Antigravity's native skill roots from the manifest.
1351
+ manifest.integrations = { mcp: true, skills: { global: ['.gemini/config/skills'], project: ['.agents/skills'] } };
1352
+ module.exports = {
1350
1353
  manifest,
1351
1354
  create,
1352
1355
  parseModelsOutput,
@@ -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'] };