@kontextmind/kxm 0.6.0 → 0.7.10

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 (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -1,4 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { win32 as win32Path } from "node:path";
4
+ import { defaultSpawn } from "./vnext-oneshot-process.ts";
2
5
 
3
6
  export const DEFAULT_HARNESS = "pi";
4
7
  export type HarnessMode = "headless" | "either";
@@ -27,6 +30,7 @@ export interface OneShotParsedOutput {
27
30
  text: string;
28
31
  usage?: OneShotUsage | undefined;
29
32
  outcome?: string | undefined;
33
+ effectiveModel?: string | undefined;
30
34
  isError?: boolean | undefined;
31
35
  errorMessage?: string | undefined;
32
36
  }
@@ -35,7 +39,7 @@ export interface HarnessOneShotConfig {
35
39
  argv: readonly string[];
36
40
  promptVia: OneShotPromptVia;
37
41
  outputFormat: "json" | "text" | "stream-json" | "jsonl";
38
- usageParser: (stdout: string, stderr: string) => OneShotParsedOutput;
42
+ usageParser: (stdout: string, stderr: string, requestedModel?: string) => OneShotParsedOutput;
39
43
  }
40
44
 
41
45
  export interface HarnessCatalogEntry {
@@ -54,10 +58,20 @@ export interface HarnessCatalogEntry {
54
58
  oneShot?: HarnessOneShotConfig | undefined;
55
59
  }
56
60
 
61
+ export type HarnessRunCommand = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult;
62
+ export type HarnessRunCommandAsync = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult | Promise<HarnessCommandResult>;
63
+
57
64
  export interface HarnessProbeOptions {
58
- env?: NodeJS.ProcessEnv;
59
- runCommand?: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult;
60
- timeoutMs?: number;
65
+ env?: NodeJS.ProcessEnv | undefined;
66
+ runCommand?: HarnessRunCommand | undefined;
67
+ timeoutMs?: number | undefined;
68
+ platform?: NodeJS.Platform | undefined;
69
+ existsSync?: ((path: string) => boolean) | undefined;
70
+ }
71
+
72
+ export interface AsyncHarnessProbeOptions extends Omit<HarnessProbeOptions, "runCommand"> {
73
+ signal?: AbortSignal | undefined;
74
+ runCommand?: HarnessRunCommandAsync | undefined;
61
75
  }
62
76
 
63
77
  export interface HarnessDispatchStatus {
@@ -73,6 +87,7 @@ export interface HarnessStatus {
73
87
  mode: HarnessMode;
74
88
  detected: boolean;
75
89
  authenticated: boolean | null;
90
+ authMethod?: "claude.ai" | "ChatGPT" | "api-key" | "antigravity-oauth" | undefined;
76
91
  dispatch?: HarnessDispatchStatus | undefined;
77
92
  command?: string;
78
93
  version?: string;
@@ -85,10 +100,10 @@ export interface HarnessInventory {
85
100
  harnesses: readonly HarnessStatus[];
86
101
  }
87
102
 
88
- const UNKNOWN_AUTH_HARNESSES = new Set(["gemini", "deepseek"]);
103
+ const UNKNOWN_AUTH_HARNESSES = new Set(["deepseek"]);
89
104
  const GROK_LOGIN_LINE = "You are logged in with grok.com.";
90
105
  /** Tab-separated `id<TAB>label` rows from the committed `agy models` probe. */
91
- const AGY_MODEL_ROW = /^[a-z0-9][a-z0-9.+_-]*\t+\S/im;
106
+ const AGY_MODEL_ROW = /^[a-z0-9]+(?:[._-][a-z0-9]+)+\s+\S/im;
92
107
  const CODEX_CHATGPT_LINE = "Logged in using ChatGPT";
93
108
  const CODEX_API_KEY_PREFIX = "Logged in using an API key";
94
109
  const CODEX_NEGATIVE_LINE = "Not logged in";
@@ -99,7 +114,6 @@ export const NATIVE_HARNESS_PROVIDERS: Readonly<Record<string, string>> = Object
99
114
  codex: "openai",
100
115
  grok: "xai",
101
116
  agy: "google",
102
- gemini: "google",
103
117
  kimi: "moonshot",
104
118
  deepseek: "deepseek",
105
119
  });
@@ -143,7 +157,11 @@ export interface HarnessUpdateStep {
143
157
  detail?: string;
144
158
  }
145
159
 
146
- export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShotParsedOutput {
160
+ function reportedModelId(value: unknown): string | undefined {
161
+ return typeof value === "string" && /^[a-z0-9][a-z0-9._:/-]{0,199}$/i.test(value) ? value : undefined;
162
+ }
163
+
164
+ export function parseClaudeOneShotUsage(stdout: string, stderr: string, requestedModel?: string): OneShotParsedOutput {
147
165
  const trimmed = stdout.trim();
148
166
  if (!trimmed) {
149
167
  return {
@@ -155,26 +173,28 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
155
173
  try {
156
174
  const payload = JSON.parse(trimmed) as Record<string, unknown>;
157
175
  const usage = (payload.usage as Record<string, unknown> | undefined) ?? {};
158
- let modelUsageDetail: Record<string, unknown> | undefined;
159
- if (payload.modelUsage && typeof payload.modelUsage === "object") {
160
- const values = Object.values(payload.modelUsage as Record<string, unknown>);
161
- if (values.length > 0 && values[0] && typeof values[0] === "object") {
162
- modelUsageDetail = values[0] as Record<string, unknown>;
163
- }
164
- }
165
- const tokensIn = (typeof modelUsageDetail?.inputTokens === "number" ? modelUsageDetail.inputTokens : undefined)
166
- ?? (typeof usage.input_tokens === "number" ? usage.input_tokens : null);
167
- const tokensOut = (typeof modelUsageDetail?.outputTokens === "number" ? modelUsageDetail.outputTokens : undefined)
168
- ?? (typeof usage.output_tokens === "number" ? usage.output_tokens : null);
169
- const cacheReadTokens = (typeof modelUsageDetail?.cacheReadInputTokens === "number" ? modelUsageDetail.cacheReadInputTokens : undefined)
170
- ?? (typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : null);
171
- const cacheWriteTokens = (typeof modelUsageDetail?.cacheCreationInputTokens === "number" ? modelUsageDetail.cacheCreationInputTokens : undefined)
172
- ?? (typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : null);
176
+ const entries = payload.modelUsage && typeof payload.modelUsage === "object" && !Array.isArray(payload.modelUsage)
177
+ ? Object.entries(payload.modelUsage as Record<string, unknown>).filter(([, value]) => value && typeof value === "object") : [];
178
+ const matches = entries.filter(([model]) => model === requestedModel
179
+ || (requestedModel && (model.startsWith(`${requestedModel}-`) || model.startsWith(`claude-${requestedModel}-`))));
180
+ const selected = matches.length === 1 ? matches[0] : entries.length === 1 ? entries[0] : undefined;
181
+ const modelUsageDetail = selected?.[1] as Record<string, unknown> | undefined;
182
+ const effectiveModel = reportedModelId(payload.model) ?? reportedModelId(selected?.[0]);
183
+ // Top-level usage is the attempt total; an auxiliary model is not the task's model.
184
+ const tokensIn = (typeof usage.input_tokens === "number" ? usage.input_tokens : undefined)
185
+ ?? (typeof modelUsageDetail?.inputTokens === "number" ? modelUsageDetail.inputTokens : null);
186
+ const tokensOut = (typeof usage.output_tokens === "number" ? usage.output_tokens : undefined)
187
+ ?? (typeof modelUsageDetail?.outputTokens === "number" ? modelUsageDetail.outputTokens : null);
188
+ const cacheReadTokens = (typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined)
189
+ ?? (typeof modelUsageDetail?.cacheReadInputTokens === "number" ? modelUsageDetail.cacheReadInputTokens : null);
190
+ const cacheWriteTokens = (typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined)
191
+ ?? (typeof modelUsageDetail?.cacheCreationInputTokens === "number" ? modelUsageDetail.cacheCreationInputTokens : null);
173
192
  const costUsd = typeof payload.total_cost_usd === "number"
174
193
  ? payload.total_cost_usd
175
194
  : (typeof modelUsageDetail?.costUSD === "number" ? modelUsageDetail.costUSD : null);
176
195
 
177
- const isError = Boolean(payload.is_error || payload.error);
196
+ const isError = Boolean(payload.is_error || payload.error
197
+ || ["error", "aborted", "failed", "max_tokens", "length"].includes(String(payload.stopReason ?? payload.stop_reason ?? "")));
178
198
  const text = typeof payload.result === "string" ? payload.result : (typeof payload.text === "string" ? payload.text : "");
179
199
  const errorMessage = isError
180
200
  ? (typeof payload.error === "string"
@@ -186,6 +206,7 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
186
206
 
187
207
  return {
188
208
  text,
209
+ effectiveModel,
189
210
  isError,
190
211
  errorMessage,
191
212
  usage: {
@@ -193,18 +214,25 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
193
214
  tokensOut,
194
215
  cacheReadTokens,
195
216
  cacheWriteTokens,
196
- contextTokens: tokensIn,
217
+ contextTokens: null,
197
218
  costUsd,
198
219
  },
199
220
  };
200
221
  } catch {
201
222
  return {
202
223
  text: trimmed,
224
+ isError: true,
225
+ errorMessage: "invalid Claude JSON response",
203
226
  usage: {},
204
227
  };
205
228
  }
206
229
  }
207
230
 
231
+ /** Grok's observed one-shot JSON uses the same usage/modelUsage envelope. */
232
+ export function parseGrokOneShotUsage(stdout: string, stderr: string, requestedModel?: string): OneShotParsedOutput {
233
+ return parseClaudeOneShotUsage(stdout, stderr, requestedModel);
234
+ }
235
+
208
236
  export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotParsedOutput {
209
237
  const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
210
238
  if (lines.length === 0) {
@@ -238,7 +266,7 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
238
266
  ? ((ev.error as Record<string, unknown>).message as string)
239
267
  : "turn_failed");
240
268
  }
241
- if (ev.item && typeof ev.item === "object" && (ev.item as Record<string, unknown>).type === "agent_message") {
269
+ if (ev.type === "item.completed" && ev.item && typeof ev.item === "object" && (ev.item as Record<string, unknown>).type === "agent_message") {
242
270
  const t = (ev.item as Record<string, unknown>).text;
243
271
  if (typeof t === "string") messageTexts.push(t);
244
272
  }
@@ -247,7 +275,8 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
247
275
  }
248
276
  }
249
277
 
250
- const text = messageTexts.join("\n").trim();
278
+ const text = (messageTexts.at(-1) ?? "").trim();
279
+ if (!events.some((event) => event.type === "turn.completed") || !text) hasError = true;
251
280
  const tokensIn = typeof usage?.input_tokens === "number" ? usage.input_tokens : null;
252
281
  const tokensOut = typeof usage?.output_tokens === "number" ? usage.output_tokens : null;
253
282
  const cacheReadTokens = typeof usage?.cached_input_tokens === "number" ? usage.cached_input_tokens : null;
@@ -262,7 +291,7 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
262
291
  tokensOut,
263
292
  cacheReadTokens,
264
293
  cacheWriteTokens,
265
- contextTokens: tokensIn,
294
+ contextTokens: null,
266
295
  costUsd: null,
267
296
  },
268
297
  };
@@ -279,7 +308,15 @@ export function parseGenericOneShotUsage(stdout: string, _stderr: string): OneSh
279
308
  : (typeof rec.text === "string"
280
309
  ? rec.text
281
310
  : (typeof rec.response === "string" ? rec.response : trimmed));
282
- return { text, usage: {} };
311
+ const u = rec.usage && typeof rec.usage === "object" ? rec.usage as Record<string, unknown> : {};
312
+ return { text, effectiveModel: reportedModelId(rec.model), isError: Boolean(rec.is_error || rec.error || rec.success === false), usage: {
313
+ tokensIn: typeof u.input_tokens === "number" ? u.input_tokens : null,
314
+ tokensOut: typeof u.output_tokens === "number" ? u.output_tokens : null,
315
+ cacheReadTokens: typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : null,
316
+ cacheWriteTokens: typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : null,
317
+ contextTokens: null,
318
+ costUsd: typeof rec.total_cost_usd === "number" ? rec.total_cost_usd : null,
319
+ } };
283
320
  }
284
321
  } catch {
285
322
  // not json
@@ -381,6 +418,18 @@ export function parseAgyOneShotUsage(stdout: string, _stderr: string): OneShotPa
381
418
  }
382
419
 
383
420
  /** Built-in harnesses. Unknown ids fail closed. Model lists live in `.kxm/models/*.yaml`, not here. */
421
+ const READ_ONLY_ONESHOT_ARGS = Object.freeze({
422
+ claude: Object.freeze(["--tools", "Read,Glob,Grep", "--restricted", "--safe-mode", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands", "--no-session-persistence"]),
423
+ codex: Object.freeze(["--sandbox", "read-only", "--ignore-user-config", "-c", 'approval_policy="never"']),
424
+ grok: Object.freeze(["--sandbox", "read-only", "--permission-mode", "plan", "--tools", "Read,Glob,Grep", "--no-subagents", "--disable-web-search"]),
425
+ agy: Object.freeze(["--mode", "plan", "--sandbox", "--disable-slash-commands"]),
426
+ kimi: Object.freeze(["--plan"]),
427
+ });
428
+
429
+ export function oneShotReadOnlyArgs(harness: string): readonly string[] | undefined {
430
+ return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness as keyof typeof READ_ONLY_ONESHOT_ARGS] : undefined;
431
+ }
432
+
384
433
  export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
385
434
  {
386
435
  id: "pi",
@@ -414,7 +463,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
414
463
  extensions: ["plugin", "update", "kxm", "-y"],
415
464
  },
416
465
  oneShot: {
417
- argv: ["-p", "--output-format", "json"],
466
+ argv: ["-p", ...oneShotReadOnlyArgs("claude")!, "--output-format", "json"],
418
467
  promptVia: "stdin",
419
468
  outputFormat: "json",
420
469
  usageParser: parseClaudeOneShotUsage,
@@ -430,7 +479,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
430
479
  authArgs: ["provider", "list"],
431
480
  update: { self: ["upgrade"] },
432
481
  oneShot: {
433
- argv: ["--output-format", "stream-json", "-p"],
482
+ argv: [...oneShotReadOnlyArgs("kimi")!, "--output-format", "stream-json", "-p"],
434
483
  promptVia: "arg",
435
484
  outputFormat: "stream-json",
436
485
  usageParser: parseKimiOneShotUsage,
@@ -446,21 +495,12 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
446
495
  authArgs: ["login", "status"],
447
496
  update: { self: ["update"] },
448
497
  oneShot: {
449
- argv: ["exec", "--json", "-"],
498
+ argv: ["exec", ...oneShotReadOnlyArgs("codex")!, "--json", "-"],
450
499
  promptVia: "stdin",
451
500
  outputFormat: "json",
452
501
  usageParser: parseCodexOneShotUsage,
453
502
  },
454
503
  },
455
- {
456
- id: "gemini",
457
- label: "Gemini CLI",
458
- default: false,
459
- mode: "either",
460
- commands: ["gemini"],
461
- versionArgs: ["--version"],
462
- update: { self: ["update"] },
463
- },
464
504
  {
465
505
  id: "deepseek",
466
506
  label: "DeepSeek",
@@ -486,10 +526,10 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
486
526
  authArgs: ["models"],
487
527
  update: { self: ["update"] },
488
528
  oneShot: {
489
- argv: ["--output-format", "json"],
490
- promptVia: "stdin",
529
+ argv: [...oneShotReadOnlyArgs("grok")!, "--output-format", "json", "--single"],
530
+ promptVia: "arg",
491
531
  outputFormat: "json",
492
- usageParser: parseClaudeOneShotUsage,
532
+ usageParser: parseGrokOneShotUsage,
493
533
  },
494
534
  },
495
535
  {
@@ -502,7 +542,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
502
542
  authArgs: ["models"],
503
543
  update: { self: ["update"] },
504
544
  oneShot: {
505
- argv: ["--output-format", "json", "-p"],
545
+ argv: [...oneShotReadOnlyArgs("agy")!, "--output-format", "json", "-p"],
506
546
  promptVia: "arg",
507
547
  outputFormat: "json",
508
548
  usageParser: parseAgyOneShotUsage,
@@ -518,7 +558,55 @@ export function isKnownHarnessId(id: string, extra: Iterable<string> = []): bool
518
558
  return false;
519
559
  }
520
560
 
521
- function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult {
561
+ /** Bare catalog command ids only; never a path or shell metacharacter. */
562
+ export const SAFE_HARNESS_COMMAND_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
563
+
564
+ export function harnessCommandCandidates(
565
+ command: string,
566
+ platform: NodeJS.Platform = process.platform,
567
+ ): readonly string[] {
568
+ if (platform !== "win32" || !SAFE_HARNESS_COMMAND_ID.test(command)) return [command];
569
+ return [command, `${command}.exe`, `${command}.cmd`];
570
+ }
571
+
572
+ export function isWindowsHarnessShim(command: string): boolean {
573
+ return /\.(cmd|bat)$/i.test(command.replace(/\\/g, "/").split("/").pop() ?? command);
574
+ }
575
+
576
+ /** Shell only for allowlisted `name.cmd` on win32. Bare ids stay spawn-without-shell. */
577
+ export function harnessSpawnUsesShell(command: string, platform: NodeJS.Platform = process.platform): boolean {
578
+ if (platform !== "win32") return false;
579
+ const base = command.replace(/\\/g, "/").split("/").pop() ?? "";
580
+ return /^[A-Za-z][A-Za-z0-9_-]*\.cmd$/i.test(base);
581
+ }
582
+
583
+ /** Allowlisted npm-global inner `.exe`, relative to the directory that contains `name.cmd`. */
584
+ export const WIN_NPM_INNER_EXE: Readonly<Record<string, readonly string[]>> = Object.freeze({
585
+ claude: Object.freeze(["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"]),
586
+ });
587
+
588
+ export function findWinNpmInnerExe(
589
+ cliId: string,
590
+ options: {
591
+ platform?: NodeJS.Platform | undefined;
592
+ pathEnv?: string | undefined;
593
+ existsSync?: ((path: string) => boolean) | undefined;
594
+ } = {},
595
+ ): string | undefined {
596
+ if ((options.platform ?? process.platform) !== "win32") return undefined;
597
+ const segments = WIN_NPM_INNER_EXE[cliId];
598
+ if (!segments) return undefined;
599
+ const exists = options.existsSync ?? existsSync;
600
+ const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
601
+ for (const dir of pathEnv.split(";").filter(Boolean)) {
602
+ if (!exists(win32Path.join(dir, `${cliId}.cmd`))) continue;
603
+ const inner = win32Path.join(dir, ...segments);
604
+ if (exists(inner)) return inner;
605
+ }
606
+ return undefined;
607
+ }
608
+
609
+ function defaultRunner(env: NodeJS.ProcessEnv): HarnessRunCommand {
522
610
  return (command, args, timeoutMs) => {
523
611
  try {
524
612
  const result = spawnSync(command, [...args], {
@@ -526,6 +614,7 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
526
614
  timeout: timeoutMs,
527
615
  windowsHide: true,
528
616
  env,
617
+ shell: harnessSpawnUsesShell(command),
529
618
  });
530
619
  if (result.error) {
531
620
  const code = (result.error as NodeJS.ErrnoException).code;
@@ -543,6 +632,37 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
543
632
  };
544
633
  }
545
634
 
635
+ function commandResultFromSpawn(result: Awaited<ReturnType<typeof defaultSpawn>>): HarnessCommandResult {
636
+ const errno = result.error && "code" in result.error
637
+ ? (result.error as NodeJS.ErrnoException).code
638
+ : undefined;
639
+ const error = (typeof errno === "string" && errno ? errno : undefined)
640
+ ?? result.error?.message
641
+ ?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
642
+ return {
643
+ ok: result.code === 0 && !error,
644
+ code: result.code,
645
+ stdout: result.stdout,
646
+ stderr: result.stderr,
647
+ ...(error ? { error } : {}),
648
+ };
649
+ }
650
+
651
+ function defaultAsyncRunner(env: NodeJS.ProcessEnv, signal?: AbortSignal): HarnessRunCommandAsync {
652
+ return async (command, args, timeoutMs) => {
653
+ try {
654
+ return commandResultFromSpawn(await defaultSpawn(command, args, {
655
+ env,
656
+ timeoutMs,
657
+ signal,
658
+ shell: harnessSpawnUsesShell(command),
659
+ }));
660
+ } catch (error) {
661
+ return { ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : "spawn_failed" };
662
+ }
663
+ };
664
+ }
665
+
546
666
  function firstLine(text: string): string | undefined {
547
667
  const line = text.split(/\r?\n/).map((candidate) => candidate.trim()).find(Boolean);
548
668
  return line && line.length <= 200 ? line : undefined;
@@ -588,7 +708,7 @@ function isCodexApiKeyLine(line: string): boolean {
588
708
  return line === CODEX_API_KEY_PREFIX || line.startsWith(`${CODEX_API_KEY_PREFIX} - `);
589
709
  }
590
710
 
591
- function interpretAuth(id: string, result: HarnessCommandResult): { authenticated: boolean | null; issues: string[] } {
711
+ function interpretAuth(id: string, result: HarnessCommandResult): Pick<HarnessStatus, "authenticated" | "authMethod"> & { issues: string[] } {
592
712
  const conflict = authConflictIssue(result);
593
713
  if (conflict) return { authenticated: null, issues: [conflict] };
594
714
  if (result.error) return { authenticated: null, issues: ["auth_probe_error"] };
@@ -597,7 +717,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
597
717
  if (id === "claude") {
598
718
  const payload = parseJsonObject(result.stdout);
599
719
  if (payload && payload.loggedIn === false) return { authenticated: false, issues: ["not_authenticated"] };
600
- if (commandSucceeded(result) && payload && payload.loggedIn === true) return { authenticated: true, issues: [] };
720
+ if (commandSucceeded(result) && payload && payload.loggedIn === true) return {
721
+ authenticated: true, issues: [],
722
+ ...(payload.authMethod === "claude.ai" || payload.authMethod === "api-key" ? { authMethod: payload.authMethod } : {}),
723
+ };
601
724
  return { authenticated: null, issues: ["auth_unparsed"] };
602
725
  }
603
726
  if (id === "codex") {
@@ -605,10 +728,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
605
728
  return { authenticated: false, issues: ["not_authenticated"] };
606
729
  }
607
730
  if (commandSucceeded(result) && lines.some((line) => line === CODEX_CHATGPT_LINE)) {
608
- return { authenticated: true, issues: [] };
731
+ return { authenticated: true, authMethod: "ChatGPT", issues: [] };
609
732
  }
610
733
  if (commandSucceeded(result) && lines.some((line) => isCodexApiKeyLine(line))) {
611
- return { authenticated: true, issues: ["auth_api_key"] };
734
+ return { authenticated: true, authMethod: "api-key", issues: ["auth_api_key"] };
612
735
  }
613
736
  return { authenticated: null, issues: ["auth_unparsed"] };
614
737
  }
@@ -621,7 +744,7 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
621
744
  if (!commandSucceeded(result) || !text.trim()) {
622
745
  return { authenticated: false, issues: ["not_authenticated"] };
623
746
  }
624
- if (AGY_MODEL_ROW.test(text)) return { authenticated: true, issues: [] };
747
+ if (AGY_MODEL_ROW.test(text)) return { authenticated: true, authMethod: "antigravity-oauth", issues: [] };
625
748
  return { authenticated: null, issues: ["auth_unparsed"] };
626
749
  }
627
750
  if (id === "kimi") {
@@ -652,37 +775,81 @@ export function resolveDispatchStatus(
652
775
  return {
653
776
  status: "no",
654
777
  supported: false,
655
- reason: entry.id === "gemini" ? "deprecated_client" : "no_headless_mode",
778
+ reason: "no_headless_mode",
656
779
  };
657
780
  }
781
+ if (entry.id !== "pi" && !oneShotReadOnlyArgs(entry.id)) {
782
+ return { status: "no", supported: false, reason: "permission_profile_unaudited" };
783
+ }
658
784
  if (authenticated === false) {
659
785
  return { status: "no", supported: false, reason: "not_authenticated" };
660
786
  }
661
787
  if (authenticated === null) {
662
- return { status: "no", supported: false, reason: issues[0] ?? "auth_unknown" };
788
+ const authIssue = issues.find((issue) => issue === "auth_context_required" || issue === "auth_unknown" || issue === "auth_unparsed" || issue.startsWith("auth_"));
789
+ return { status: "no", supported: false, reason: authIssue ?? issues[0] ?? "auth_unknown" };
663
790
  }
664
791
  return { status: "yes", supported: true };
665
792
  }
666
793
 
794
+ function tryHarnessCommand(
795
+ candidate: string,
796
+ entry: HarnessCatalogEntry,
797
+ runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
798
+ timeoutMs: number,
799
+ ): { command: string; version: string | undefined } | undefined {
800
+ const result = runCommand(candidate, entry.versionArgs, timeoutMs);
801
+ if (result.error === "ENOENT") return undefined;
802
+ if (result.error && result.code === null && !result.stdout && !result.stderr) return undefined;
803
+ return {
804
+ command: candidate,
805
+ version: firstLine(result.stdout) ?? firstLine(result.stderr),
806
+ };
807
+ }
808
+
809
+ function detectHarnessCommand(
810
+ entry: HarnessCatalogEntry,
811
+ runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
812
+ timeoutMs: number,
813
+ platform: NodeJS.Platform,
814
+ probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
815
+ ): { command: string; version: string | undefined } | undefined {
816
+ const candidates = entry.commands.flatMap((base) => [...harnessCommandCandidates(base, platform)]);
817
+ for (const candidate of candidates) {
818
+ if (isWindowsHarnessShim(candidate)) continue;
819
+ const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
820
+ if (found) return found;
821
+ }
822
+ const inner = findWinNpmInnerExe(entry.id, {
823
+ platform,
824
+ pathEnv: probe.env?.PATH ?? process.env.PATH ?? "",
825
+ existsSync: probe.existsSync,
826
+ });
827
+ if (inner) {
828
+ const found = tryHarnessCommand(inner, entry, runCommand, timeoutMs);
829
+ if (found) return found;
830
+ }
831
+ for (const candidate of candidates) {
832
+ if (!isWindowsHarnessShim(candidate)) continue;
833
+ const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
834
+ if (found) return found;
835
+ }
836
+ return undefined;
837
+ }
838
+
667
839
  function probeEntry(
668
840
  entry: HarnessCatalogEntry,
669
841
  runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
670
842
  timeoutMs: number,
843
+ platform: NodeJS.Platform,
844
+ probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
671
845
  ): HarnessStatus {
672
846
  const issues: string[] = [];
673
- let detected = false;
674
- let command: string | undefined;
675
- let version: string | undefined;
676
- for (const candidate of entry.commands) {
677
- const result = runCommand(candidate, entry.versionArgs, timeoutMs);
678
- if (result.error === "ENOENT") continue;
679
- if (result.error && result.code === null && !result.stdout && !result.stderr) continue;
680
- detected = true;
681
- command = candidate;
682
- version = firstLine(result.stdout) ?? firstLine(result.stderr);
683
- break;
684
- }
847
+ const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, probe);
848
+ const detected = Boolean(found);
849
+ const command = found?.command;
850
+ const version = found?.version;
685
851
  let authenticated: boolean | null = null;
852
+ let authMethod: HarnessStatus["authMethod"];
686
853
  if (!detected) authenticated = false;
687
854
  else if (entry.id === "pi") {
688
855
  authenticated = null;
@@ -691,10 +858,14 @@ function probeEntry(
691
858
  authenticated = null;
692
859
  if (UNKNOWN_AUTH_HARNESSES.has(entry.id)) issues.push("auth_unknown");
693
860
  } else if (command) {
694
- const parsed = interpretAuth(entry.id, runCommand(command, entry.authArgs, timeoutMs));
861
+ // Auth CLIs may initialize hooks/OAuth state and exceed the short binary-detection timeout.
862
+ // Keep detection bounded while allowing the documented auth probe to complete.
863
+ const parsed = interpretAuth(entry.id, runCommand(command, entry.authArgs, Math.max(timeoutMs, 10_000)));
695
864
  authenticated = parsed.authenticated;
865
+ authMethod = parsed.authMethod;
696
866
  issues.push(...parsed.issues);
697
867
  }
868
+ if (command && isWindowsHarnessShim(command)) issues.push("windows_shim");
698
869
  const dispatch = resolveDispatchStatus(entry, detected, authenticated, issues);
699
870
  return {
700
871
  id: entry.id,
@@ -703,6 +874,7 @@ function probeEntry(
703
874
  mode: entry.mode,
704
875
  detected,
705
876
  authenticated,
877
+ ...(authMethod ? { authMethod } : {}),
706
878
  dispatch,
707
879
  ...(command ? { command } : {}),
708
880
  ...(version ? { version } : {}),
@@ -718,15 +890,28 @@ function probeEntry(
718
890
  export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInventory {
719
891
  const timeoutMs = options.timeoutMs ?? 3000;
720
892
  const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
893
+ const platform = options.platform ?? process.platform;
721
894
  return {
722
895
  defaultHarness: DEFAULT_HARNESS,
723
- harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs)),
896
+ harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs, platform, options)),
724
897
  };
725
898
  }
726
899
 
900
+ /**
901
+ * Inventory probe that never uses spawnSync. Product CLI/hub/extension paths
902
+ * must await this so capability/auth checks cannot block the event loop.
903
+ */
904
+ export async function probeHarnessesAsync(options: AsyncHarnessProbeOptions = {}): Promise<HarnessInventory> {
905
+ return replayHarnessProbe(
906
+ options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
907
+ (runCommand) => probeHarnesses({ ...options, runCommand }),
908
+ 256,
909
+ );
910
+ }
911
+
727
912
  export function eligibleHarnesses(inventory: HarnessInventory): readonly string[] {
728
913
  const eligible = inventory.harnesses
729
- .filter((entry) => entry.detected && entry.authenticated === true)
914
+ .filter((entry) => entry !== undefined && entry.detected && entry.authenticated === true)
730
915
  .map((entry) => entry.id);
731
916
  if (eligible.length === 0) throw new Error("no_authenticated_harness");
732
917
  return eligible;
@@ -785,7 +970,7 @@ export function validateHarnessModelPair(
785
970
  return { valid: true };
786
971
  }
787
972
 
788
- if (harnessId === "agy" || harnessId === "gemini") {
973
+ if (harnessId === "agy") {
789
974
  if (provider && provider !== "google") {
790
975
  return { valid: false, issue: "harness_unhosted_model", message: `harness ${harnessId} does not host provider ${provider}` };
791
976
  }
@@ -799,7 +984,7 @@ export function validateHarnessModelPair(
799
984
  if (provider && provider !== "moonshot") {
800
985
  return { valid: false, issue: "harness_unhosted_model", message: `harness kimi does not host provider ${provider}` };
801
986
  }
802
- if (model && !/^(kimi|moonshot)-/i.test(model)) {
987
+ if (model && !/^(kimi|moonshot)(?:-|$)/i.test(model) && !/^kimi-for-coding(?:-|$)/i.test(model)) {
803
988
  return { valid: false, issue: "harness_unhosted_model", message: `harness kimi only hosts kimi/moonshot models, received ${model}` };
804
989
  }
805
990
  return { valid: true };
@@ -843,11 +1028,14 @@ export interface HarnessAssignmentProbeOptions {
843
1028
  env?: NodeJS.ProcessEnv | undefined;
844
1029
  runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult) | undefined;
845
1030
  timeoutMs?: number | undefined;
1031
+ platform?: NodeJS.Platform | undefined;
1032
+ existsSync?: ((path: string) => boolean) | undefined;
846
1033
  }
847
1034
 
848
1035
  export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions): HarnessStatus {
849
1036
  const timeoutMs = options.timeoutMs ?? 3000;
850
1037
  const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
1038
+ const platform = options.platform ?? process.platform;
851
1039
  const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === options.harness);
852
1040
  if (!entry) {
853
1041
  return {
@@ -884,18 +1072,11 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
884
1072
  }
885
1073
  }
886
1074
 
887
- let detected = false;
888
- let command: string | undefined;
889
- let version: string | undefined;
890
- for (const candidate of entry.commands) {
891
- const result = runCommand(candidate, entry.versionArgs, timeoutMs);
892
- if (result.error === "ENOENT") continue;
893
- if (result.error && result.code === null && !result.stdout && !result.stderr) continue;
894
- detected = true;
895
- command = candidate;
896
- version = firstLine(result.stdout) ?? firstLine(result.stderr);
897
- break;
898
- }
1075
+ const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, options);
1076
+ const detected = Boolean(found);
1077
+ const command = found?.command;
1078
+ const version = found?.version;
1079
+ const shimIssue = command && isWindowsHarnessShim(command) ? ["windows_shim"] : [];
899
1080
 
900
1081
  if (!detected || !command) {
901
1082
  return {
@@ -932,7 +1113,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
932
1113
  extensions: Boolean(entry.update.extensions?.length),
933
1114
  models: Boolean(entry.update.models?.length),
934
1115
  },
935
- issues: ["auth_context_required"],
1116
+ issues: ["auth_context_required", ...shimIssue],
936
1117
  };
937
1118
  }
938
1119
 
@@ -960,6 +1141,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
960
1141
  authenticated = false;
961
1142
  issues.push("not_authenticated");
962
1143
  }
1144
+ issues.push(...shimIssue);
963
1145
 
964
1146
  const dispatch = resolveDispatchStatus(entry, true, authenticated, issues);
965
1147
 
@@ -982,7 +1164,62 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
982
1164
  };
983
1165
  }
984
1166
 
985
- return probeEntry(entry, runCommand, timeoutMs);
1167
+ return probeEntry(entry, runCommand, timeoutMs, platform);
1168
+ }
1169
+
1170
+ export interface AsyncHarnessAssignmentProbeOptions extends Omit<HarnessAssignmentProbeOptions, "runCommand"> {
1171
+ signal?: AbortSignal | undefined;
1172
+ runCommand?: HarnessRunCommandAsync | undefined;
1173
+ }
1174
+
1175
+ class PendingHarnessCommand {
1176
+ command: string;
1177
+ args: readonly string[];
1178
+ timeoutMs: number;
1179
+ key: string;
1180
+ constructor(command: string, args: readonly string[], timeoutMs: number, key: string) {
1181
+ this.command = command;
1182
+ this.args = [...args];
1183
+ this.timeoutMs = timeoutMs;
1184
+ this.key = key;
1185
+ }
1186
+ }
1187
+
1188
+ async function replayHarnessProbe<T>(
1189
+ run: HarnessRunCommandAsync,
1190
+ execute: (runCommand: HarnessRunCommand) => T,
1191
+ limit: number,
1192
+ ): Promise<T> {
1193
+ const observed = new Map<string, HarnessCommandResult>();
1194
+ for (let commands = 0; commands <= limit; commands++) {
1195
+ try {
1196
+ return execute((command, args, timeoutMs) => {
1197
+ const key = JSON.stringify([command, args, timeoutMs]);
1198
+ const result = observed.get(key);
1199
+ if (result) return result;
1200
+ throw new PendingHarnessCommand(command, args, timeoutMs, key);
1201
+ });
1202
+ } catch (pending) {
1203
+ if (!(pending instanceof PendingHarnessCommand)) throw pending;
1204
+ if (commands === limit) throw new Error("auth_probe_command_limit");
1205
+ observed.set(pending.key, await run(pending.command, pending.args, pending.timeoutMs));
1206
+ }
1207
+ }
1208
+ throw new Error("auth_probe_command_limit");
1209
+ }
1210
+
1211
+ /**
1212
+ * Execute the existing probe policy without blocking the Runtime event loop.
1213
+ * Replay pure policy decisions from per-call command observations, yielding for
1214
+ * every missing observation. This reuses the exact auth/model/brake parsers,
1215
+ * deduplicates repeated detection, and never caches auth across assignments.
1216
+ */
1217
+ export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
1218
+ return replayHarnessProbe(
1219
+ options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
1220
+ (runCommand) => probeHarnessAssignment({ ...options, runCommand }),
1221
+ 32,
1222
+ );
986
1223
  }
987
1224
 
988
1225
  export function probeHarnessesForModel(
@@ -991,6 +1228,7 @@ export function probeHarnessesForModel(
991
1228
  ): HarnessInventory {
992
1229
  const timeoutMs = options.timeoutMs ?? 3000;
993
1230
  const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
1231
+ const platform = options.platform ?? process.platform;
994
1232
  return {
995
1233
  defaultHarness: DEFAULT_HARNESS,
996
1234
  harnesses: BUILTIN_HARNESSES.map((entry) =>
@@ -1000,11 +1238,25 @@ export function probeHarnessesForModel(
1000
1238
  model: modelSpec.model,
1001
1239
  runCommand,
1002
1240
  timeoutMs,
1241
+ platform,
1242
+ env: options.env,
1243
+ existsSync: options.existsSync,
1003
1244
  })
1004
1245
  ),
1005
1246
  };
1006
1247
  }
1007
1248
 
1249
+ export async function probeHarnessesForModelAsync(
1250
+ modelSpec: HarnessModelSpec,
1251
+ options: AsyncHarnessProbeOptions = {},
1252
+ ): Promise<HarnessInventory> {
1253
+ return replayHarnessProbe(
1254
+ options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
1255
+ (runCommand) => probeHarnessesForModel(modelSpec, { ...options, runCommand }),
1256
+ 256,
1257
+ );
1258
+ }
1259
+
1008
1260
  function scopesFor(scope: HarnessUpdateScope, entry: HarnessCatalogEntry): Exclude<HarnessUpdateScope, "all">[] {
1009
1261
  if (scope === "self") return ["self"];
1010
1262
  if (scope === "extensions") return entry.update.extensions ? ["extensions"] : [];