@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
|
@@ -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: "
|
|
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
|
-
...(
|
|
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
|
|
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,
|
|
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
|
-
|
|
480
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|
package/src/main/adapters/dsh.js
CHANGED
|
@@ -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 =>
|
|
7
|
-
|
|
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]));
|
package/src/main/adapters/omp.js
CHANGED
|
@@ -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: {
|
|
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 };
|
package/src/main/adapters/pi.js
CHANGED
|
@@ -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: {
|
|
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
|
-
|
|
188
|
-
const child = job.childId ? rt.threads.find(t => t.id === job.childId) : await rt.createThread({
|
|
189
|
-
|
|
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
|
-
|
|
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 {
|
|
231
|
-
|
|
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 {
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|