@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.
- package/README.md +86 -56
- package/docs/harness-management.md +4 -4
- package/docs/multi-agent-collaboration.md +9 -9
- package/docs/storage-verification-design.md +49 -0
- package/output/native-build/renderer-extension.js +4210 -2273
- package/package.json +11 -9
- package/scripts/codebuddy-migration-test.cjs +1 -1
- package/scripts/collaboration-test.cjs +8 -3
- package/scripts/collaboration-ui-smoke.cjs +40 -26
- package/scripts/core-runtime-test.cjs +9 -1
- package/scripts/integrations-test.cjs +63 -4
- package/scripts/integrations-ui-smoke.cjs +77 -16
- package/scripts/native-protocol-test.cjs +113 -2
- package/scripts/native-sidebar-test.cjs +7 -0
- package/scripts/native-skill-roots-test.cjs +90 -0
- package/scripts/native-ui-smoke.cjs +23 -4
- package/scripts/projector-test.cjs +13 -0
- package/scripts/storage-verification-test.cjs +78 -0
- package/scripts/switch-harness-test.cjs +1 -1
- package/scripts/test-live-mentions.cjs +152 -0
- package/src/main/adapters/antigravity.js +20 -17
- package/src/main/adapters/claude.js +41 -8
- package/src/main/adapters/codebuddy.js +1 -0
- package/src/main/adapters/codex.js +20 -2
- package/src/main/adapters/cursor.js +5 -1
- package/src/main/adapters/dsh.js +8 -1
- package/src/main/adapters/grok.js +4 -1
- package/src/main/adapters/hermes.js +8 -0
- package/src/main/adapters/kiro.js +2 -1
- package/src/main/adapters/managed-mcp.js +15 -2
- package/src/main/adapters/omp.js +11 -0
- package/src/main/adapters/openclaw.js +6 -0
- package/src/main/adapters/opencode.js +5 -1
- package/src/main/adapters/pi.js +5 -1
- package/src/main/adapters/qoder.js +1 -0
- package/src/main/adapters/trae.js +4 -0
- package/src/main/adapters/zcode.js +3 -0
- package/src/main/host/collaboration.js +59 -11
- package/src/main/host/core-session.js +7 -1
- package/src/main/host/handoff-checkpoints.js +8 -1
- package/src/main/host/integrations.js +164 -26
- package/src/main/host/runtime.js +270 -116
- package/src/main/host/store.js +32 -11
- package/src/main/host/thread-storage.js +73 -0
- package/src/main/host/thread-store.js +123 -0
- package/src/main/host/verification-gates.js +118 -0
- package/src/main/native/protocol.js +209 -83
- package/src/main/native/thread-list.js +3 -0
- package/src/main/protocol-core/projector.js +20 -3
- package/src/main/shared-contracts/event.js +2 -1
- package/src/main/shared-contracts/item.js +1 -0
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts +2 -2
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
- 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-chatgpt-context.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts +11 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-mentions.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +29 -4
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +5 -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-settings-lifecycle.d.ts +2 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- 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/integrations-page.d.ts +2 -1
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.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/storage-page.d.ts +20 -0
- package/src/native-ui/renderer-extension/dist/types/settings/storage-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +8 -88
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +150 -168
- package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +5 -1
- package/src/native-ui/renderer-extension/src/renderer-harness-mentions.ts +556 -123
- package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +36 -3
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +4 -0
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +12 -0
- package/src/native-ui/renderer-extension/src/settings/icons.ts +6 -3
- package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +1434 -62
- package/src/native-ui/renderer-extension/src/settings/localization.ts +8 -4
- package/src/native-ui/renderer-extension/src/settings/pages.ts +17 -8
- package/src/native-ui/renderer-extension/src/settings/shell.css +103 -8
- package/src/native-ui/renderer-extension/src/settings/storage-page.ts +74 -0
- package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +4 -1
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +2 -0
- package/src/native-ui/renderer-extension/test/settings/harness-settings.test.ts +5 -0
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
- package/src/native-ui/renderer-extension/test/settings/pages.test.ts +50 -4
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +8 -2
|
@@ -21,6 +21,13 @@ assert.deepEqual(merged.data.map(t => t.id), ['official', 'external']);
|
|
|
21
21
|
assert.equal(merged.nextCursor, 'next');
|
|
22
22
|
assert.equal(mergeThreadPage(page, [thread], { cursor: 'next' }, t => t), page);
|
|
23
23
|
|
|
24
|
+
const archivedThread = { id: 'archived-ext', archived: true, createdAt: 15 };
|
|
25
|
+
const pageWithArchived = { data: [{ id: 'official', createdAt: 20 }, archivedThread], nextCursor: null };
|
|
26
|
+
const mergedWithoutArchived = mergeThreadPage(pageWithArchived, [archivedThread], { archived: false }, t => t);
|
|
27
|
+
assert.deepEqual(mergedWithoutArchived.data.map(t => t.id), ['official']);
|
|
28
|
+
const mergedWithArchived = mergeThreadPage({ data: [{ id: 'official-archived', archived: true, createdAt: 20 }, archivedThread], nextCursor: null }, [archivedThread], { archived: true }, t => t);
|
|
29
|
+
assert.deepEqual(mergedWithArchived.data.map(t => t.id), ['official-archived', 'archived-ext']);
|
|
30
|
+
|
|
24
31
|
const root = fs.mkdtempSync(path.resolve('output/native-runtime-cache-test-'));
|
|
25
32
|
const resources = path.join(root, 'resources');
|
|
26
33
|
const cache = path.join(root, 'cache');
|
|
@@ -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; });
|
|
@@ -12,13 +12,20 @@ app.whenReady().then(async () => {
|
|
|
12
12
|
const icons = getAllIconsDictionary();
|
|
13
13
|
icons.harnesses['claude-code'] = icons.harnesses.claude;
|
|
14
14
|
icons.harnesses['deepseek-harness'] = icons.harnesses.dsh;
|
|
15
|
-
const bundle = await esbuild.build({ stdin: { contents: `export { createRendererAgentIcon } from './src/native-ui/renderer-extension/src/renderer-agent-icon.ts'; export { mountRendererAgentPicker, renderRendererAgentPicker } from './src/native-ui/renderer-extension/src/renderer-agent-picker.ts'; export { mountRendererModelPicker, renderRendererModelPicker } from './src/native-ui/renderer-extension/src/renderer-model-picker.ts'; export { mountRendererHarnessHandoff } from './src/native-ui/renderer-extension/src/renderer-harness-handoff.ts'; export { restoredThreadOwnership } from './src/native-ui/renderer-extension/src/renderer-binding-probe.ts'; export { installRendererSidebarAgentIcons } from './src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts'; export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { aboutPage } from './src/native-ui/renderer-extension/src/settings/pages.ts'; export { rendererSettingsMessages } from './src/native-ui/renderer-extension/src/settings/localization.ts'; export { installRendererSettingsShell } from './src/native-ui/renderer-extension/src/settings/shell.ts';`, resolveDir: process.cwd() }, bundle: true, alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') }, platform: 'browser', format: 'iife', globalName: 'NativeUI', write: false, loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' }, logLevel: 'silent' });
|
|
15
|
+
const bundle = await esbuild.build({ stdin: { contents: `export { createRendererAgentIcon } from './src/native-ui/renderer-extension/src/renderer-agent-icon.ts'; export { mountRendererAgentPicker, renderRendererAgentPicker } from './src/native-ui/renderer-extension/src/renderer-agent-picker.ts'; export { mountRendererModelPicker, renderRendererModelPicker } from './src/native-ui/renderer-extension/src/renderer-model-picker.ts'; export { mountRendererHarnessHandoff } from './src/native-ui/renderer-extension/src/renderer-harness-handoff.ts'; export { restoredThreadOwnership } from './src/native-ui/renderer-extension/src/renderer-binding-probe.ts'; export { installRendererSidebarAgentIcons } from './src/native-ui/renderer-extension/src/renderer-sidebar-agent-icons.ts'; export { installRendererSettingsLifecycle } from './src/native-ui/renderer-extension/src/harness-mix-settings.ts'; export { createAccountsSettingsPage } from './src/native-ui/renderer-extension/src/settings/accounts-page.ts'; export { createStorageSettingsPage } from './src/native-ui/renderer-extension/src/settings/storage-page.ts'; export { aboutPage } from './src/native-ui/renderer-extension/src/settings/pages.ts'; export { rendererSettingsMessages } from './src/native-ui/renderer-extension/src/settings/localization.ts'; export { installRendererSettingsShell } from './src/native-ui/renderer-extension/src/settings/shell.ts';`, resolveDir: process.cwd() }, bundle: true, alias: { '@codexhost/shared-contracts': path.resolve('src/native-ui/shared-contracts/src/index.ts') }, platform: 'browser', format: 'iife', globalName: 'NativeUI', write: false, loader: { '.svg': 'dataurl', '.png': 'dataurl', '.css': 'text' }, logLevel: 'silent' });
|
|
16
16
|
const win = new BrowserWindow({ show: false, width: 900, height: 650, webPreferences: { contextIsolation: true, sandbox: true, offscreen: true, backgroundThrottling: false } });
|
|
17
17
|
await win.loadURL('data:text/html,<html><head><style>body{background:%2317191d;color:white;font:16px Arial;padding:40px}button{background:%23272a30;color:white;border:0;padding:12px}img{vertical-align:middle}main{display:flex;gap:25px;margin-bottom:40px}</style></head><body><h2>Harness Mix native components</h2><main></main></body></html>');
|
|
18
18
|
await win.webContents.executeJavaScript(`globalThis.__HARNESS_MIX_ICONS__=${JSON.stringify(icons)};globalThis.__HARNESS_MIX_MODEL_FAMILIES__=${JSON.stringify(MODEL_FAMILIES.map(f => ({ id: f.id, pattern: f.regex.source })))};${bundle.outputFiles[0].text}`);
|
|
19
|
+
const liveSettingsPages = await win.webContents.executeJavaScript(`(() => {
|
|
20
|
+
const lifecycle = NativeUI.installRendererSettingsLifecycle(window, {});
|
|
21
|
+
const pageIds = globalThis.__codexhostSettingsShellV1.registry.pages.map(page => page.id);
|
|
22
|
+
lifecycle.dispose();
|
|
23
|
+
return pageIds;
|
|
24
|
+
})()`);
|
|
25
|
+
assert.deepEqual(liveSettingsPages, ['connections', 'accounts', 'mcp', 'skills', 'session-import', 'storage', 'updates', 'about']);
|
|
19
26
|
const { NativeProtocol } = require('../src/main/native/protocol');
|
|
20
27
|
const nativeThreads = ['pi', 'claude', 'dsh', 'antigravity'].map((harnessId, index) => ({ id: 'sidebar-' + index, harnessId, model: { id: 'model-' + index, provider: 'native' }, options: {} }));
|
|
21
|
-
const protocol = new NativeProtocol({ threads: nativeThreads, subscribe: () => () => {}, core: { subscribe: () => () => {} }, describe: async () => ({}), getCapabilities: () => ({}) }, () => {});
|
|
28
|
+
const protocol = new NativeProtocol({ threads: nativeThreads, getThread: id => nativeThreads.find(thread => thread.id === id), subscribe: () => () => {}, core: { subscribe: () => () => {} }, describe: async () => ({}), getCapabilities: () => ({}) }, () => {});
|
|
22
29
|
const inspections = await Promise.all(nativeThreads.map(thread => protocol.request('codexhost/thread/inspect', { threadId: thread.id })));
|
|
23
30
|
await win.webContents.executeJavaScript(`globalThis.testInspections=${JSON.stringify(inspections)}`);
|
|
24
31
|
const result = await win.webContents.executeJavaScript(`(async () => {
|
|
@@ -126,6 +133,10 @@ app.whenReady().then(async () => {
|
|
|
126
133
|
};
|
|
127
134
|
const shell = NativeUI.installRendererSettingsShell([
|
|
128
135
|
NativeUI.createAccountsSettingsPage(messages, () => client),
|
|
136
|
+
NativeUI.createStorageSettingsPage(messages, () => ({
|
|
137
|
+
inspectStorage:async () => ({storageSchemaVersion:3,threadCount:12,loadedThreadCount:2,indexBytes:2048,recordBytes:5242880,recordCount:12,legacyBytes:0}),
|
|
138
|
+
optimizeStorage:async () => ({before:{storageSchemaVersion:3,threadCount:12,loadedThreadCount:2,indexBytes:2048,recordBytes:5242880,recordCount:12,legacyBytes:0},after:{storageSchemaVersion:3,threadCount:12,loadedThreadCount:2,indexBytes:2048,recordBytes:4194304,recordCount:12,legacyBytes:0}}),
|
|
139
|
+
})),
|
|
129
140
|
NativeUI.aboutPage(messages, () => ({ readCurrentVersion:async () => ({version:'0.1.4'}), checkUpdate:async () => { throw new Error('not expected'); } })),
|
|
130
141
|
], messages, document);
|
|
131
142
|
shell.openSettings(undefined, 'accounts');
|
|
@@ -135,8 +146,16 @@ app.whenReady().then(async () => {
|
|
|
135
146
|
})()`);
|
|
136
147
|
assert.match(settingsResult.text, /native.*example\.com/s);
|
|
137
148
|
assert.match(settingsResult.text, /Plus/);
|
|
138
|
-
assert.equal(settingsResult.addHidden,
|
|
149
|
+
assert.equal(settingsResult.addHidden, false, 'multi-account Add Account action remains visible');
|
|
139
150
|
fs.writeFileSync(path.join(out, 'settings-account.png'), (await win.webContents.capturePage()).toPNG());
|
|
151
|
+
const storageText = await win.webContents.executeJavaScript(`(async () => {
|
|
152
|
+
globalThis.__codexhostSettingsShellV1.openSettings(undefined, 'storage');
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
154
|
+
return globalThis.__codexhostSettingsShellV1.root.shadowRoot.textContent;
|
|
155
|
+
})()`);
|
|
156
|
+
assert.match(storageText, /Schema v3/);
|
|
157
|
+
assert.match(storageText, /12 个任务/);
|
|
158
|
+
fs.writeFileSync(path.join(out, 'settings-storage.png'), (await win.webContents.capturePage()).toPNG());
|
|
140
159
|
const aboutText = await win.webContents.executeJavaScript(`(async () => {
|
|
141
160
|
globalThis.__codexhostSettingsShellV1.openSettings(undefined, 'about');
|
|
142
161
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -145,6 +164,6 @@ app.whenReady().then(async () => {
|
|
|
145
164
|
assert.match(aboutText, /v0\.1\.4/);
|
|
146
165
|
fs.writeFileSync(path.join(out, 'settings-about.png'), (await win.webContents.capturePage()).toPNG());
|
|
147
166
|
win.destroy();
|
|
148
|
-
console.log('PASS: real Electron native picker, graphical Harness handoff, official Account/Usage, live About version and all project Harness/model SVGs load');
|
|
167
|
+
console.log('PASS: real Electron native picker, graphical Harness handoff, official Account/Usage, sharded Storage metrics, live About version and all project Harness/model SVGs load');
|
|
149
168
|
app.quit();
|
|
150
169
|
}).catch(error => { console.error(error); app.exit(1); });
|
|
@@ -129,6 +129,19 @@ test('usage.updated:upsert usage item + thread.usage 合并', () => {
|
|
|
129
129
|
assert.deepStrictEqual(thread.usage, { inputTokens: 10, outputTokens: 5 });
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test('verification.updated:终态 Turn 可追加并更新可审计验证报告', () => {
|
|
133
|
+
const { core } = coreWithThread();
|
|
134
|
+
const turn = core.createTurn({ threadId: 'thread_1' });
|
|
135
|
+
core.dispatch({ threadId: 'thread_1', turnId: turn.id, type: 'turn.started' });
|
|
136
|
+
core.dispatch({ threadId: 'thread_1', turnId: turn.id, type: 'turn.completed' });
|
|
137
|
+
core.dispatch({ threadId: 'thread_1', turnId: turn.id, type: 'verification.updated', payload: { report: { reportId: 'r1', status: 'failed' } } });
|
|
138
|
+
const item = core.getItemsForTurn(turn.id).find(candidate => candidate.type === 'verification_report');
|
|
139
|
+
assert.strictEqual(item.report.status, 'failed');
|
|
140
|
+
core.dispatch({ threadId: 'thread_1', turnId: turn.id, type: 'verification.updated', payload: { report: { reportId: 'r2', status: 'passed' } } });
|
|
141
|
+
assert.strictEqual(core.getItemsForTurn(turn.id).filter(candidate => candidate.type === 'verification_report').length, 1);
|
|
142
|
+
assert.strictEqual(item.report.status, 'passed');
|
|
143
|
+
});
|
|
144
|
+
|
|
132
145
|
test('snapshot / reset', () => {
|
|
133
146
|
const { core } = coreWithThread();
|
|
134
147
|
const turn = core.createTurn({ threadId: 'thread_1' });
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { promises: fs } = require('node:fs');
|
|
5
|
+
const { Store } = require('../src/main/host/store');
|
|
6
|
+
const { compactThread, hydrateThread, storageProjection } = require('../src/main/host/thread-storage');
|
|
7
|
+
const { VerificationGates, normalizePolicy } = require('../src/main/host/verification-gates');
|
|
8
|
+
const { ThreadStore } = require('../src/main/host/thread-store');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'harness-mix-storage-'));
|
|
12
|
+
try {
|
|
13
|
+
const turn = { id: 'turn-1', threadId: 'thread-1', status: 'completed', itemIds: ['answer-1'], createdAt: 1, updatedAt: 2, startedAt: 1, completedAt: 2, lastSequence: 2, nativeTurnRef: {} };
|
|
14
|
+
const item = { id: 'answer-1', threadId: 'thread-1', turnId: 'turn-1', type: 'agent_message', status: 'completed', content: 'large answer '.repeat(1000), createdAt: 1, updatedAt: 2, nativeRef: {} };
|
|
15
|
+
const coreThread = { id: 'thread-1', workspaceId: root, harnessId: 'fixture', status: 'idle', activeTurnId: null, nativeSessionRef: {}, metadata: {}, usage: {}, createdAt: 1, updatedAt: 2 };
|
|
16
|
+
const thread = { id: 'thread-1', title: 'fixture', cwd: root, harnessId: 'fixture', nativeSessionId: 'native-1', status: 'ready', connectionStatus: 'ready', createdAt: 1,
|
|
17
|
+
messages: [{ id: 'user-1', role: 'user', text: 'hello', at: 1 }, { id: 'turn-1', role: 'assistant', coreTurnId: 'turn-1', coreTurn: turn, coreItems: [item], text: item.content, thinking: '', at: 1, endedAt: 2, stopReason: 'completed' }],
|
|
18
|
+
tools: [], pendingApprovals: [], coreThread, currentTurn: turn, coreState: { version: 1, thread: coreThread, turns: [turn], items: [item] } };
|
|
19
|
+
await fs.writeFile(path.join(root, 'threads.json'), JSON.stringify([thread]));
|
|
20
|
+
const store = new Store(root, 'threads.json', { schemaVersion: 2, serialize: compactThread, deserialize: hydrateThread, backup: true });
|
|
21
|
+
const loaded = await store.load();
|
|
22
|
+
assert.equal(loaded[0].messages[1].text, item.content, 'legacy array remains readable');
|
|
23
|
+
await store.save(loaded);
|
|
24
|
+
const persisted = JSON.parse(await fs.readFile(path.join(root, 'threads.json'), 'utf8'));
|
|
25
|
+
assert.equal(persisted.schemaVersion, 2);
|
|
26
|
+
assert.equal(persisted.threads[0].messages[1].text, undefined, 'derived assistant text is not duplicated');
|
|
27
|
+
assert.equal(persisted.threads[0].coreState.items[0].content, item.content, 'authoritative Core content is preserved');
|
|
28
|
+
assert.ok((await fs.stat(path.join(root, 'threads.json.bak'))).size > 0, 'legacy store is backed up during migration');
|
|
29
|
+
const projection = storageProjection(loaded);
|
|
30
|
+
assert.ok(projection.savedBytes > 0);
|
|
31
|
+
const shardedRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'harness-mix-sharded-'));
|
|
32
|
+
await fs.writeFile(path.join(shardedRoot, 'threads.json'), JSON.stringify({ schemaVersion: 2, threads: [compactThread(thread)] }));
|
|
33
|
+
const sharded = new ThreadStore(shardedRoot);
|
|
34
|
+
const migration = await sharded.loadIndex();
|
|
35
|
+
assert.equal(migration[0]._storageStub, undefined, 'legacy records load fully for one-time migration');
|
|
36
|
+
migration[0].status = 'working';
|
|
37
|
+
migration[0].connectionStatus = 'busy';
|
|
38
|
+
await sharded.save(migration);
|
|
39
|
+
const stubs = await sharded.loadIndex();
|
|
40
|
+
assert.equal(stubs[0]._storageStub, true);
|
|
41
|
+
assert.equal(stubs[0].status, 'interrupted', 'cold startup never presents a stale running task');
|
|
42
|
+
assert.equal(stubs[0].connectionStatus, 'ready');
|
|
43
|
+
assert.equal(stubs[0].messages.length, 0, 'cold index does not load transcript bodies');
|
|
44
|
+
sharded.hydrateInto(stubs[0]);
|
|
45
|
+
assert.equal(stubs[0].coreState.items[0].content, item.content);
|
|
46
|
+
assert.equal((await sharded.inspectFiles()).recordCount, 1);
|
|
47
|
+
assert.ok((await fs.stat(path.join(shardedRoot, 'threads.json.bak'))).size > 0, 'monolithic source remains recoverable');
|
|
48
|
+
await fs.rm(shardedRoot, { recursive: true, force: true });
|
|
49
|
+
const unsupportedRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'harness-mix-storage-schema-'));
|
|
50
|
+
await fs.writeFile(path.join(unsupportedRoot, 'threads.json'), JSON.stringify({ schemaVersion: 99, threads: [] }));
|
|
51
|
+
await assert.rejects(new Store(unsupportedRoot, 'threads.json', { schemaVersion: 2 }).load(), /Unsupported thread store schema/);
|
|
52
|
+
await fs.rm(unsupportedRoot, { recursive: true, force: true });
|
|
53
|
+
|
|
54
|
+
const fakeRuntime = {
|
|
55
|
+
core: { getTurn: () => turn, getItemsForTurn: () => [item], dispatch: () => {} },
|
|
56
|
+
execution: { lastTurn: () => turn, sync: () => {} },
|
|
57
|
+
};
|
|
58
|
+
const gates = new VerificationGates(fakeRuntime);
|
|
59
|
+
gates.configure(thread, { mode: 'required', checks: { cleanWorkingTree: false }, commands: [{ id: 'node', command: `"${process.execPath}" -e "process.exit(0)"`, timeoutMs: 10_000 }] });
|
|
60
|
+
assert.throws(() => gates.assertSatisfied(thread, '交付'), /未通过/);
|
|
61
|
+
const passed = await gates.run(thread);
|
|
62
|
+
assert.equal(passed.status, 'passed');
|
|
63
|
+
assert.match(passed.contentDigest, /^[a-f0-9]{64}$/);
|
|
64
|
+
assert.equal(gates.inspect(thread).satisfied, true);
|
|
65
|
+
const stale = gates.invalidate(thread, 'test state changed');
|
|
66
|
+
assert.equal(stale.status, 'stale');
|
|
67
|
+
assert.equal(gates.inspect(thread).satisfied, false, 'required reports become stale after task state changes');
|
|
68
|
+
gates.configure(thread, { mode: 'advisory', commands: [`"${process.execPath}" -e "process.exit(3)"`] });
|
|
69
|
+
const failed = await gates.run(thread);
|
|
70
|
+
assert.equal(failed.status, 'failed');
|
|
71
|
+
assert.equal(gates.inspect(thread).satisfied, true, 'advisory failures do not block delivery');
|
|
72
|
+
assert.throws(() => normalizePolicy({ commands: [{ command: '' }] }), /Invalid verification command/);
|
|
73
|
+
assert.throws(() => normalizePolicy({ commands: [{ command: 'tool --token sk-1234567890abcdef' }] }), /credential-like data/);
|
|
74
|
+
console.log('PASS: schema v2 compact migration, backup, storage metrics and configurable verification gates');
|
|
75
|
+
} finally {
|
|
76
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -154,7 +154,7 @@ async function main() {
|
|
|
154
154
|
|
|
155
155
|
// 8) 持久化:threads.json 里切换链完整(含原生会话引用,供重启后惰性恢复)
|
|
156
156
|
await rt.store.save(rt.threads);
|
|
157
|
-
const persisted =
|
|
157
|
+
const persisted = (await rt.store.load()).find(t => t.id === thread.id);
|
|
158
158
|
assert.equal(persisted.harnessId, 'a');
|
|
159
159
|
assert.deepEqual(persisted.harnessChain.map(e => e.harnessId), ['a', 'b']);
|
|
160
160
|
assert.equal(persisted.harnessChain[0].nativeSessionId, aNativeId);
|
|
@@ -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
|
|
1215
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
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,
|