@harness-mix/cli 0.2.3 → 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 (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +469 -467
  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/desktop-controller.mjs +1 -1
  7. package/output/native-build/renderer-extension.js +172 -23
  8. package/package.json +13 -9
  9. package/scripts/acp-image-test.cjs +69 -0
  10. package/scripts/adapters-test.cjs +25 -0
  11. package/scripts/antigravity-adapter-test.cjs +647 -626
  12. package/scripts/codex-accounts-test.cjs +34 -0
  13. package/scripts/codex-adapter-test.cjs +238 -127
  14. package/scripts/collaboration-test.cjs +577 -262
  15. package/scripts/collaboration-ui-smoke.cjs +25 -3
  16. package/scripts/e2e-delegate.cjs +1 -1
  17. package/scripts/e2e-hermes-image.cjs +60 -0
  18. package/scripts/jsonl-stdin-test.cjs +40 -31
  19. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  20. package/scripts/native-acp-depth-test.cjs +30 -5
  21. package/scripts/native-update-apply-test.cjs +269 -215
  22. package/scripts/native-update.cjs +78 -0
  23. package/scripts/native-vendor-adapters-test.cjs +196 -154
  24. package/scripts/openclaw-adapter-test.cjs +121 -3
  25. package/scripts/openclaw-mcp-probe.cjs +100 -0
  26. package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
  27. package/scripts/openclaw-thinking-probe.cjs +73 -0
  28. package/scripts/salvage-rollout-writes.cjs +72 -0
  29. package/scripts/storage-verification-test.cjs +11 -0
  30. package/scripts/zcode-adapter-test.cjs +329 -0
  31. package/scripts/zcode-live-probe.cjs +66 -0
  32. package/src/main/adapters/antigravity.js +1428 -1418
  33. package/src/main/adapters/claude.js +15 -7
  34. package/src/main/adapters/codex-app-server.js +29 -11
  35. package/src/main/adapters/codex.js +690 -649
  36. package/src/main/adapters/native-acp-command.js +51 -48
  37. package/src/main/adapters/native-acp.js +47 -12
  38. package/src/main/adapters/omp.js +7 -2
  39. package/src/main/adapters/openclaw.js +534 -343
  40. package/src/main/adapters/pi-family.js +35 -8
  41. package/src/main/adapters/qoder.js +12 -8
  42. package/src/main/adapters/zcode.js +925 -10
  43. package/src/main/host/collaboration-tools.js +1 -1
  44. package/src/main/host/collaboration.js +971 -714
  45. package/src/main/host/jsonl.js +130 -120
  46. package/src/main/host/runtime.js +9 -3
  47. package/src/main/host/verification-gates.js +14 -2
  48. package/src/main/native/codex-accounts.js +15 -4
  49. package/src/main/native/config.js +9 -9
  50. package/src/main/native/launcher.js +252 -237
  51. package/src/main/native/process-utils.js +157 -57
  52. package/src/main/native/protocol.js +1227 -1187
  53. package/src/main/native/update-state.js +123 -110
  54. package/src/main/native/updater.js +460 -394
  55. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
  56. package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
  57. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  58. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  59. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  60. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +1 -0
  61. package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
  62. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -0
  63. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  64. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts +4 -0
  65. package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
  66. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3224 -3181
  68. package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
  69. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
  70. package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
  71. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
  72. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
@@ -17027,9 +17027,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
17027
17027
  "claude-code": { command: "npm install -g @anthropic-ai/claude-code" },
17028
17028
  "deepseek-harness": { command: "pip install deepseek-harness" },
17029
17029
  opencode: { command: "npm install -g opencode-ai" },
17030
- grok: { command: "npm install -g @xai/grok-cli" },
17030
+ grok: { command: "powershell -NoProfile -ExecutionPolicy Bypass -Command iex (irm https://x.ai/cli/install.ps1)" },
17031
17031
  omp: { command: "npm install -g @oh-my-pi/pi-coding-agent" },
17032
- antigravity: { command: "npm install -g @google/antigravity-cli" },
17032
+ antigravity: { command: "powershell -NoProfile -ExecutionPolicy Bypass -Command iex (irm https://antigravity.google/cli/install.ps1)" },
17033
17033
  openclaw: { command: "npm install -g openclaw" },
17034
17034
  hermes: { command: "pip install hermes-agent" },
17035
17035
  cline: { command: "npm install -g cline" },
@@ -23131,6 +23131,10 @@ ${error51.stderrTail}`] : []
23131
23131
  var THREAD_OWNERSHIP_LIST_METHOD = "harnessmix/thread/ownership/list";
23132
23132
  var THREAD_USAGE_INSPECT_METHOD = "harnessmix/thread/usage/inspect";
23133
23133
  var THREAD_TEAM_INSPECT_METHOD = "harnessmix/thread/team/inspect";
23134
+ var THREAD_TEAM_TASK_CANCEL_METHOD = "harnessmix/thread/team/task/cancel";
23135
+ var THREAD_TEAM_TASK_REASSIGN_METHOD = "harnessmix/thread/team/task/reassign";
23136
+ var THREAD_TEAM_MESSAGE_SEND_METHOD = "harnessmix/thread/team/message/send";
23137
+ var THREAD_COLLABORATION_CONTINUE_METHOD = "harnessmix/thread/collaboration/continue";
23134
23138
  var THREAD_USAGE_UPDATED_METHOD = "harnessmix/thread/usage/updated";
23135
23139
  var THREAD_TOKEN_USAGE_UPDATED_METHOD = "thread/tokenUsage/updated";
23136
23140
  var UPDATE_CHECK_METHOD = "harnessmix/update/check";
@@ -23361,6 +23365,15 @@ ${error51.stderrTail}`] : []
23361
23365
  const threadId = hostThreadIdSchema.parse(input.threadId);
23362
23366
  return manager.sendRequest(THREAD_TEAM_INSPECT_METHOD, { threadId, ...input.teamId ? { teamId: input.teamId } : {} });
23363
23367
  },
23368
+ async collaborationUserAction(input) {
23369
+ const threadId = hostThreadIdSchema.parse(input.threadId);
23370
+ const method = input.action === "task/cancel" ? THREAD_TEAM_TASK_CANCEL_METHOD : input.action === "task/reassign" ? THREAD_TEAM_TASK_REASSIGN_METHOD : input.action === "message/send" ? THREAD_TEAM_MESSAGE_SEND_METHOD : THREAD_COLLABORATION_CONTINUE_METHOD;
23371
+ const params = { threadId };
23372
+ for (const [key, value] of Object.entries(input)) {
23373
+ if (key !== "action" && key !== "threadId" && value !== void 0) params[key] = value;
23374
+ }
23375
+ return manager.sendRequest(method, params);
23376
+ },
23364
23377
  subscribeThreadUsage(listener) {
23365
23378
  const notifications = notificationTarget(source);
23366
23379
  if (!notifications?.addNotificationCallback) {
@@ -24966,6 +24979,28 @@ ${error51.stderrTail}`] : []
24966
24979
  const agent = payload.agent_type || "agent";
24967
24980
  const icon2 = collaborationIcon(agent, agent, 18);
24968
24981
  strip.append(icon2);
24982
+ if (payload.status === "interrupted" && payload.parent_thread_id && options.continueCollab) {
24983
+ const resumeBtn = document.createElement("button");
24984
+ resumeBtn.type = "button";
24985
+ resumeBtn.className = "harness-mix-collab-resume";
24986
+ resumeBtn.style.cssText = "display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;border:1px solid #c1702266;background:#c170221a;color:#c17022;font:inherit;cursor:pointer;font-size:12px;font-weight:500";
24987
+ resumeBtn.textContent = "\u25B6 \u6062\u590D\u6B64\u4EFB\u52A1";
24988
+ resumeBtn.title = "\u6062\u590D\u4E2D\u65AD\u7684\u59D4\u6D3E\uFF1A\u4E3B\u5BFC\u8005\u4F1A\u6536\u5230\u6062\u590D\u6307\u4EE4\uFF08\u4E0D\u91CD\u653E\u5DF2\u5B8C\u6210\u526F\u4F5C\u7528\uFF09";
24989
+ resumeBtn.addEventListener("click", async (e) => {
24990
+ e.preventDefault();
24991
+ e.stopPropagation();
24992
+ resumeBtn.disabled = true;
24993
+ try {
24994
+ await options.continueCollab(payload.parent_thread_id, payload.task_id);
24995
+ resumeBtn.textContent = "\u2713 \u5DF2\u4E0B\u53D1\u6062\u590D\u6307\u4EE4";
24996
+ } catch (err) {
24997
+ resumeBtn.disabled = false;
24998
+ resumeBtn.textContent = "\u25B6 \u6062\u590D\u5931\u8D25\uFF08\u91CD\u8BD5\uFF09";
24999
+ resumeBtn.title = `\u6062\u590D\u5931\u8D25\uFF1A${err?.message || err}`;
25000
+ }
25001
+ });
25002
+ strip.append(resumeBtn);
25003
+ }
24969
25004
  if (payload.child_thread_id) {
24970
25005
  const childId = payload.child_thread_id;
24971
25006
  const jumpBtn = document.createElement("button");
@@ -25120,7 +25155,7 @@ ${error51.stderrTail}`] : []
25120
25155
  }
25121
25156
  var stateColor = (status) => ({ ready: "#8b8b8b", pending: "#8b8b8b", working: "#2878e3", in_progress: "#2878e3", active: "#2878e3", completed: "#1f9d68", blocked: "#c17022", failed: "#d14343", interrupted: "#c17022" })[status ?? ""] ?? "#8b8b8b";
25122
25157
  var stateLabel = (status) => ({ ready: "\u5C31\u7EEA", pending: "\u5F85\u5F00\u59CB", working: "\u5DE5\u4F5C\u4E2D", in_progress: "\u8FDB\u884C\u4E2D", completed: "\u5DF2\u5B8C\u6210", blocked: "\u7B49\u5F85\u4F9D\u8D56", failed: "\u5931\u8D25", interrupted: "\u5DF2\u4E2D\u65AD", active: "\u534F\u4F5C\u4E2D" })[status ?? ""] ?? status ?? "\u672A\u77E5";
25123
- var actionLabel = (action) => ({ team_created: "\u56E2\u961F\u5EFA\u7ACB", task_assigned: "\u4EFB\u52A1\u5206\u914D", task_updated: "\u4EFB\u52A1\u66F4\u65B0", message_sent: "\u56E2\u961F\u901A\u4FE1", task_started: "\u5F00\u59CB\u6267\u884C", member_session_ready: "\u4F1A\u8BDD\u5C31\u7EEA", task_settled: "\u4EFB\u52A1\u7ED3\u7B97", task_failed: "\u4EFB\u52A1\u5931\u8D25", task_cancelled: "\u4EFB\u52A1\u53D6\u6D88", task_interrupted: "\u4EFB\u52A1\u4E2D\u65AD", task_resumed: "\u6062\u590D\u6267\u884C", task_followup: "\u7EE7\u7EED\u6267\u884C" })[action ?? ""] ?? action ?? "\u5B9E\u65F6\u72B6\u6001";
25158
+ var actionLabel = (action) => ({ team_created: "\u56E2\u961F\u5EFA\u7ACB", task_assigned: "\u4EFB\u52A1\u5206\u914D", task_updated: "\u4EFB\u52A1\u66F4\u65B0", message_sent: "\u56E2\u961F\u901A\u4FE1", task_started: "\u5F00\u59CB\u6267\u884C", member_session_ready: "\u4F1A\u8BDD\u5C31\u7EEA", task_settled: "\u4EFB\u52A1\u7ED3\u7B97", task_failed: "\u4EFB\u52A1\u5931\u8D25", task_cancelled: "\u4EFB\u52A1\u53D6\u6D88", task_interrupted: "\u4EFB\u52A1\u4E2D\u65AD", task_resumed: "\u6062\u590D\u6267\u884C", task_followup: "\u7EE7\u7EED\u6267\u884C", task_reassigned: "\u4EFB\u52A1\u6539\u6D3E", task_retry: "\u81EA\u52A8\u91CD\u8BD5" })[action ?? ""] ?? action ?? "\u5B9E\u65F6\u72B6\u6001";
25124
25159
  var memberPalette = ["#3b82f6", "#f59e0b", "#10b981", "#8b5cf6", "#f97316", "#ec4899", "#14b8a6", "#6366f1"];
25125
25160
  var memberColor = (index) => memberPalette[index % memberPalette.length];
25126
25161
  function el(tag, className, css) {
@@ -25182,6 +25217,41 @@ ${error51.stderrTail}`] : []
25182
25217
  }
25183
25218
  });
25184
25219
  }
25220
+ function actionButton(label, title, onClick, tone) {
25221
+ const node = el("button", "harness-mix-team-action", `border:1px solid color-mix(in srgb,${tone ?? "currentColor"} 26%,transparent);border-radius:6px;background:color-mix(in srgb,${tone ?? "currentColor"} 7%,transparent);color:${tone ?? "inherit"};font:650 9px system-ui;padding:3px 8px;cursor:pointer;white-space:nowrap${tone ? "" : ";opacity:.85"}`);
25222
+ node.type = "button";
25223
+ node.textContent = label;
25224
+ node.title = title;
25225
+ node.addEventListener("click", (event) => {
25226
+ event.stopPropagation();
25227
+ event.preventDefault();
25228
+ onClick();
25229
+ });
25230
+ return node;
25231
+ }
25232
+ async function runUserAction(userAction, node, input) {
25233
+ node.classList.remove("harness-mix-team-open-failed");
25234
+ try {
25235
+ await userAction(input);
25236
+ } catch (error51) {
25237
+ console.warn("[TeamCards] \u770B\u677F\u64CD\u4F5C\u5931\u8D25:", error51);
25238
+ node.classList.add("harness-mix-team-open-failed");
25239
+ node.title = `\u64CD\u4F5C\u5931\u8D25\uFF1A${error51?.message ?? String(error51)}`;
25240
+ setTimeout(() => node.classList.remove("harness-mix-team-open-failed"), 1800);
25241
+ }
25242
+ }
25243
+ function continueButton(payload, userAction) {
25244
+ const busy = payload.lead?.display_status === "working";
25245
+ const node = actionButton("\u7EE7\u7EED\u534F\u4F5C", busy ? "\u4E3B\u5BFC\u8005\u56DE\u5408\u8FDB\u884C\u4E2D\uFF0C\u7ED3\u675F\u540E\u518D\u7EE7\u7EED" : "\u628A\u4E2D\u65AD\u7684\u59D4\u6D3E\u6062\u590D\u4E3A\u65B0\u7684\u4E3B\u5BFC\u8005\u56DE\u5408", () => {
25246
+ if (payload.lead_thread_id) void runUserAction(userAction, node, { action: "continue", threadId: payload.lead_thread_id });
25247
+ });
25248
+ if (busy) {
25249
+ node.disabled = true;
25250
+ node.style.opacity = ".45";
25251
+ node.style.cursor = "not-allowed";
25252
+ }
25253
+ return node;
25254
+ }
25185
25255
  function metrics(payload) {
25186
25256
  const node = el("div", "harness-mix-team-metrics", "display:flex;gap:18px;align-items:center");
25187
25257
  const values = [[`${payload.tasks.filter((task) => task.status === "completed").length}/${payload.tasks.length}`, "\u5B8C\u6210", "#1f9d68"], [`${payload.tasks.filter((task) => task.status === "in_progress").length}`, "\u8FDB\u884C\u4E2D", "#2878e3"], [`${payload.tasks.filter((task) => task.status === "blocked").length}`, "\u7B49\u5F85", "#c17022"]];
@@ -25225,8 +25295,8 @@ ${error51.stderrTail}`] : []
25225
25295
  dot.dataset.status = status ?? "";
25226
25296
  return dot;
25227
25297
  }
25228
- var actionIcon = (action) => ({ team_created: "\u{1F3AC}", task_assigned: "\u{1F4CB}", task_updated: "\u{1F504}", task_started: "\u{1F680}", member_session_ready: "\u{1F50C}", task_settled: "\u2705", task_failed: "\u274C", task_cancelled: "\u26D4", task_interrupted: "\u23F8", task_resumed: "\u25B6\uFE0F", task_followup: "\u{1F4AC}" })[action ?? ""] ?? "\u2022";
25229
- function renderBoard(payload, openThread, snapshots = []) {
25298
+ var actionIcon = (action) => ({ team_created: "\u{1F3AC}", task_assigned: "\u{1F4CB}", task_updated: "\u{1F504}", task_started: "\u{1F680}", member_session_ready: "\u{1F50C}", task_settled: "\u2705", task_failed: "\u274C", task_cancelled: "\u26D4", task_interrupted: "\u23F8", task_resumed: "\u25B6\uFE0F", task_followup: "\u{1F4AC}", task_reassigned: "\u{1F501}", task_retry: "\u267B\uFE0F" })[action ?? ""] ?? "\u2022";
25299
+ function renderBoard(payload, openThread, snapshots = [], userAction) {
25230
25300
  const board = el("main", "harness-mix-team-board harness-mix-team-body", "height:100%;min-height:0;color:inherit");
25231
25301
  const lead = payload.lead ?? { id: "lead", name: "Team Lead", role: "\u534F\u8C03\u4E0E\u9A8C\u6536", agent: "codex", display_status: "working" };
25232
25302
  const tasksOf = (memberId) => payload.tasks.filter((task) => task.assignee === memberId);
@@ -25249,12 +25319,11 @@ ${error51.stderrTail}`] : []
25249
25319
  };
25250
25320
  const memberCard = (member, index) => {
25251
25321
  const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter((task) => task.status === "completed").length;
25252
- const button = el("button", "harness-mix-team-member", "appearance:none;width:100%;box-sizing:border-box;border:1px solid color-mix(in srgb,currentColor 10%,transparent);background:color-mix(in srgb,Canvas 92%,transparent);box-shadow:0 2px 8px color-mix(in srgb,#000 6%,transparent);color:inherit;font:inherit;display:grid;grid-template-columns:40px minmax(0,1fr);align-items:center;gap:9px;padding:9px 10px;border-radius:12px;text-align:left;cursor:default");
25253
- button.type = "button";
25254
- button.dataset.agent = member.agent;
25255
- button.dataset.status = member.display_status ?? "";
25256
- button.style.setProperty("--lane-color", color ?? "");
25257
- wireOpen(button, member.childId ?? member.child_thread_id, member.name, openThread);
25322
+ const card = el("div", "harness-mix-team-member", "position:relative;width:100%;box-sizing:border-box;border:1px solid color-mix(in srgb,currentColor 10%,transparent);background:color-mix(in srgb,Canvas 92%,transparent);box-shadow:0 2px 8px color-mix(in srgb,#000 6%,transparent);color:inherit;font:inherit;display:grid;grid-template-columns:40px minmax(0,1fr);align-items:center;gap:9px;padding:9px 10px;border-radius:12px;text-align:left;cursor:default");
25323
+ card.dataset.agent = member.agent;
25324
+ card.dataset.status = member.display_status ?? "";
25325
+ card.style.setProperty("--lane-color", color ?? "");
25326
+ wireOpen(card, member.childId ?? member.child_thread_id, member.name, openThread);
25258
25327
  const avatar = el("span", "harness-mix-team-avatar", `position:relative;display:grid;place-items:center;width:40px;height:40px;flex:none;border-radius:11px;border:2px solid ${color};background:Canvas`);
25259
25328
  avatar.dataset.status = member.display_status ?? "";
25260
25329
  const dot = el("span", "harness-mix-team-status-dot", `position:absolute;right:-4px;bottom:-4px;width:11px;height:11px;box-sizing:border-box;border-radius:50%;background:${stateColor(member.display_status)};border:2.5px solid Canvas`);
@@ -25264,6 +25333,7 @@ ${error51.stderrTail}`] : []
25264
25333
  copy.append(text("strong", member.name, "font-size:12px;font-weight:680;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"));
25265
25334
  const status = el("span", void 0, "display:flex;align-items:center;gap:5px;min-width:0");
25266
25335
  status.append(text("span", stateLabel(member.display_status), `font-size:9px;color:${stateColor(member.display_status)};white-space:nowrap`), text("span", `${done}/${assigned.length}`, "font-size:9px;font-weight:650;opacity:.5;margin-left:auto"));
25336
+ if (member.unread) status.append(text("span", `\u{1F4AC}${member.unread}`, "font-size:8.5px;font-weight:700;color:#2878e3;background:color-mix(in srgb,#2878e3 12%,transparent);border-radius:99px;padding:1px 6px;flex:none"));
25267
25337
  copy.append(status);
25268
25338
  const progress = el("progress");
25269
25339
  progress.max = Math.max(1, assigned.length);
@@ -25271,8 +25341,26 @@ ${error51.stderrTail}`] : []
25271
25341
  progress.style.cssText = `width:100%;height:4px;margin:0;accent-color:${color}`;
25272
25342
  progress.title = `${done}/${assigned.length} \u4E2A\u4EFB\u52A1\u5B8C\u6210`;
25273
25343
  copy.append(progress);
25274
- button.append(avatar, copy);
25275
- return button;
25344
+ card.append(avatar, copy);
25345
+ if (userAction && payload.lead_thread_id) {
25346
+ const askRow = el("span", void 0, "position:absolute;top:5px;right:6px;display:flex;align-items:center;gap:4px;max-width:72%");
25347
+ const askButton = actionButton("\u8FFD\u95EE", `\u4EE5\u4E3B\u5BFC\u8005\u8EAB\u4EFD\u5411 ${member.name} \u53D1\u9001\u56E2\u961F\u6D88\u606F`, () => {
25348
+ const input = el("input", void 0, "font:600 10px system-ui;padding:3px 6px;border-radius:6px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);color:inherit;background:Canvas;min-width:96px;flex:1");
25349
+ input.placeholder = `\u5411 ${member.name} \u8BF4\u2026`;
25350
+ const send = actionButton("\u53D1\u9001", "\u53D1\u9001\u56E2\u961F\u6D88\u606F", () => {
25351
+ const message = input.value.trim();
25352
+ if (message) void runUserAction(userAction, send, { action: "message/send", threadId: payload.lead_thread_id, teamId: payload.team_id, to: member.id, message });
25353
+ });
25354
+ input.addEventListener("keydown", (event) => {
25355
+ if (event.key === "Enter") send.click();
25356
+ });
25357
+ askRow.replaceChildren(input, send, actionButton("\xD7", "\u53D6\u6D88", () => askRow.replaceChildren(askButton)));
25358
+ input.focus();
25359
+ });
25360
+ askRow.append(askButton);
25361
+ card.append(askRow);
25362
+ }
25363
+ return card;
25276
25364
  };
25277
25365
  const laneOf = (member, index) => {
25278
25366
  const color = memberColor(index), assigned = tasksOf(member.id), done = assigned.filter((task) => task.status === "completed").length;
@@ -25302,6 +25390,35 @@ ${error51.stderrTail}`] : []
25302
25390
  title.title = task.title;
25303
25391
  card.append(topRow, title);
25304
25392
  if ((task.dependsOn?.length ?? 0) > 0) card.append(text("div", `\u4F9D\u8D56 ${task.dependsOn.length} \u9879\u4EFB\u52A1`, "font-size:8px;opacity:.45"));
25393
+ if (userAction && payload.lead_thread_id) {
25394
+ const assignee = payload.members.find((member2) => member2.id === task.assignee);
25395
+ const row = el("div", void 0, "display:flex;align-items:center;gap:5px;flex-wrap:wrap;min-width:0");
25396
+ const renderDefault = () => {
25397
+ row.replaceChildren();
25398
+ if (task.status === "in_progress") {
25399
+ row.append(actionButton("\u53D6\u6D88", "\u53D6\u6D88\u8BE5\u4EFB\u52A1\u5E76\u505C\u6B62\u6210\u5458\u4F1A\u8BDD", () => {
25400
+ if (window.confirm(`\u53D6\u6D88\u4EFB\u52A1\u300C${task.title}\u300D\uFF1F\u8FD0\u884C\u4E2D\u7684\u6210\u5458\u4F1A\u8BDD\u5C06\u88AB\u505C\u6B62\u3002`)) void runUserAction(userAction, row, { action: "task/cancel", threadId: payload.lead_thread_id, teamId: payload.team_id, taskId: task.id });
25401
+ }, "#d14343"));
25402
+ } else if (task.status === "failed" || task.status === "interrupted") {
25403
+ row.append(
25404
+ actionButton(task.status === "failed" ? "\u91CD\u8BD5" : "\u6062\u590D", `\u628A\u4EFB\u52A1\u91CD\u65B0\u6D3E\u7ED9 ${assignee?.name ?? "\u539F\u6210\u5458"}`, () => {
25405
+ void runUserAction(userAction, row, { action: "task/reassign", threadId: payload.lead_thread_id, teamId: payload.team_id, taskId: task.id, memberId: task.assignee });
25406
+ }, "#c17022"),
25407
+ actionButton("\u6539\u6D3E", "\u6539\u6D3E\u7ED9\u5176\u4ED6\u6210\u5458", () => {
25408
+ const select = el("select", void 0, "font:600 9px system-ui;padding:3px 4px;border-radius:6px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);color:inherit;background:Canvas;max-width:120px;min-width:0");
25409
+ select.append(new Option("\u6539\u6D3E\u7ED9\u2026", ""));
25410
+ for (const candidate of payload.members) if (candidate.id !== task.assignee) select.append(new Option(candidate.name, candidate.id));
25411
+ const go = actionButton("\u786E\u5B9A", "\u6267\u884C\u6539\u6D3E", () => {
25412
+ if (select.value) void runUserAction(userAction, row, { action: "task/reassign", threadId: payload.lead_thread_id, teamId: payload.team_id, taskId: task.id, memberId: select.value });
25413
+ });
25414
+ row.replaceChildren(select, go, actionButton("\xD7", "\u53D6\u6D88\u6539\u6D3E", renderDefault));
25415
+ })
25416
+ );
25417
+ }
25418
+ };
25419
+ renderDefault();
25420
+ if (row.childElementCount) card.append(row);
25421
+ }
25305
25422
  list.append(card);
25306
25423
  }
25307
25424
  if (!assigned.length) list.append(text("div", "\u7B49\u5F85\u4E3B\u5BFC\u8005\u5206\u914D\u4EFB\u52A1", "font-size:9px;opacity:.42;padding:10px 2px;text-align:center"));
@@ -25350,7 +25467,7 @@ ${error51.stderrTail}`] : []
25350
25467
  board.append(left, feed);
25351
25468
  return board;
25352
25469
  }
25353
- function renderSummary(payload, open, openThread) {
25470
+ function renderSummary(payload, open, openThread, userAction) {
25354
25471
  const panel = el("section", "harness-mix-team-panel", 'box-sizing:border-box;border:1px solid color-mix(in srgb,currentColor 12%,transparent);border-radius:14px;background:color-mix(in srgb,Canvas 90%,transparent);box-shadow:0 8px 28px color-mix(in srgb,#000 7%,transparent);backdrop-filter:blur(18px);color:inherit;font:13px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;padding:12px 14px;display:grid;gap:10px;min-width:0');
25355
25472
  panel.dataset.teamId = payload.team_id;
25356
25473
  panel.dataset.teamStatus = payload.status;
@@ -25370,6 +25487,7 @@ ${error51.stderrTail}`] : []
25370
25487
  button.textContent = "\u5C55\u5F00\u8BE6\u60C5";
25371
25488
  button.addEventListener("click", open);
25372
25489
  top.append(identity, metrics(payload), button);
25490
+ if (userAction && payload.lead_thread_id && payload.tasks.some((task) => task.status === "interrupted")) top.append(continueButton(payload, userAction));
25373
25491
  const members = el("div", "harness-mix-team-summary-members harness-mix-team-scroll", "display:flex;align-items:center;gap:6px;min-width:0;overflow-x:auto;padding-bottom:1px");
25374
25492
  payload.members.forEach((member, index) => {
25375
25493
  const color = memberColor(index);
@@ -25386,6 +25504,7 @@ ${error51.stderrTail}`] : []
25386
25504
  icon2.append(collaborationIcon(member.agent, member.name, 18), dot);
25387
25505
  const label = el("span", void 0, "display:grid;min-width:0;line-height:1.2");
25388
25506
  label.append(text("strong", member.name, "font-size:10px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"), text("span", member.role, "font-size:8.5px;opacity:.52;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"), text("span", stateLabel(member.display_status), `font-size:8.5px;color:${stateColor(member.display_status)}`));
25507
+ if (member.unread) label.append(text("span", `\u{1F4AC} ${member.unread} \u6761\u672A\u8BFB`, "font-size:8.5px;font-weight:700;color:#2878e3"));
25389
25508
  chip.append(icon2, label);
25390
25509
  members.append(chip);
25391
25510
  });
@@ -25463,7 +25582,8 @@ ${error51.stderrTail}`] : []
25463
25582
  collapse.textContent = "\u6536\u8D77\u8BE6\u60C5";
25464
25583
  collapse.addEventListener("click", close);
25465
25584
  timeline.append(event, range, live, play, collapse);
25466
- header.append(title, statsWrap, progressWrap, timeline);
25585
+ const headerActions = el("div", void 0, "display:flex;align-items:center;gap:6px;flex:none");
25586
+ header.append(title, statsWrap, progressWrap, timeline, headerActions);
25467
25587
  const content = el("div", void 0, "min-height:0");
25468
25588
  workbench.append(header, content);
25469
25589
  source.after(workbench);
@@ -25478,13 +25598,14 @@ ${error51.stderrTail}`] : []
25478
25598
  content.replaceChildren(renderBoard(visible, async (threadId) => {
25479
25599
  close();
25480
25600
  await options.openThread?.(threadId);
25481
- }, feedSnapshots));
25601
+ }, feedSnapshots, options.userAction));
25482
25602
  teamName.textContent = visible.name;
25483
25603
  subtitle.textContent = visible.goal;
25484
25604
  subtitle.title = visible.goal;
25485
25605
  teamPill.textContent = stateLabel(visible.status);
25486
25606
  teamPill.style.cssText = pillCss(visible.status);
25487
25607
  statsWrap.replaceChildren(...headerStats(visible));
25608
+ headerActions.replaceChildren(...options.userAction && visible.tasks.some((task) => task.status === "interrupted") ? [continueButton(visible, options.userAction)] : []);
25488
25609
  const completion = completionOf(visible);
25489
25610
  progressBar.max = Math.max(1, completion.total);
25490
25611
  progressBar.value = completion.done;
@@ -25550,7 +25671,7 @@ ${error51.stderrTail}`] : []
25550
25671
  if ([...candidate.querySelectorAll(selector)].some((child) => parseTeamPayload(child.textContent ?? ""))) continue;
25551
25672
  const signature = `${payload.updated_at ?? 0}:${payload.tasks.length}:${payload.messages.length}:${payload.members.map((member) => member.display_status).join(",")}`;
25552
25673
  if (signatures.get(candidate) === signature) continue;
25553
- const panel = renderSummary(payload, () => open(payload), options.openThread);
25674
+ const panel = renderSummary(payload, () => open(payload), options.openThread, options.userAction);
25554
25675
  if (candidate.tagName === "PRE") {
25555
25676
  if (!candidate.dataset.harnessMixTeamDisplay) candidate.dataset.harnessMixTeamDisplay = candidate.style.display || "__empty__";
25556
25677
  candidate.style.display = "none";
@@ -25600,7 +25721,7 @@ ${error51.stderrTail}`] : []
25600
25721
  if (activePanel?.isConnected && activeSignature === signature && activeThreadId === context.threadId) return;
25601
25722
  const reopen = workbench?.dataset.teamId === payload.team_id && workbench.dataset.teamSource === "active-thread";
25602
25723
  removeActivePanel();
25603
- activePanel = renderSummary(payload, () => open(payload), options.openThread);
25724
+ activePanel = renderSummary(payload, () => open(payload), options.openThread, options.userAction);
25604
25725
  activePanel.classList.add("harness-mix-team-launcher");
25605
25726
  activePanel.dataset.teamSource = "active-thread";
25606
25727
  activePanel.style.cssText += ";margin:8px 16px 0;flex:none;position:relative;z-index:11";
@@ -32795,6 +32916,10 @@ ${pet_market_default}`;
32795
32916
  applyWorkspace: async (threadId, digest) => {
32796
32917
  const client = modelClientForHost("local");
32797
32918
  return client?.applyThreadWorkspace?.({ threadId, digest }) ?? { patch: "", digest: "" };
32919
+ },
32920
+ continueCollab: async (threadId, taskId) => {
32921
+ const client = modelClientForHost("local");
32922
+ return client?.collaborationUserAction?.({ action: "continue", threadId, ...taskId ? { taskId } : {} });
32798
32923
  }
32799
32924
  });
32800
32925
  const teamCards = installTeamCards({
@@ -32802,6 +32927,10 @@ ${pet_market_default}`;
32802
32927
  const client = modelClientForHost("local");
32803
32928
  return client?.inspectThreadTeam?.({ threadId, ...teamId ? { teamId } : {} }) ?? null;
32804
32929
  },
32930
+ userAction: async (input) => {
32931
+ const client = modelClientForHost("local");
32932
+ return client?.collaborationUserAction?.(input) ?? null;
32933
+ },
32805
32934
  openThread: (threadId) => openRendererThread(hostThreadIdSchema.parse(threadId), { hostId: "local" }),
32806
32935
  activeThread: () => {
32807
32936
  const mounted = [...mountedByComposer.values()].find((candidate) => {
@@ -32991,14 +33120,15 @@ ${pet_market_default}`;
32991
33120
  const refreshCommands = async (mounted) => {
32992
33121
  const generation = ++mounted.commandRequestGeneration;
32993
33122
  const agent = controller.get(mounted.composer).agent;
32994
- const hostId = threadIdFromComposerModelTarget(mounted.modelTarget) ? mounted.hostId : activeModelHostId();
33123
+ const threadId = threadIdFromComposerModelTarget(mounted.modelTarget);
33124
+ const hostId = threadId ? mounted.hostId : activeModelHostId();
32995
33125
  const requestControl = modelControl;
32996
33126
  const client = modelClientForHostFrom(requestControl, hostId);
32997
33127
  mounted.control.harnessCommands.setCommands([]);
32998
33128
  if (agent === "codex" || !client) return;
32999
33129
  try {
33000
- const catalog = await client.inspectHarnessCommands({ harnessId: externalHarnessIds[agent] });
33001
- if (disposed || mountedByComposer.get(mounted.composer) !== mounted || mounted.commandRequestGeneration !== generation || requestControl !== modelControl || (threadIdFromComposerModelTarget(mounted.modelTarget) ? mounted.hostId : activeModelHostId()) !== hostId || controller.get(mounted.composer).agent !== agent)
33130
+ const catalog = threadId ? await client.inspectThreadCommands({ threadId }) : await client.inspectHarnessCommands({ harnessId: externalHarnessIds[agent] });
33131
+ if (disposed || mountedByComposer.get(mounted.composer) !== mounted || mounted.commandRequestGeneration !== generation || requestControl !== modelControl || threadIdFromComposerModelTarget(mounted.modelTarget) !== threadId || (threadId ? mounted.hostId : activeModelHostId()) !== hostId || controller.get(mounted.composer).agent !== agent)
33002
33132
  return;
33003
33133
  mounted.control.harnessCommands.setCommands(
33004
33134
  catalog.commands,
@@ -33358,7 +33488,8 @@ ${pet_market_default}`;
33358
33488
  }
33359
33489
  if (isDraft) mounted.hostId = requestHostId;
33360
33490
  const availability = hostHarnessAvailabilityState(requestHostId).availability[agent];
33361
- if (availability !== "ready") {
33491
+ const lockedOwnership = mounted.ownershipStatus === "ready" && state.phase === "locked";
33492
+ if (availability !== "ready" && !lockedOwnership) {
33362
33493
  mounted.modelView = {
33363
33494
  status: adapterStatus.state !== "ready" || availability === "checking" ? "waitingForAdapter" : "error",
33364
33495
  thinkingSelectionSupported: false,
@@ -34654,7 +34785,25 @@ ${pet_market_default}`;
34654
34785
  const selections = connectedComposers().map((mounted) => ({
34655
34786
  composerId: mounted.composerId,
34656
34787
  agent: controller.get(mounted.composer).agent,
34657
- phase: controller.get(mounted.composer).phase
34788
+ phase: controller.get(mounted.composer).phase,
34789
+ // Diagnostic surface for live composer debugging: the raw external
34790
+ // model/permission views plus the controller-held selection refs.
34791
+ ownership: mounted.ownershipStatus,
34792
+ modelView: {
34793
+ status: mounted.modelView.status,
34794
+ ...mounted.modelView.catalog ? { catalogModels: mounted.modelView.catalog.models.map((m) => m.ref.id.slice(0, 24)) } : {},
34795
+ ...mounted.modelView.selected ? { selected: mounted.modelView.selected.id.slice(0, 24) } : {},
34796
+ ...mounted.modelView.error ? { error: mounted.modelView.error.slice(0, 160) } : {}
34797
+ },
34798
+ ...controller.get(mounted.composer).phase === "locked" && mounted.composerId ? {
34799
+ controllerModel: (() => {
34800
+ const agent = controller.get(mounted.composer).agent;
34801
+ if (agent === "codex") return void 0;
34802
+ const model = controller.modelForAgent(mounted.composer, agent);
34803
+ return model ? model.id.slice(0, 24) : void 0;
34804
+ })()
34805
+ } : {},
34806
+ permissionView: mounted.permissionModeView.status
34658
34807
  }));
34659
34808
  return {
34660
34809
  version: 2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-mix/cli",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "license": "(Apache-2.0 OR MIT)",
5
5
  "description": "Harness Mix native Codex UI bridge for Codex, Pi, Claude Code, DeepSeek Harness, Antigravity, CodeBuddy, Kiro CLI, Cursor CLI and other native coding harnesses.",
6
6
  "keywords": [
@@ -78,6 +78,7 @@
78
78
  "test:codebuddy-migration": "node scripts/codebuddy-migration-test.cjs",
79
79
  "e2e:native-acp": "node scripts/e2e-native-acp.cjs --live",
80
80
  "test:native-acp-depth": "node scripts/native-acp-depth-test.cjs",
81
+ "test:acp-image": "node scripts/acp-image-test.cjs",
81
82
  "test:kiro-cursor": "node scripts/kiro-cursor-adapters-test.cjs",
82
83
  "test:docs": "node scripts/docs-command-test.cjs",
83
84
  "typecheck:native-ui": "tsc -b src/native-ui/shared-contracts/tsconfig.json src/native-ui/desktop-control/tsconfig.json src/native-ui/renderer-extension/tsconfig.json --pretty false",
@@ -106,6 +107,7 @@
106
107
  "e2e:native:pi": "node scripts/e2e-native-host.cjs --live --harness=pi",
107
108
  "e2e:native:dsh": "node scripts/e2e-native-host.cjs --live --harness=deepseek-harness",
108
109
  "e2e:native:claude": "node scripts/e2e-native-host.cjs --live --harness=claude-code",
110
+ "e2e:zcode": "node scripts/zcode-live-probe.cjs",
109
111
  "start:codex": "node scripts/launch-codex.cjs",
110
112
  "check": "node scripts/check.cjs",
111
113
  "test:contracts": "node scripts/contracts-test.cjs",
@@ -123,7 +125,7 @@
123
125
  "test:core-concurrency": "node scripts/core-concurrency-test.cjs",
124
126
  "test:thread-title": "node scripts/thread-title-test.cjs",
125
127
  "test:handoff": "node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs",
126
- "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/codex-accounts-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs && node scripts/integrations-test.cjs && node scripts/storage-verification-test.cjs && node scripts/stuck-turn-test.cjs && node scripts/pi-cancel-race-test.cjs && node scripts/send-cancel-race-test.cjs && node scripts/delegation-await-test.cjs && node scripts/core-review-test.cjs && node scripts/jsonl-stdin-test.cjs && node scripts/send-pre-turn-cancel-test.cjs",
128
+ "test:core-all": "node scripts/contracts-test.cjs && node scripts/projector-test.cjs && node scripts/turn-manager-test.cjs && node scripts/sequence-test.cjs && node scripts/shadow-test.cjs && node scripts/architecture-test.cjs && node scripts/adapters-test.cjs && node scripts/dsh-adapter-test.cjs && node scripts/codex-adapter-test.cjs && node scripts/codex-accounts-test.cjs && node scripts/antigravity-adapter-test.cjs && node scripts/openclaw-adapter-test.cjs && node scripts/acp-image-test.cjs && node scripts/core-replay-test.cjs && node scripts/determinism-test.cjs && node scripts/core-runtime-test.cjs && node scripts/core-services-test.cjs && node scripts/native-file-change-test.cjs && node scripts/native-updater-test.cjs && node scripts/native-update-apply-test.cjs && node scripts/native-vendor-adapters-test.cjs && node scripts/core-concurrency-test.cjs && node scripts/concurrency-layers-test.cjs && node scripts/tool-concurrency-regression-test.cjs && node scripts/handoff-checkpoint-test.cjs && node scripts/switch-harness-test.cjs && node scripts/native-acp-depth-test.cjs && node scripts/thread-title-test.cjs && node scripts/native-job-object-test.cjs && node scripts/integrations-test.cjs && node scripts/storage-verification-test.cjs && node scripts/stuck-turn-test.cjs && node scripts/pi-cancel-race-test.cjs && node scripts/send-cancel-race-test.cjs && node scripts/delegation-await-test.cjs && node scripts/core-review-test.cjs && node scripts/zcode-adapter-test.cjs && node scripts/jsonl-stdin-test.cjs && node scripts/send-pre-turn-cancel-test.cjs && node scripts/collaboration-test.cjs && node scripts/collaboration-recovery-test.cjs",
127
129
  "test:codex-accounts": "node scripts/codex-accounts-test.cjs",
128
130
  "smoke:codex-accounts-ui": "electron scripts/codex-accounts-ui-smoke.cjs",
129
131
  "test:transcript": "node scripts/transcript-test.cjs",
@@ -136,6 +138,7 @@
136
138
  "e2e:dsh": "node scripts/e2e-dsh.cjs",
137
139
  "e2e:openclaw": "node scripts/e2e-openclaw.cjs",
138
140
  "e2e:openclaw:image": "node scripts/e2e-openclaw-image.cjs",
141
+ "e2e:hermes:image": "node scripts/e2e-hermes-image.cjs",
139
142
  "e2e:codex": "node scripts/e2e-codex.cjs",
140
143
  "e2e:delegate": "node scripts/e2e-delegate.cjs",
141
144
  "test:determinism": "node scripts/determinism-test.cjs",
@@ -171,7 +174,8 @@
171
174
  "test:delegation-await": "node scripts/delegation-await-test.cjs",
172
175
  "test:core-review": "node scripts/core-review-test.cjs",
173
176
  "test:jsonl-stdin": "node scripts/jsonl-stdin-test.cjs",
174
- "test:send-pre-turn-cancel": "node scripts/send-pre-turn-cancel-test.cjs"
177
+ "test:send-pre-turn-cancel": "node scripts/send-pre-turn-cancel-test.cjs",
178
+ "test:zcode-adapter": "node scripts/zcode-adapter-test.cjs"
175
179
  },
176
180
  "devDependencies": {
177
181
  "@types/node": "24.13.3",
@@ -187,11 +191,11 @@
187
191
  "zod": "4.4.3"
188
192
  },
189
193
  "optionalDependencies": {
190
- "@harness-mix/native-win32-x64": "0.2.2",
191
- "@harness-mix/native-win32-arm64": "0.2.2",
192
- "@harness-mix/native-darwin-x64": "0.2.2",
193
- "@harness-mix/native-darwin-arm64": "0.2.2",
194
- "@harness-mix/native-linux-x64": "0.2.2",
195
- "@harness-mix/native-linux-arm64": "0.2.2"
194
+ "@harness-mix/native-win32-x64": "0.2.4",
195
+ "@harness-mix/native-win32-arm64": "0.2.4",
196
+ "@harness-mix/native-darwin-x64": "0.2.4",
197
+ "@harness-mix/native-darwin-arm64": "0.2.4",
198
+ "@harness-mix/native-linux-x64": "0.2.4",
199
+ "@harness-mix/native-linux-arm64": "0.2.4"
196
200
  }
197
201
  }
@@ -0,0 +1,69 @@
1
+ const assert = require('node:assert/strict');
2
+ const { acpAdapter } = require('../src/main/adapters/acp');
3
+ const { randomUUID } = require('node:crypto');
4
+
5
+ // acpAdapter(Hermes 所在 ACP 家族工厂)图片附件路径单元测试:
6
+ // initialize 实测声明 image 支持 → session/prompt 携带 {type:'image',data,mimeType} 块;
7
+ // 声明不支持 → send 直接拒绝("原生 ACP 不支持图片"),不静默丢图。
8
+ // fixture 子进程模式仿 native-acp-depth-test.cjs。
9
+
10
+ if (process.argv.includes('--fixture')) {
11
+ const write = value => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\n');
12
+ const noImages = process.argv.includes('--no-images');
13
+ let sid, sawImageBlock = null;
14
+ require('node:readline').createInterface({ input: process.stdin }).on('line', line => {
15
+ const r = JSON.parse(line);
16
+ let result = {};
17
+ if (r.method === 'initialize') result = { agentCapabilities: { loadSession: true, promptCapabilities: { image: !noImages } } };
18
+ if (r.method === 'session/new' || r.method === 'session/load') {
19
+ sid = randomUUID();
20
+ result = { sessionId: sid, configOptions: [{ id: 'model', currentValue: 'native-model', options: [{ value: 'native-model', name: 'Native' }] }] };
21
+ }
22
+ if (r.method === 'session/prompt') {
23
+ if (r.params.prompt[0]?.text === 'image') {
24
+ sawImageBlock = r.params.prompt[1];
25
+ // 形状断言在 fixture 内完成:不匹配直接以错误回包,让适配器侧 reject
26
+ const ok = JSON.stringify(sawImageBlock) === JSON.stringify({ type: 'image', data: 'AA==', mimeType: 'image/png' });
27
+ if (!ok) { write({ id: r.id, error: { message: `image block shape mismatch: ${JSON.stringify(sawImageBlock)}` } }); return; }
28
+ }
29
+ result = { stopReason: 'end_turn', userMessageId: 'fixture-user' };
30
+ }
31
+ if (r.id !== undefined) write({ id: r.id, result });
32
+ if (r.method === 'session/prompt' && sawImageBlock) process.stderr.write(`IMAGE_BLOCK_OK:${JSON.stringify(sawImageBlock)}\n`);
33
+ });
34
+ } else {
35
+ (async () => {
36
+ const build = () => acpAdapter({
37
+ id: 'hermes', name: 'Hermes', bin: 'hermes-fixture', executable: true, args: ['acp'],
38
+ images: true, fork: true, thinking: false, permissions: false, questions: false, compaction: false, usage: false, contextUsage: false,
39
+ resolveCommand: () => ({ command: process.execPath, args: [__filename, '--fixture'] }),
40
+ }).create();
41
+
42
+ // 1. 握手声明 image 支持 → 图片块按 ACP 形状进入 session/prompt
43
+ const adapter = build();
44
+ const session = await adapter.open({ thread: { cwd: process.cwd() }, emit: () => {} });
45
+ try {
46
+ assert.equal(session.state.agentCapabilities?.promptCapabilities?.image, true, '握手能力必须被采集');
47
+ await adapter.send(session, 'image', { emit: () => {} }, { images: [{ data: 'AA==', mime: 'image/png' }] });
48
+ } finally { await adapter.close(session); }
49
+
50
+ // 2. 纯文本回合不受影响
51
+ const textAdapter = build();
52
+ const textSession = await textAdapter.open({ thread: { cwd: process.cwd() }, emit: () => {} });
53
+ try { await textAdapter.send(textSession, 'plain', { emit: () => {} }); }
54
+ finally { await textAdapter.close(textSession); }
55
+
56
+ // 3. 旧版上游(initialize 不声明 image)→ send 拒绝而不是静默丢图
57
+ const old = acpAdapter({
58
+ id: 'hermes', name: 'Hermes', bin: 'hermes-fixture', executable: true, args: ['acp'],
59
+ images: true, fork: true, thinking: false, permissions: false, questions: false, compaction: false, usage: false, contextUsage: false,
60
+ resolveCommand: () => ({ command: process.execPath, args: [__filename, '--fixture', '--no-images'] }),
61
+ }).create();
62
+ const oldSession = await old.open({ thread: { cwd: process.cwd() }, emit: () => {} });
63
+ try {
64
+ await assert.rejects(old.send(oldSession, 'image', { emit: () => {} }, { images: [{ data: 'AA==', mime: 'image/png' }] }), /Hermes 原生 ACP 不支持图片/);
65
+ } finally { await old.close(oldSession); }
66
+
67
+ console.log('acp image: prompt block shape, handshake gating and loud rejection PASS');
68
+ })().catch(error => { console.error(error); process.exitCode = 1; });
69
+ }
@@ -1,4 +1,5 @@
1
1
  // PR 5 验收:对注册表中全部 Adapter 执行同一组契约测试(无 Electron、不启动原生进程)。
2
+ const assert = require('node:assert/strict');
2
3
  const { buildAdapters } = require('../src/main/adapters');
3
4
  const { runAdapterContractTests } = require('../src/main/harness-adapter/contract-test');
4
5
  const { normalizeCapabilities } = require('../src/main/harness-adapter/manifest');
@@ -22,4 +23,28 @@ for (const adapter of adapters) {
22
23
  });
23
24
  }
24
25
 
26
+ // Pi 家族权限目录按成员原生面分开(诚实面):Pi=项目信任旗标,OMP=--approval-mode 三档。
27
+ // 实测依据:pi 0.84.2 `pi --help`(--approve/-a、--no-approve/-na);
28
+ // @oh-my-pi/pi-coding-agent 18.1.19 flag-tables.ts/settings-schema.ts(always-ask|write|yolo)。
29
+ {
30
+ const { PI_PERMISSION_MODES, OMP_APPROVAL_MODES, piPermissionLaunchArgs, ompPermissionLaunchArgs } = require('../src/main/adapters/pi-family');
31
+ test('[pi] 权限目录 = 项目信任三档(default/approve/no-approve),不携带 OMP 档', () => {
32
+ assert.deepEqual(PI_PERMISSION_MODES.map((m) => m.id), ['default', 'approve', 'no-approve']);
33
+ assert.deepEqual(piPermissionLaunchArgs('approve'), ['--approve']);
34
+ assert.deepEqual(piPermissionLaunchArgs('no-approve'), ['--no-approve']);
35
+ assert.deepEqual(piPermissionLaunchArgs('yolo'), [], 'Pi 不认 OMP 的档位 id');
36
+ });
37
+ test('[omp] 权限目录 = --approval-mode 三档(always-ask/write/yolo),不携带 Pi 旗标', () => {
38
+ assert.deepEqual(OMP_APPROVAL_MODES.map((m) => m.id), ['always-ask', 'write', 'yolo']);
39
+ for (const mode of ['always-ask', 'write', 'yolo']) assert.deepEqual(ompPermissionLaunchArgs(mode), ['--approval-mode', mode]);
40
+ assert.deepEqual(ompPermissionLaunchArgs('no-approve'), [], 'OMP 无 --no-approve 旗标,不虚构');
41
+ });
42
+ const omp = require('../src/main/adapters/omp');
43
+ const pi = require('../src/main/adapters/pi');
44
+ test('[pi/omp] 工厂注入各自的权限目录(describe 之外也可校验)', () => {
45
+ assert.deepEqual(pi.permissionModes.map((m) => m.id), ['default', 'approve', 'no-approve']);
46
+ assert.deepEqual(omp.permissionModes.map((m) => m.id), ['always-ask', 'write', 'yolo'], 'OMP 不再共用 Pi 的项目信任目录');
47
+ });
48
+ }
49
+
25
50
  console.log(`adapters: ${passed} passed${failed ? `, ${failed} failed` : ''} (${adapters.length} adapters)`);