@harness-mix/cli 0.2.4 → 0.3.0

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 (49) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -4
  3. package/docs/harness-management.md +1 -1
  4. package/docs/multi-agent-collaboration.md +3 -1
  5. package/docs/native-acp.md +138 -127
  6. package/output/native-build/renderer-extension.js +149 -19
  7. package/package.json +4 -2
  8. package/scripts/acp-image-test.cjs +69 -0
  9. package/scripts/adapters-test.cjs +25 -0
  10. package/scripts/codex-accounts-test.cjs +34 -0
  11. package/scripts/codex-adapter-test.cjs +77 -1
  12. package/scripts/collaboration-test.cjs +304 -1
  13. package/scripts/collaboration-ui-smoke.cjs +25 -3
  14. package/scripts/e2e-delegate.cjs +1 -1
  15. package/scripts/e2e-hermes-image.cjs +60 -0
  16. package/scripts/openclaw-adapter-test.cjs +121 -3
  17. package/scripts/openclaw-mcp-probe.cjs +100 -0
  18. package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
  19. package/scripts/openclaw-thinking-probe.cjs +73 -0
  20. package/scripts/storage-verification-test.cjs +11 -0
  21. package/src/main/adapters/claude.js +15 -7
  22. package/src/main/adapters/codex-app-server.js +29 -11
  23. package/src/main/adapters/codex.js +39 -5
  24. package/src/main/adapters/omp.js +7 -2
  25. package/src/main/adapters/openclaw.js +534 -343
  26. package/src/main/adapters/pi-family.js +35 -8
  27. package/src/main/adapters/zcode.js +12 -8
  28. package/src/main/host/collaboration-tools.js +1 -1
  29. package/src/main/host/collaboration.js +285 -36
  30. package/src/main/host/runtime.js +9 -3
  31. package/src/main/host/verification-gates.js +14 -2
  32. package/src/main/native/codex-accounts.js +15 -4
  33. package/src/main/native/protocol.js +6 -0
  34. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
  35. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
  36. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +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-collab-cards.d.ts +1 -0
  39. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
  40. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -0
  41. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  42. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts +4 -0
  43. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  44. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  45. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +20 -7
  46. package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
  47. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
  48. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
  49. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
@@ -0,0 +1,100 @@
1
+ // OpenClaw 托管 MCP 对账实测探针:
2
+ // 备份 openclaw.json → 推送一个仓库内 stdio MCP 服务器(collaboration-mcp.cjs)→
3
+ // 验证 openclaw mcp list / Gateway tools 面能看到 → 移除 → 断言用户键零变化。
4
+ // 任何一步异常都会用备份还原配置。
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const { execFileSync } = require("node:child_process");
8
+ const { OpenClawGatewayHost } = require("../src/main/adapters/openclaw-gateway");
9
+ const { applyManagedMcpServers, MCP_OWNED_PREFIX } = require("../src/main/adapters/openclaw");
10
+ const { OPENCLAW_CONFIG } = require("../src/main/adapters/openclaw-gateway");
11
+ const { cliSpawn } = require("../src/main/host/jsonl");
12
+
13
+ const OUT_DIR = path.join(__dirname, "..", "output", "openclaw-mcp-probe");
14
+ const SERVER = path.join(__dirname, "..", "src", "main", "host", "collaboration-mcp.cjs");
15
+
16
+ const readCfg = () => JSON.parse(fs.readFileSync(OPENCLAW_CONFIG, "utf8"));
17
+ const stripOwned = (servers) => Object.fromEntries(Object.entries(servers ?? {}).filter(([k]) => !k.startsWith(MCP_OWNED_PREFIX)));
18
+ // 键序无关的规范化序列化(CLI 规范化会重排键序);meta.lastTouched* 是 CLI 自维护簿记,不计入
19
+ const canonical = (value) => {
20
+ if (Array.isArray(value)) return value.map(canonical);
21
+ if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, canonical(value[k])]));
22
+ return value;
23
+ };
24
+ const userViewOf = (cfg) => {
25
+ const { meta, ...rest } = cfg;
26
+ const mcp = { ...rest.mcp, servers: stripOwned(rest.mcp?.servers) };
27
+ // CLI 规范化会删除空的 mcp/servers 键;空对象与键缺失视为等价(不含用户内容)
28
+ if (mcp.servers && Object.keys(mcp.servers).length === 0) delete mcp.servers;
29
+ if (Object.keys(mcp).length === 0) delete rest.mcp; else rest.mcp = mcp;
30
+ return rest;
31
+ };
32
+ const userView = (cfg) => JSON.stringify(canonical(userViewOf(cfg)));
33
+ const diffViews = (beforeStr, nowStr) => {
34
+ const walk = (x, y, p) => {
35
+ for (const k of new Set([...Object.keys(x || {}), ...Object.keys(y || {})])) {
36
+ const q = p ? `${p}.${k}` : k;
37
+ if (JSON.stringify(x?.[k]) !== JSON.stringify(y?.[k])) {
38
+ if (x?.[k] && y?.[k] && typeof x[k] === "object" && typeof y[k] === "object" && !Array.isArray(x[k]) && !Array.isArray(y[k])) walk(x[k], y[k], q);
39
+ else console.error(`USERVIEW DIFF: ${q} | ${JSON.stringify(x?.[k])?.slice(0, 90)} -> ${JSON.stringify(y?.[k])?.slice(0, 90)}`);
40
+ }
41
+ }
42
+ };
43
+ walk(JSON.parse(beforeStr), JSON.parse(nowStr), "");
44
+ };
45
+ const mcpList = () => {
46
+ const cli = cliSpawn("openclaw", ["mcp", "list", "--json"]);
47
+ return JSON.parse(execFileSync(cli.command, cli.args, { encoding: "utf8", windowsHide: true, timeout: 20000 }));
48
+ };
49
+
50
+ (async () => {
51
+ fs.mkdirSync(OUT_DIR, { recursive: true });
52
+ const backup = path.join(OUT_DIR, `openclaw.json.backup-${Date.now()}`);
53
+ fs.copyFileSync(OPENCLAW_CONFIG, backup);
54
+ console.log("backup ->", backup);
55
+ const before = userView(readCfg());
56
+ let ok = false;
57
+ try {
58
+ // 1. 推送托管服务器(与适配器 open() 同路径)
59
+ await applyManagedMcpServers([{ name: "probe", command: process.execPath, args: [SERVER], env: {} }], (line) => console.log(line));
60
+ const listed = mcpList();
61
+ console.log("mcp list after push ->", JSON.stringify(Object.keys(listed)));
62
+ if (!listed[MCP_OWNED_PREFIX + "probe"]) throw new Error("自有键未出现在 mcp list");
63
+ const viewNow = userView(readCfg());
64
+ if (viewNow !== before) {
65
+ diffViews(before, viewNow);
66
+ throw new Error("用户键视图发生变化(不应发生)");
67
+ }
68
+
69
+ // 2. Gateway 侧确认注册表可读;tools 面尝试 catalog/effective(effective 需要 sessionKey)
70
+ const host = await OpenClawGatewayHost.acquire(() => {});
71
+ try {
72
+ const sessionKey = `harness-mix-mcp-probe-${Date.now()}`;
73
+ await host.call("sessions.create", { key: sessionKey, label: `Harness Mix MCP probe ${Date.now()}` });
74
+ const evidence = {};
75
+ evidence.catalog = await host.call("tools.catalog", {}).catch((e) => ({ error: e.message }));
76
+ evidence.effective = await host.call("tools.effective", { sessionKey }).catch((e) => ({ error: e.message }));
77
+ const text = JSON.stringify(evidence);
78
+ fs.writeFileSync(path.join(OUT_DIR, "tools-evidence.json"), JSON.stringify(evidence, null, 2));
79
+ const hitCount = (text.match(new RegExp(MCP_OWNED_PREFIX + "probe", "g")) || []).length;
80
+ console.log("tools evidence saved; owned-server mentions:", hitCount);
81
+ console.log("effective summary ->", JSON.stringify(evidence.effective).slice(0, 400));
82
+ } finally { await OpenClawGatewayHost.release(); }
83
+
84
+ // 3. 移除托管服务器,断言注册表回到初始用户视图
85
+ await applyManagedMcpServers([], (line) => console.log(line));
86
+ const after = mcpList();
87
+ console.log("mcp list after cleanup ->", JSON.stringify(Object.keys(after)));
88
+ if (after[MCP_OWNED_PREFIX + "probe"]) throw new Error("自有键未被移除");
89
+ const finalView = userView(readCfg());
90
+ if (finalView !== before) { diffViews(before, finalView); throw new Error("清理后用户键视图与初始不一致"); }
91
+ ok = true;
92
+ console.log("PROBE OK:推送/注册表可见/清理/用户键零变化 全部通过");
93
+ } finally {
94
+ if (!ok) {
95
+ fs.copyFileSync(backup, OPENCLAW_CONFIG);
96
+ console.error("已用备份还原 openclaw.json");
97
+ }
98
+ }
99
+ process.exit(ok ? 0 : 1);
100
+ })().catch((e) => { console.error("FAILED:", e.message); process.exit(1); });
@@ -0,0 +1,55 @@
1
+ // OpenClaw 托管 MCP 端到端实测:推送极简 echo MCP 服务器 → 真实 agent 回合调用其工具 →
2
+ // 捕获 tool 流帧验证 → 清理并断言注册表还原。证据落 output/openclaw-mcp-probe/。
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { randomUUID } = require("node:crypto");
6
+ const { OpenClawGatewayHost, OPENCLAW_CONFIG } = require("../src/main/adapters/openclaw-gateway");
7
+ const { applyManagedMcpServers, MCP_OWNED_PREFIX } = require("../src/main/adapters/openclaw");
8
+
9
+ const OUT_DIR = path.join(__dirname, "..", "output", "openclaw-mcp-probe");
10
+ const ECHO_SERVER = path.join(OUT_DIR, "echo-mcp-server.cjs");
11
+
12
+ (async () => {
13
+ const backup = path.join(OUT_DIR, `openclaw.json.backup-tool-${Date.now()}`);
14
+ fs.copyFileSync(OPENCLAW_CONFIG, backup);
15
+ const frames = [];
16
+ let ok = false;
17
+ try {
18
+ await applyManagedMcpServers([{ name: "probe", command: process.execPath, args: [ECHO_SERVER] }], (l) => console.log(l));
19
+ const host = await OpenClawGatewayHost.acquire((l) => console.error(l));
20
+ try {
21
+ const baseKey = `harness-mix-mcp-tool-probe-${Date.now()}`;
22
+ const created = await host.call("sessions.create", { key: baseKey, label: `Harness Mix MCP tool probe ${Date.now()}` });
23
+ const sessionKey = created?.key ?? baseKey;
24
+ const unwatch = host.onEvent((frame) => {
25
+ if (frame?.type !== "event" || frame.event !== "agent") return;
26
+ const p = frame.payload ?? {};
27
+ if (p.sessionKey && p.sessionKey !== sessionKey) return;
28
+ frames.push(p);
29
+ if (p.stream === "tool") console.log(`[tool] ${p.data?.name ?? ""} phase=${p.data?.phase} meta=${p.data?.meta ?? ""}`);
30
+ });
31
+ const accepted = await host.call("agent", {
32
+ message: "请调用 MCP 工具 hm_echo(若名称带前缀也用它),参数 text=HELLO_HM。完成后只回复工具返回的原始文本,不要添加别的内容。",
33
+ sessionKey, idempotencyKey: randomUUID(), deliver: false,
34
+ });
35
+ console.log("agent accepted:", JSON.stringify(accepted));
36
+ const settled = await host.call("agent.wait", { runId: accepted.runId, timeoutMs: 240_000 }, 250_000);
37
+ console.log("agent.wait:", JSON.stringify(settled).slice(0, 300));
38
+ unwatch();
39
+ const toolCalls = frames.filter((f) => f.stream === "tool");
40
+ const echoHit = toolCalls.some((f) => String(f.data?.name ?? "").includes("hm_echo") || String(f.data?.meta ?? "").includes("hm_echo"));
41
+ const assistantText = frames.filter((f) => f.stream === "assistant").map((f) => f.data?.delta ?? f.data?.text ?? "").join("");
42
+ console.log(`tool frames: ${toolCalls.length}, hm_echo hit: ${echoHit}, assistant: ${assistantText.slice(0, 120)}`);
43
+ fs.writeFileSync(path.join(OUT_DIR, "tool-call-evidence.json"), JSON.stringify({ capturedAt: new Date().toISOString(), frames, echoHit, assistantText }, null, 2));
44
+ ok = echoHit || /ECHO:HELLO_HM/.test(assistantText);
45
+ console.log(ok ? "TOOL PROBE OK:托管 MCP 工具被真实调用" : "TOOL PROBE 未观测到工具调用(模型未配合或 bundle 未加载)");
46
+ } finally { await OpenClawGatewayHost.release(); }
47
+ } finally {
48
+ await applyManagedMcpServers([], () => {});
49
+ if (!ok) {
50
+ fs.copyFileSync(backup, OPENCLAW_CONFIG);
51
+ console.error("已用备份还原 openclaw.json");
52
+ }
53
+ }
54
+ process.exit(ok ? 0 : 1);
55
+ })().catch((e) => { console.error("FAILED:", e); process.exit(1); });
@@ -0,0 +1,73 @@
1
+ // OpenClaw Gateway 思考流探针:开启 reasoningLevel=stream 后跑一个真实回合,
2
+ // 捕获 agent 广播原始帧(重点 stream:"thinking"),为适配器投影与能力声明提供实测证据。
3
+ // 用法:node scripts/openclaw-thinking-probe.cjs [modelId]
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const { randomUUID } = require("node:crypto");
7
+ const { OpenClawGatewayHost } = require("../src/main/adapters/openclaw-gateway");
8
+
9
+ const OUT_DIR = path.join(__dirname, "..", "output", "openclaw-thinking-probe");
10
+
11
+ (async () => {
12
+ fs.mkdirSync(OUT_DIR, { recursive: true });
13
+ const host = await OpenClawGatewayHost.acquire((line) => console.error(line));
14
+ const frames = [];
15
+ try {
16
+ const key = `harness-mix-thinking-probe-${Date.now()}`;
17
+ const created = await host.call("sessions.create", { key, label: `Harness Mix thinking probe ${Date.now()}` });
18
+ console.log("sessions.create ->", JSON.stringify(created));
19
+ const sessionKey = created?.key ?? key;
20
+
21
+ // 两个独立维度:thinkingLevel 控制推理投入,reasoningLevel=stream 才把思考流广播给客户端
22
+ let patched = null;
23
+ try {
24
+ patched = await host.call("sessions.patch", { key: sessionKey, reasoningLevel: "stream", thinkingLevel: "medium" });
25
+ } catch (error) {
26
+ console.error("sessions.patch FAILED:", error.message, "code=", error.code);
27
+ }
28
+ console.log("sessions.patch ->", JSON.stringify(patched));
29
+
30
+ const modelArg = process.argv[2] || undefined;
31
+ if (modelArg) console.log("using model override:", modelArg);
32
+
33
+ const unwatch = host.onEvent((frame) => {
34
+ if (frame?.type !== "event" || frame.event !== "agent") return;
35
+ const p = frame.payload ?? {};
36
+ if (p.sessionKey && p.sessionKey !== sessionKey) return;
37
+ frames.push(p);
38
+ const data = p.data ?? {};
39
+ const brief = typeof data.delta === "string" ? data.delta : typeof data.text === "string" ? data.text : "";
40
+ console.log(`[stream=${p.stream}] ${(brief || JSON.stringify(data)).slice(0, 160).replace(/\n/g, "\\n")}`);
41
+ });
42
+
43
+ const accepted = await host.call("agent", {
44
+ message: "一个三位数,各位数字之和为 18,百位比个位大 3,且它是 4 的倍数。求这个数,并给出推理过程。",
45
+ sessionKey,
46
+ idempotencyKey: randomUUID(),
47
+ deliver: false,
48
+ thinking: "high",
49
+ ...(modelArg ? { model: modelArg } : {}),
50
+ });
51
+ console.log("agent ->", JSON.stringify(accepted));
52
+ if (!accepted?.runId) throw new Error("未返回 runId");
53
+ const settled = await host.call("agent.wait", { runId: accepted.runId, timeoutMs: 240_000 }, 250_000);
54
+ console.log("agent.wait ->", JSON.stringify(settled));
55
+ unwatch();
56
+
57
+ const described = await host.call("sessions.describe", { key: sessionKey }).catch((e) => ({ error: e.message }));
58
+ console.log("sessions.describe ->", JSON.stringify(described?.session ?? described));
59
+
60
+ const summary = {};
61
+ for (const f of frames) summary[f.stream] = (summary[f.stream] ?? 0) + 1;
62
+ const hasThinking = frames.some((f) => f.stream === "thinking" && (f.data?.delta || f.data?.text));
63
+ console.log("stream summary ->", JSON.stringify(summary), "thinkingVerified=", hasThinking);
64
+
65
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
66
+ const outFile = path.join(OUT_DIR, `thinking-frames-${stamp}.json`);
67
+ fs.writeFileSync(outFile, JSON.stringify({ capturedAt: new Date().toISOString(), key, frames, described: described?.session ?? null, summary, hasThinking }, null, 2));
68
+ console.log("saved ->", outFile);
69
+ } finally {
70
+ await OpenClawGatewayHost.release();
71
+ }
72
+ process.exit(0);
73
+ })().catch((e) => { console.error("FAILED:", e); process.exit(1); });
@@ -69,6 +69,17 @@ const { ThreadStore } = require('../src/main/host/thread-store');
69
69
  const failed = await gates.run(thread);
70
70
  assert.equal(failed.status, 'failed');
71
71
  assert.equal(gates.inspect(thread).satisfied, true, 'advisory failures do not block delivery');
72
+ // advisory():off 策略的 apply 零配置安全网——跑内建检查,不跑用户命令、
73
+ // 不写 latestReport、不改线程策略;显式策略下不附加
74
+ gates.configure(thread, { mode: 'off', commands: [`"${process.execPath}" -e "process.exit(3)"`] });
75
+ const advisory = await gates.advisory(thread);
76
+ assert.equal(advisory.mode, 'advisory');
77
+ assert.deepEqual(advisory.commands, [], 'advisory 不跑用户命令');
78
+ assert.ok(advisory.checks.some(check => check.id === 'turnCompleted'), '内建检查在列');
79
+ assert.equal(gates.inspect(thread).policy.mode, 'off', '线程策略保持 off');
80
+ assert.equal(gates.inspect(thread).report, null, 'advisory 不写 latestReport');
81
+ gates.configure(thread, { mode: 'advisory' });
82
+ assert.equal(await gates.advisory(thread), null, '显式策略下不附加安全网');
72
83
  assert.throws(() => normalizePolicy({ commands: [{ command: '' }] }), /Invalid verification command/);
73
84
  assert.throws(() => normalizePolicy({ commands: [{ command: 'tool --token sk-1234567890abcdef' }] }), /credential-like data/);
74
85
  console.log('PASS: schema v2 compact migration, backup, storage metrics and configurable verification gates');
@@ -13,7 +13,10 @@ const manifest = {
13
13
  capabilities: { collaborationTools: true, streaming: true, thinking: true, tools: true, approvals: true, questions: true, models: true, thinkingLevels: true, permissionModes: true, resume: true, fork: true, forkFromMessage: true, compaction: true, usage: true, contextUsage: true, attachments: true },
14
14
  };
15
15
 
16
- /** Claude Code 原生权限模式(SDK PermissionMode 全集),与其 TUI/Desktop 一致 */
16
+ /** Claude Code 原生权限模式(SDK PermissionMode 配置值全集),与其 TUI/Desktop 一致。
17
+ * 实测校准(本机 claude 2.1.220 `--help` 与 code.claude.com/docs/en/permission-modes):
18
+ * CLI 旗标选项为 acceptEdits/auto/bypassPermissions/manual/dontAsk/plan,其中 manual 只是
19
+ * default 的 CLI 别名(SDK/钩子配置值仍为 default),auto/dontAsk 为新增档;此处按 SDK 配置值列出。 */
17
20
  const CLAUDE_PERMISSION_MODES = [
18
21
  { id: "default", label: "默认(询问)", description: "编辑和其他受保护操作前询问" },
19
22
  { id: "plan", label: "规划模式", description: "探索并制定计划;批准计划后退出规划" },
@@ -457,12 +460,17 @@ function create() {
457
460
  if (!session?.query) return base;
458
461
  try {
459
462
  const commands = await session.query.supportedCommands();
460
- return [
461
- ...base,
462
- ...commands.filter((c) => c.name !== 'compact').map((c) => ({
463
- id: c.name, label: '/' + c.name, description: c.description ?? '', action: 'insert', text: '/' + c.name + ' ',
464
- })),
465
- ];
463
+ const seen = new Set(['compact']); // 与既有 id(含原生 compact)去重
464
+ const mapped = [];
465
+ for (const c of commands) {
466
+ if (!c?.name || seen.has(c.name)) continue;
467
+ seen.add(c.name);
468
+ mapped.push({
469
+ id: c.name, label: '/' + c.name, action: 'insert', text: '/' + c.name + ' ',
470
+ description: `${c.description ?? ''}${c.argumentHint ? `(参数:${c.argumentHint})` : ''}`,
471
+ });
472
+ }
473
+ return [...base, ...mapped];
466
474
  } catch { return base; }
467
475
  },
468
476
  async executeCommand(session, id, hooks) {
@@ -2,6 +2,14 @@ const { JsonlProcess, cliSpawn } = require('../host/jsonl');
2
2
 
3
3
  const shared = new Map();
4
4
 
5
+ // codex 拒绝在缺失的 CODEX_HOME 下启动、或握手被外部因素(杀软扫描/磁盘/版本握手)挂住时,
6
+ // initialize 永不返回。没有这个上限,thread/start 会永久 pending,Desktop 端表现为
7
+ // 新对话"一直在执行"却没有会话产生。测试可用 env 覆盖。
8
+ function handshakeTimeoutMs() {
9
+ const raw = Number(process.env.HARNESS_MIX_CODEX_HANDSHAKE_TIMEOUT_MS);
10
+ return Number.isFinite(raw) && raw >= 100 ? raw : 20_000;
11
+ }
12
+
5
13
  /**
6
14
  * A single native Codex app-server connection shared by every Codex thread.
7
15
  * app-server owns thread/session persistence; this class only routes JSON-RPC
@@ -25,17 +33,27 @@ class CodexAppServer {
25
33
  onDiagnostic: (line) => this.#diagnostic(line),
26
34
  onExit: (error) => this.#exit(error),
27
35
  });
28
- this.ready = this.process.request('initialize', {
29
- clientInfo: { name: 'harness-mix', title: 'Harness Mix', version: '0.1.0' },
30
- capabilities: {
31
- experimentalApi: true,
32
- requestAttestation: false,
33
- mcpServerOpenaiFormElicitation: false,
34
- },
35
- }).then((result) => {
36
- this.process.notify('initialized', {});
37
- return result;
38
- });
36
+ let rejectHandshake;
37
+ const handshakeGuard = new Promise((_unused, reject) => { rejectHandshake = reject; });
38
+ const handshakeTimer = setTimeout(() => {
39
+ this.stop();
40
+ rejectHandshake(new Error(`Codex app-server 未在 ${Math.round(handshakeTimeoutMs() / 1000)} 秒内完成初始化握手,已终止进程`));
41
+ }, handshakeTimeoutMs());
42
+ handshakeTimer.unref?.();
43
+ this.ready = Promise.race([
44
+ this.process.request('initialize', {
45
+ clientInfo: { name: 'harness-mix', title: 'Harness Mix', version: '0.1.0' },
46
+ capabilities: {
47
+ experimentalApi: true,
48
+ requestAttestation: false,
49
+ mcpServerOpenaiFormElicitation: false,
50
+ },
51
+ }).then((result) => {
52
+ this.process.notify('initialized', {});
53
+ return result;
54
+ }),
55
+ handshakeGuard,
56
+ ]).finally(() => clearTimeout(handshakeTimer));
39
57
  }
40
58
 
41
59
  static async acquire(diagnostic, codexHome) {
@@ -119,6 +119,12 @@ function usageView(tokenUsage) {
119
119
  };
120
120
  }
121
121
 
122
+ /** Skill name → UI 契约 id([A-Za-z0-9._:-]+,≤128);"Agent Browser" → "agent-browser" */
123
+ function slugifySkillId(name) {
124
+ if (typeof name !== 'string') return '';
125
+ return name.trim().toLowerCase().replace(/[^a-z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 128);
126
+ }
127
+
122
128
  function emitTool(item, session, emit, state = toolState(item)) {
123
129
  emit({
124
130
  kind: 'tool', toolCallId: item.id, title: toolTitle(item), state,
@@ -362,16 +368,20 @@ function reviewerToWire(reviewer) {
362
368
  }
363
369
 
364
370
  const APP_SERVER_BUSY = /^Agent is already processing(?:\.|$)/i;
371
+ // busy 重试总预算:覆盖同线程 queue-start 的瞬时清槽,也覆盖共享 app-server 上
372
+ // 另一线程的短 turn(部分版本按进程串行 turn);长 turn 仍会超预算失败并如实报错
373
+ const BUSY_RETRY_BUDGET_MS = 20_000;
365
374
 
366
375
  async function startTurnAfterNativeSettlement(host, params) {
367
376
  // A queue-start can arrive immediately after turn/completed, while app-server is still
368
377
  // clearing its active-turn slot. Keep the same logical Core turn and retry only this
369
378
  // narrow transient; other errors must remain visible and must never be duplicated.
370
- for (let attempt = 0, delay = 25; ; attempt++, delay *= 2) {
379
+ const deadline = Date.now() + BUSY_RETRY_BUDGET_MS;
380
+ for (let delay = 25; ; delay = Math.min(delay * 2, 2_000)) {
371
381
  try {
372
382
  return await host.request('turn/start', params);
373
383
  } catch (error) {
374
- if (!APP_SERVER_BUSY.test(String(error?.message ?? error)) || attempt >= 5) throw error;
384
+ if (!APP_SERVER_BUSY.test(String(error?.message ?? error)) || Date.now() + delay > deadline) throw error;
375
385
  await new Promise(resolve => setTimeout(resolve, delay));
376
386
  }
377
387
  }
@@ -528,8 +538,32 @@ function create() {
528
538
  } else pending.resolve({ decision });
529
539
  },
530
540
 
531
- listCommands() {
532
- return [{ id: 'compact', label: '压缩上下文', description: '由 Codex 原生 app-server 压缩当前 Thread', action: 'execute' }];
541
+ // 原生 app-server 的 skills/list 即斜杠命令目录(/<skill-name> 触发插入);
542
+ // 旧版 app-server 无该 RPC 时回落静态目录。无会话(session==null)不拉起进程。
543
+ async listCommands(session) {
544
+ const base = [{ id: 'compact', label: '压缩上下文', description: '由 Codex 原生 app-server 压缩当前 Thread', action: 'execute' }];
545
+ if (!session) return base;
546
+ try {
547
+ const rows = await listAll(session.host, 'skills/list', { cwds: [session.cwd] });
548
+ const seen = new Set(['compact']);
549
+ const skills = [];
550
+ for (const row of rows) {
551
+ for (const skill of row?.skills ?? []) {
552
+ if (skill?.enabled === false) continue;
553
+ const id = slugifySkillId(skill.name);
554
+ if (!id || seen.has(id)) continue;
555
+ seen.add(id);
556
+ skills.push({
557
+ id,
558
+ label: '/' + (skill.interface?.displayName || skill.name),
559
+ description: `${String(skill.shortDescription || skill.description || '').slice(0, 512)}(Codex 技能·${skill.scope || 'user'})`,
560
+ action: 'insert',
561
+ text: '/' + id + ' ',
562
+ });
563
+ }
564
+ }
565
+ return [...base, ...skills];
566
+ } catch { return base; }
533
567
  },
534
568
 
535
569
  async executeCommand(session, id, { emit }) {
@@ -653,4 +687,4 @@ manifest.integrations = { mcp: true, skills: {
653
687
  project: ['.agents/skills'],
654
688
  overrides: { '.codex/skills': { env: 'CODEX_HOME', suffix: 'skills' } },
655
689
  } };
656
- module.exports = { manifest, create, projectNotification, queueRequest, usageView, modelView };
690
+ module.exports = { manifest, create, projectNotification, queueRequest, usageView, modelView, startTurnAfterNativeSettlement };
@@ -1,6 +1,9 @@
1
1
  // OMP(Oh My Pi)Adapter:Pi 的 fork,CLI 与 --mode rpc 协议同源。
2
- // 实现共享自 Pi 家族工厂(见 pi-family.js);OMP 侧差异(如有)在此覆盖。
3
- const { piFamily } = require('./pi-family');
2
+ // 实现共享自 Pi 家族工厂(见 pi-family.js);OMP 侧差异在此覆盖。
3
+ // 权限模型已分叉:OMP --approval-mode always-ask|write|yolo(无 Pi 的
4
+ // --approve/--no-approve 项目信任旗标),目录与启动旗标见 pi-family.js 的
5
+ // OMP_APPROVAL_MODES(实测 @oh-my-pi/pi-coding-agent 18.1.19 旗标表与设置 schema)。
6
+ const { piFamily, OMP_APPROVAL_MODES, ompPermissionLaunchArgs } = require('./pi-family');
4
7
 
5
8
  module.exports = piFamily({
6
9
  id: 'omp',
@@ -9,6 +12,8 @@ module.exports = piFamily({
9
12
  bin: 'omp',
10
13
  packageHint: 'npm i -g @oh-my-pi/pi-coding-agent 或 https://omp.sh/install',
11
14
  aliases: ['omp', 'oh-my-pi'],
15
+ permissionModes: OMP_APPROVAL_MODES,
16
+ permissionLaunchArgs: ompPermissionLaunchArgs,
12
17
  });
13
18
  module.exports.manifest.integrations = {
14
19
  mcp: false,