@kontextmind/kxm 0.6.0 → 0.7.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 (117) 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 +5 -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 +2 -0
  14. package/docs/agent-skills.md +118 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/assignment-runner.md +21 -8
  17. package/docs/configuration.md +2 -2
  18. package/docs/kxm-handbook.md +3 -3
  19. package/docs/operator-pi-packages.md +63 -0
  20. package/docs/skills/repo-work-delivery.md +102 -0
  21. package/docs/test-matrix.md +4 -3
  22. package/docs/troubleshooting.md +19 -0
  23. package/docs/vnext/validation.md +9 -0
  24. package/docs/webhook-workflows.md +2 -2
  25. package/examples/README.md +1 -1
  26. package/package.json +16 -17
  27. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  28. package/plugins/kxm/README.md +1 -1
  29. package/plugins/kxm/dist/cli.js +6875 -3672
  30. package/plugins/kxm/dist/core.js +214 -34
  31. package/plugins/kxm/dist/extension.js +7721 -85
  32. package/plugins/kxm/dist/mcp-server.js +75 -21
  33. package/plugins/kxm/dist/runtime.js +5403 -1014
  34. package/plugins/kxm/dist/server.js +3008 -2268
  35. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
  36. package/plugins/kxm/package.json +1 -1
  37. package/plugins/kxm/skills/SUITE.md +5 -0
  38. package/plugins/kxm/skills/hints.json +73 -0
  39. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  40. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  41. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  42. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  43. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  44. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
  45. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  46. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  47. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  48. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
  49. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  50. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  51. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  52. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  53. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  54. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  55. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  56. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  57. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  58. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  59. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  60. package/plugins/kxm/src/autocomplete.ts +9 -3
  61. package/plugins/kxm/src/cli.ts +1077 -11
  62. package/plugins/kxm/src/commands.ts +150 -8
  63. package/plugins/kxm/src/config.ts +7 -4
  64. package/plugins/kxm/src/context-packet.ts +172 -0
  65. package/plugins/kxm/src/extension.ts +36 -1
  66. package/plugins/kxm/src/external-effects.ts +356 -7
  67. package/plugins/kxm/src/hub.ts +2 -4
  68. package/plugins/kxm/src/improve.ts +72 -0
  69. package/plugins/kxm/src/mcp-server.ts +1 -1
  70. package/plugins/kxm/src/model-inventory.ts +127 -0
  71. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  72. package/plugins/kxm/src/policy-draft.mjs +565 -0
  73. package/plugins/kxm/src/price-calc.ts +17 -18
  74. package/plugins/kxm/src/prices.ts +32 -16
  75. package/plugins/kxm/src/producers.ts +71 -0
  76. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  77. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  78. package/plugins/kxm/src/role.ts +375 -0
  79. package/plugins/kxm/src/routing.ts +99 -1
  80. package/plugins/kxm/src/session-work.ts +9 -2
  81. package/plugins/kxm/src/studio-layout.ts +660 -17
  82. package/plugins/kxm/src/suggest.ts +7 -13
  83. package/plugins/kxm/src/telemetry.ts +82 -0
  84. package/plugins/kxm/src/tui.ts +140 -0
  85. package/plugins/kxm/src/vnext-config.ts +15 -110
  86. package/plugins/kxm/src/vnext-engine.ts +198 -62
  87. package/plugins/kxm/src/vnext-harness.ts +263 -81
  88. package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
  89. package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
  90. package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
  91. package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
  92. package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
  93. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  94. package/plugins/kxm/src/workflow-manager.ts +392 -0
  95. package/plugins/kxm/src/workflow-tui.ts +255 -0
  96. package/schemas/policy-draft/README.md +17 -0
  97. package/schemas/policy-draft/model.v2.schema.json +140 -0
  98. package/schemas/policy-draft/role.v2.schema.json +91 -0
  99. package/schemas/vnext/role.schema.json +76 -0
  100. package/schemas/vnext/run-event.schema.json +1 -0
  101. package/scripts/assignment-run.d.mts +1 -1
  102. package/scripts/assignment-run.mjs +44 -35
  103. package/scripts/check-generated.mjs +33 -9
  104. package/scripts/emit-codex-artifacts.mjs +255 -11
  105. package/scripts/harness-run.d.mts +12 -4
  106. package/scripts/harness-run.mjs +65 -17
  107. package/scripts/kxm-hub.mjs +6 -0
  108. package/scripts/native-critic.d.mts +5 -0
  109. package/scripts/native-critic.mjs +60 -0
  110. package/.kxm/config/README.md +0 -5
  111. package/.kxm/config/agents.json +0 -43
  112. package/.kxm/config/env.example +0 -56
  113. package/.kxm/config/update.example.yaml +0 -9
  114. package/.kxm/config/workflows/fix.json +0 -160
  115. package/.kxm/config/workflows/jira-development.json +0 -116
  116. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  117. 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 {
@@ -55,9 +59,11 @@ export interface HarnessCatalogEntry {
55
59
  }
56
60
 
57
61
  export interface HarnessProbeOptions {
58
- env?: NodeJS.ProcessEnv;
59
- runCommand?: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult;
60
- timeoutMs?: number;
62
+ env?: NodeJS.ProcessEnv | undefined;
63
+ runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult) | undefined;
64
+ timeoutMs?: number | undefined;
65
+ platform?: NodeJS.Platform | undefined;
66
+ existsSync?: ((path: string) => boolean) | undefined;
61
67
  }
62
68
 
63
69
  export interface HarnessDispatchStatus {
@@ -73,6 +79,7 @@ export interface HarnessStatus {
73
79
  mode: HarnessMode;
74
80
  detected: boolean;
75
81
  authenticated: boolean | null;
82
+ authMethod?: "claude.ai" | "ChatGPT" | "api-key" | "antigravity-oauth" | undefined;
76
83
  dispatch?: HarnessDispatchStatus | undefined;
77
84
  command?: string;
78
85
  version?: string;
@@ -85,10 +92,10 @@ export interface HarnessInventory {
85
92
  harnesses: readonly HarnessStatus[];
86
93
  }
87
94
 
88
- const UNKNOWN_AUTH_HARNESSES = new Set(["gemini", "deepseek"]);
95
+ const UNKNOWN_AUTH_HARNESSES = new Set(["deepseek"]);
89
96
  const GROK_LOGIN_LINE = "You are logged in with grok.com.";
90
97
  /** 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;
98
+ const AGY_MODEL_ROW = /^[a-z0-9]+(?:[._-][a-z0-9]+)+\s+\S/im;
92
99
  const CODEX_CHATGPT_LINE = "Logged in using ChatGPT";
93
100
  const CODEX_API_KEY_PREFIX = "Logged in using an API key";
94
101
  const CODEX_NEGATIVE_LINE = "Not logged in";
@@ -99,7 +106,6 @@ export const NATIVE_HARNESS_PROVIDERS: Readonly<Record<string, string>> = Object
99
106
  codex: "openai",
100
107
  grok: "xai",
101
108
  agy: "google",
102
- gemini: "google",
103
109
  kimi: "moonshot",
104
110
  deepseek: "deepseek",
105
111
  });
@@ -143,7 +149,11 @@ export interface HarnessUpdateStep {
143
149
  detail?: string;
144
150
  }
145
151
 
146
- export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShotParsedOutput {
152
+ function reportedModelId(value: unknown): string | undefined {
153
+ return typeof value === "string" && /^[a-z0-9][a-z0-9._:/-]{0,199}$/i.test(value) ? value : undefined;
154
+ }
155
+
156
+ export function parseClaudeOneShotUsage(stdout: string, stderr: string, requestedModel?: string): OneShotParsedOutput {
147
157
  const trimmed = stdout.trim();
148
158
  if (!trimmed) {
149
159
  return {
@@ -155,26 +165,28 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
155
165
  try {
156
166
  const payload = JSON.parse(trimmed) as Record<string, unknown>;
157
167
  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);
168
+ const entries = payload.modelUsage && typeof payload.modelUsage === "object" && !Array.isArray(payload.modelUsage)
169
+ ? Object.entries(payload.modelUsage as Record<string, unknown>).filter(([, value]) => value && typeof value === "object") : [];
170
+ const matches = entries.filter(([model]) => model === requestedModel
171
+ || (requestedModel && (model.startsWith(`${requestedModel}-`) || model.startsWith(`claude-${requestedModel}-`))));
172
+ const selected = matches.length === 1 ? matches[0] : entries.length === 1 ? entries[0] : undefined;
173
+ const modelUsageDetail = selected?.[1] as Record<string, unknown> | undefined;
174
+ const effectiveModel = reportedModelId(payload.model) ?? reportedModelId(selected?.[0]);
175
+ // Top-level usage is the attempt total; an auxiliary model is not the task's model.
176
+ const tokensIn = (typeof usage.input_tokens === "number" ? usage.input_tokens : undefined)
177
+ ?? (typeof modelUsageDetail?.inputTokens === "number" ? modelUsageDetail.inputTokens : null);
178
+ const tokensOut = (typeof usage.output_tokens === "number" ? usage.output_tokens : undefined)
179
+ ?? (typeof modelUsageDetail?.outputTokens === "number" ? modelUsageDetail.outputTokens : null);
180
+ const cacheReadTokens = (typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined)
181
+ ?? (typeof modelUsageDetail?.cacheReadInputTokens === "number" ? modelUsageDetail.cacheReadInputTokens : null);
182
+ const cacheWriteTokens = (typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined)
183
+ ?? (typeof modelUsageDetail?.cacheCreationInputTokens === "number" ? modelUsageDetail.cacheCreationInputTokens : null);
173
184
  const costUsd = typeof payload.total_cost_usd === "number"
174
185
  ? payload.total_cost_usd
175
186
  : (typeof modelUsageDetail?.costUSD === "number" ? modelUsageDetail.costUSD : null);
176
187
 
177
- const isError = Boolean(payload.is_error || payload.error);
188
+ const isError = Boolean(payload.is_error || payload.error
189
+ || ["error", "aborted", "failed", "max_tokens", "length"].includes(String(payload.stopReason ?? payload.stop_reason ?? "")));
178
190
  const text = typeof payload.result === "string" ? payload.result : (typeof payload.text === "string" ? payload.text : "");
179
191
  const errorMessage = isError
180
192
  ? (typeof payload.error === "string"
@@ -186,6 +198,7 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
186
198
 
187
199
  return {
188
200
  text,
201
+ effectiveModel,
189
202
  isError,
190
203
  errorMessage,
191
204
  usage: {
@@ -193,18 +206,25 @@ export function parseClaudeOneShotUsage(stdout: string, stderr: string): OneShot
193
206
  tokensOut,
194
207
  cacheReadTokens,
195
208
  cacheWriteTokens,
196
- contextTokens: tokensIn,
209
+ contextTokens: null,
197
210
  costUsd,
198
211
  },
199
212
  };
200
213
  } catch {
201
214
  return {
202
215
  text: trimmed,
216
+ isError: true,
217
+ errorMessage: "invalid Claude JSON response",
203
218
  usage: {},
204
219
  };
205
220
  }
206
221
  }
207
222
 
223
+ /** Grok's observed one-shot JSON uses the same usage/modelUsage envelope. */
224
+ export function parseGrokOneShotUsage(stdout: string, stderr: string, requestedModel?: string): OneShotParsedOutput {
225
+ return parseClaudeOneShotUsage(stdout, stderr, requestedModel);
226
+ }
227
+
208
228
  export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotParsedOutput {
209
229
  const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
210
230
  if (lines.length === 0) {
@@ -238,7 +258,7 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
238
258
  ? ((ev.error as Record<string, unknown>).message as string)
239
259
  : "turn_failed");
240
260
  }
241
- if (ev.item && typeof ev.item === "object" && (ev.item as Record<string, unknown>).type === "agent_message") {
261
+ if (ev.type === "item.completed" && ev.item && typeof ev.item === "object" && (ev.item as Record<string, unknown>).type === "agent_message") {
242
262
  const t = (ev.item as Record<string, unknown>).text;
243
263
  if (typeof t === "string") messageTexts.push(t);
244
264
  }
@@ -247,7 +267,8 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
247
267
  }
248
268
  }
249
269
 
250
- const text = messageTexts.join("\n").trim();
270
+ const text = (messageTexts.at(-1) ?? "").trim();
271
+ if (!events.some((event) => event.type === "turn.completed") || !text) hasError = true;
251
272
  const tokensIn = typeof usage?.input_tokens === "number" ? usage.input_tokens : null;
252
273
  const tokensOut = typeof usage?.output_tokens === "number" ? usage.output_tokens : null;
253
274
  const cacheReadTokens = typeof usage?.cached_input_tokens === "number" ? usage.cached_input_tokens : null;
@@ -262,7 +283,7 @@ export function parseCodexOneShotUsage(stdout: string, stderr: string): OneShotP
262
283
  tokensOut,
263
284
  cacheReadTokens,
264
285
  cacheWriteTokens,
265
- contextTokens: tokensIn,
286
+ contextTokens: null,
266
287
  costUsd: null,
267
288
  },
268
289
  };
@@ -279,7 +300,15 @@ export function parseGenericOneShotUsage(stdout: string, _stderr: string): OneSh
279
300
  : (typeof rec.text === "string"
280
301
  ? rec.text
281
302
  : (typeof rec.response === "string" ? rec.response : trimmed));
282
- return { text, usage: {} };
303
+ const u = rec.usage && typeof rec.usage === "object" ? rec.usage as Record<string, unknown> : {};
304
+ return { text, effectiveModel: reportedModelId(rec.model), isError: Boolean(rec.is_error || rec.error || rec.success === false), usage: {
305
+ tokensIn: typeof u.input_tokens === "number" ? u.input_tokens : null,
306
+ tokensOut: typeof u.output_tokens === "number" ? u.output_tokens : null,
307
+ cacheReadTokens: typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : null,
308
+ cacheWriteTokens: typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : null,
309
+ contextTokens: null,
310
+ costUsd: typeof rec.total_cost_usd === "number" ? rec.total_cost_usd : null,
311
+ } };
283
312
  }
284
313
  } catch {
285
314
  // not json
@@ -381,6 +410,16 @@ export function parseAgyOneShotUsage(stdout: string, _stderr: string): OneShotPa
381
410
  }
382
411
 
383
412
  /** Built-in harnesses. Unknown ids fail closed. Model lists live in `.kxm/models/*.yaml`, not here. */
413
+ const READ_ONLY_ONESHOT_ARGS = Object.freeze({
414
+ claude: Object.freeze(["--tools", "Read,Glob,Grep", "--restricted", "--safe-mode", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands", "--no-session-persistence"]),
415
+ codex: Object.freeze(["--sandbox", "read-only", "--ignore-user-config", "-c", 'approval_policy="never"']),
416
+ grok: Object.freeze(["--sandbox", "read-only", "--permission-mode", "plan", "--tools", "Read,Glob,Grep", "--no-subagents", "--disable-web-search"]),
417
+ });
418
+
419
+ export function oneShotReadOnlyArgs(harness: string): readonly string[] | undefined {
420
+ return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness as keyof typeof READ_ONLY_ONESHOT_ARGS] : undefined;
421
+ }
422
+
384
423
  export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
385
424
  {
386
425
  id: "pi",
@@ -414,7 +453,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
414
453
  extensions: ["plugin", "update", "kxm", "-y"],
415
454
  },
416
455
  oneShot: {
417
- argv: ["-p", "--output-format", "json"],
456
+ argv: ["-p", ...oneShotReadOnlyArgs("claude")!, "--output-format", "json"],
418
457
  promptVia: "stdin",
419
458
  outputFormat: "json",
420
459
  usageParser: parseClaudeOneShotUsage,
@@ -446,21 +485,12 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
446
485
  authArgs: ["login", "status"],
447
486
  update: { self: ["update"] },
448
487
  oneShot: {
449
- argv: ["exec", "--json", "-"],
488
+ argv: ["exec", ...oneShotReadOnlyArgs("codex")!, "--json", "-"],
450
489
  promptVia: "stdin",
451
490
  outputFormat: "json",
452
491
  usageParser: parseCodexOneShotUsage,
453
492
  },
454
493
  },
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
494
  {
465
495
  id: "deepseek",
466
496
  label: "DeepSeek",
@@ -486,10 +516,10 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
486
516
  authArgs: ["models"],
487
517
  update: { self: ["update"] },
488
518
  oneShot: {
489
- argv: ["--output-format", "json"],
490
- promptVia: "stdin",
519
+ argv: [...oneShotReadOnlyArgs("grok")!, "--output-format", "json", "--single"],
520
+ promptVia: "arg",
491
521
  outputFormat: "json",
492
- usageParser: parseClaudeOneShotUsage,
522
+ usageParser: parseGrokOneShotUsage,
493
523
  },
494
524
  },
495
525
  {
@@ -518,6 +548,54 @@ export function isKnownHarnessId(id: string, extra: Iterable<string> = []): bool
518
548
  return false;
519
549
  }
520
550
 
551
+ /** Bare catalog command ids only; never a path or shell metacharacter. */
552
+ export const SAFE_HARNESS_COMMAND_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
553
+
554
+ export function harnessCommandCandidates(
555
+ command: string,
556
+ platform: NodeJS.Platform = process.platform,
557
+ ): readonly string[] {
558
+ if (platform !== "win32" || !SAFE_HARNESS_COMMAND_ID.test(command)) return [command];
559
+ return [command, `${command}.exe`, `${command}.cmd`];
560
+ }
561
+
562
+ export function isWindowsHarnessShim(command: string): boolean {
563
+ return /\.(cmd|bat)$/i.test(command.replace(/\\/g, "/").split("/").pop() ?? command);
564
+ }
565
+
566
+ /** Shell only for allowlisted `name.cmd` on win32. Bare ids stay spawn-without-shell. */
567
+ export function harnessSpawnUsesShell(command: string, platform: NodeJS.Platform = process.platform): boolean {
568
+ if (platform !== "win32") return false;
569
+ const base = command.replace(/\\/g, "/").split("/").pop() ?? "";
570
+ return /^[A-Za-z][A-Za-z0-9_-]*\.cmd$/i.test(base);
571
+ }
572
+
573
+ /** Allowlisted npm-global inner `.exe`, relative to the directory that contains `name.cmd`. */
574
+ export const WIN_NPM_INNER_EXE: Readonly<Record<string, readonly string[]>> = Object.freeze({
575
+ claude: Object.freeze(["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"]),
576
+ });
577
+
578
+ export function findWinNpmInnerExe(
579
+ cliId: string,
580
+ options: {
581
+ platform?: NodeJS.Platform | undefined;
582
+ pathEnv?: string | undefined;
583
+ existsSync?: ((path: string) => boolean) | undefined;
584
+ } = {},
585
+ ): string | undefined {
586
+ if ((options.platform ?? process.platform) !== "win32") return undefined;
587
+ const segments = WIN_NPM_INNER_EXE[cliId];
588
+ if (!segments) return undefined;
589
+ const exists = options.existsSync ?? existsSync;
590
+ const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
591
+ for (const dir of pathEnv.split(";").filter(Boolean)) {
592
+ if (!exists(win32Path.join(dir, `${cliId}.cmd`))) continue;
593
+ const inner = win32Path.join(dir, ...segments);
594
+ if (exists(inner)) return inner;
595
+ }
596
+ return undefined;
597
+ }
598
+
521
599
  function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult {
522
600
  return (command, args, timeoutMs) => {
523
601
  try {
@@ -526,6 +604,7 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
526
604
  timeout: timeoutMs,
527
605
  windowsHide: true,
528
606
  env,
607
+ shell: harnessSpawnUsesShell(command),
529
608
  });
530
609
  if (result.error) {
531
610
  const code = (result.error as NodeJS.ErrnoException).code;
@@ -588,7 +667,7 @@ function isCodexApiKeyLine(line: string): boolean {
588
667
  return line === CODEX_API_KEY_PREFIX || line.startsWith(`${CODEX_API_KEY_PREFIX} - `);
589
668
  }
590
669
 
591
- function interpretAuth(id: string, result: HarnessCommandResult): { authenticated: boolean | null; issues: string[] } {
670
+ function interpretAuth(id: string, result: HarnessCommandResult): Pick<HarnessStatus, "authenticated" | "authMethod"> & { issues: string[] } {
592
671
  const conflict = authConflictIssue(result);
593
672
  if (conflict) return { authenticated: null, issues: [conflict] };
594
673
  if (result.error) return { authenticated: null, issues: ["auth_probe_error"] };
@@ -597,7 +676,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
597
676
  if (id === "claude") {
598
677
  const payload = parseJsonObject(result.stdout);
599
678
  if (payload && payload.loggedIn === false) return { authenticated: false, issues: ["not_authenticated"] };
600
- if (commandSucceeded(result) && payload && payload.loggedIn === true) return { authenticated: true, issues: [] };
679
+ if (commandSucceeded(result) && payload && payload.loggedIn === true) return {
680
+ authenticated: true, issues: [],
681
+ ...(payload.authMethod === "claude.ai" || payload.authMethod === "api-key" ? { authMethod: payload.authMethod } : {}),
682
+ };
601
683
  return { authenticated: null, issues: ["auth_unparsed"] };
602
684
  }
603
685
  if (id === "codex") {
@@ -605,10 +687,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
605
687
  return { authenticated: false, issues: ["not_authenticated"] };
606
688
  }
607
689
  if (commandSucceeded(result) && lines.some((line) => line === CODEX_CHATGPT_LINE)) {
608
- return { authenticated: true, issues: [] };
690
+ return { authenticated: true, authMethod: "ChatGPT", issues: [] };
609
691
  }
610
692
  if (commandSucceeded(result) && lines.some((line) => isCodexApiKeyLine(line))) {
611
- return { authenticated: true, issues: ["auth_api_key"] };
693
+ return { authenticated: true, authMethod: "api-key", issues: ["auth_api_key"] };
612
694
  }
613
695
  return { authenticated: null, issues: ["auth_unparsed"] };
614
696
  }
@@ -621,7 +703,7 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
621
703
  if (!commandSucceeded(result) || !text.trim()) {
622
704
  return { authenticated: false, issues: ["not_authenticated"] };
623
705
  }
624
- if (AGY_MODEL_ROW.test(text)) return { authenticated: true, issues: [] };
706
+ if (AGY_MODEL_ROW.test(text)) return { authenticated: true, authMethod: "antigravity-oauth", issues: [] };
625
707
  return { authenticated: null, issues: ["auth_unparsed"] };
626
708
  }
627
709
  if (id === "kimi") {
@@ -652,37 +734,81 @@ export function resolveDispatchStatus(
652
734
  return {
653
735
  status: "no",
654
736
  supported: false,
655
- reason: entry.id === "gemini" ? "deprecated_client" : "no_headless_mode",
737
+ reason: "no_headless_mode",
656
738
  };
657
739
  }
740
+ if (entry.id !== "pi" && !oneShotReadOnlyArgs(entry.id)) {
741
+ return { status: "no", supported: false, reason: "permission_profile_unaudited" };
742
+ }
658
743
  if (authenticated === false) {
659
744
  return { status: "no", supported: false, reason: "not_authenticated" };
660
745
  }
661
746
  if (authenticated === null) {
662
- return { status: "no", supported: false, reason: issues[0] ?? "auth_unknown" };
747
+ const authIssue = issues.find((issue) => issue === "auth_context_required" || issue === "auth_unknown" || issue === "auth_unparsed" || issue.startsWith("auth_"));
748
+ return { status: "no", supported: false, reason: authIssue ?? issues[0] ?? "auth_unknown" };
663
749
  }
664
750
  return { status: "yes", supported: true };
665
751
  }
666
752
 
753
+ function tryHarnessCommand(
754
+ candidate: string,
755
+ entry: HarnessCatalogEntry,
756
+ runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
757
+ timeoutMs: number,
758
+ ): { command: string; version: string | undefined } | undefined {
759
+ const result = runCommand(candidate, entry.versionArgs, timeoutMs);
760
+ if (result.error === "ENOENT") return undefined;
761
+ if (result.error && result.code === null && !result.stdout && !result.stderr) return undefined;
762
+ return {
763
+ command: candidate,
764
+ version: firstLine(result.stdout) ?? firstLine(result.stderr),
765
+ };
766
+ }
767
+
768
+ function detectHarnessCommand(
769
+ entry: HarnessCatalogEntry,
770
+ runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
771
+ timeoutMs: number,
772
+ platform: NodeJS.Platform,
773
+ probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
774
+ ): { command: string; version: string | undefined } | undefined {
775
+ const candidates = entry.commands.flatMap((base) => [...harnessCommandCandidates(base, platform)]);
776
+ for (const candidate of candidates) {
777
+ if (isWindowsHarnessShim(candidate)) continue;
778
+ const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
779
+ if (found) return found;
780
+ }
781
+ const inner = findWinNpmInnerExe(entry.id, {
782
+ platform,
783
+ pathEnv: probe.env?.PATH ?? process.env.PATH ?? "",
784
+ existsSync: probe.existsSync,
785
+ });
786
+ if (inner) {
787
+ const found = tryHarnessCommand(inner, entry, runCommand, timeoutMs);
788
+ if (found) return found;
789
+ }
790
+ for (const candidate of candidates) {
791
+ if (!isWindowsHarnessShim(candidate)) continue;
792
+ const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
793
+ if (found) return found;
794
+ }
795
+ return undefined;
796
+ }
797
+
667
798
  function probeEntry(
668
799
  entry: HarnessCatalogEntry,
669
800
  runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
670
801
  timeoutMs: number,
802
+ platform: NodeJS.Platform,
803
+ probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
671
804
  ): HarnessStatus {
672
805
  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
- }
806
+ const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, probe);
807
+ const detected = Boolean(found);
808
+ const command = found?.command;
809
+ const version = found?.version;
685
810
  let authenticated: boolean | null = null;
811
+ let authMethod: HarnessStatus["authMethod"];
686
812
  if (!detected) authenticated = false;
687
813
  else if (entry.id === "pi") {
688
814
  authenticated = null;
@@ -691,10 +817,14 @@ function probeEntry(
691
817
  authenticated = null;
692
818
  if (UNKNOWN_AUTH_HARNESSES.has(entry.id)) issues.push("auth_unknown");
693
819
  } else if (command) {
694
- const parsed = interpretAuth(entry.id, runCommand(command, entry.authArgs, timeoutMs));
820
+ // Auth CLIs may initialize hooks/OAuth state and exceed the short binary-detection timeout.
821
+ // Keep detection bounded while allowing the documented auth probe to complete.
822
+ const parsed = interpretAuth(entry.id, runCommand(command, entry.authArgs, Math.max(timeoutMs, 10_000)));
695
823
  authenticated = parsed.authenticated;
824
+ authMethod = parsed.authMethod;
696
825
  issues.push(...parsed.issues);
697
826
  }
827
+ if (command && isWindowsHarnessShim(command)) issues.push("windows_shim");
698
828
  const dispatch = resolveDispatchStatus(entry, detected, authenticated, issues);
699
829
  return {
700
830
  id: entry.id,
@@ -703,6 +833,7 @@ function probeEntry(
703
833
  mode: entry.mode,
704
834
  detected,
705
835
  authenticated,
836
+ ...(authMethod ? { authMethod } : {}),
706
837
  dispatch,
707
838
  ...(command ? { command } : {}),
708
839
  ...(version ? { version } : {}),
@@ -718,15 +849,16 @@ function probeEntry(
718
849
  export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInventory {
719
850
  const timeoutMs = options.timeoutMs ?? 3000;
720
851
  const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
852
+ const platform = options.platform ?? process.platform;
721
853
  return {
722
854
  defaultHarness: DEFAULT_HARNESS,
723
- harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs)),
855
+ harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs, platform, options)),
724
856
  };
725
857
  }
726
858
 
727
859
  export function eligibleHarnesses(inventory: HarnessInventory): readonly string[] {
728
860
  const eligible = inventory.harnesses
729
- .filter((entry) => entry.detected && entry.authenticated === true)
861
+ .filter((entry) => entry !== undefined && entry.detected && entry.authenticated === true)
730
862
  .map((entry) => entry.id);
731
863
  if (eligible.length === 0) throw new Error("no_authenticated_harness");
732
864
  return eligible;
@@ -785,7 +917,7 @@ export function validateHarnessModelPair(
785
917
  return { valid: true };
786
918
  }
787
919
 
788
- if (harnessId === "agy" || harnessId === "gemini") {
920
+ if (harnessId === "agy") {
789
921
  if (provider && provider !== "google") {
790
922
  return { valid: false, issue: "harness_unhosted_model", message: `harness ${harnessId} does not host provider ${provider}` };
791
923
  }
@@ -799,7 +931,7 @@ export function validateHarnessModelPair(
799
931
  if (provider && provider !== "moonshot") {
800
932
  return { valid: false, issue: "harness_unhosted_model", message: `harness kimi does not host provider ${provider}` };
801
933
  }
802
- if (model && !/^(kimi|moonshot)-/i.test(model)) {
934
+ if (model && !/^(kimi|moonshot)(?:-|$)/i.test(model) && !/^kimi-for-coding(?:-|$)/i.test(model)) {
803
935
  return { valid: false, issue: "harness_unhosted_model", message: `harness kimi only hosts kimi/moonshot models, received ${model}` };
804
936
  }
805
937
  return { valid: true };
@@ -843,11 +975,14 @@ export interface HarnessAssignmentProbeOptions {
843
975
  env?: NodeJS.ProcessEnv | undefined;
844
976
  runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult) | undefined;
845
977
  timeoutMs?: number | undefined;
978
+ platform?: NodeJS.Platform | undefined;
979
+ existsSync?: ((path: string) => boolean) | undefined;
846
980
  }
847
981
 
848
982
  export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions): HarnessStatus {
849
983
  const timeoutMs = options.timeoutMs ?? 3000;
850
984
  const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
985
+ const platform = options.platform ?? process.platform;
851
986
  const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === options.harness);
852
987
  if (!entry) {
853
988
  return {
@@ -884,18 +1019,11 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
884
1019
  }
885
1020
  }
886
1021
 
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
- }
1022
+ const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, options);
1023
+ const detected = Boolean(found);
1024
+ const command = found?.command;
1025
+ const version = found?.version;
1026
+ const shimIssue = command && isWindowsHarnessShim(command) ? ["windows_shim"] : [];
899
1027
 
900
1028
  if (!detected || !command) {
901
1029
  return {
@@ -932,7 +1060,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
932
1060
  extensions: Boolean(entry.update.extensions?.length),
933
1061
  models: Boolean(entry.update.models?.length),
934
1062
  },
935
- issues: ["auth_context_required"],
1063
+ issues: ["auth_context_required", ...shimIssue],
936
1064
  };
937
1065
  }
938
1066
 
@@ -960,6 +1088,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
960
1088
  authenticated = false;
961
1089
  issues.push("not_authenticated");
962
1090
  }
1091
+ issues.push(...shimIssue);
963
1092
 
964
1093
  const dispatch = resolveDispatchStatus(entry, true, authenticated, issues);
965
1094
 
@@ -982,7 +1111,56 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
982
1111
  };
983
1112
  }
984
1113
 
985
- return probeEntry(entry, runCommand, timeoutMs);
1114
+ return probeEntry(entry, runCommand, timeoutMs, platform);
1115
+ }
1116
+
1117
+ export interface AsyncHarnessAssignmentProbeOptions extends Omit<HarnessAssignmentProbeOptions, "runCommand"> {
1118
+ signal?: AbortSignal | undefined;
1119
+ runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult | Promise<HarnessCommandResult>) | undefined;
1120
+ }
1121
+
1122
+ class PendingHarnessCommand {
1123
+ command: string;
1124
+ args: readonly string[];
1125
+ timeoutMs: number;
1126
+ key: string;
1127
+ constructor(command: string, args: readonly string[], timeoutMs: number, key: string) {
1128
+ this.command = command;
1129
+ this.args = [...args];
1130
+ this.timeoutMs = timeoutMs;
1131
+ this.key = key;
1132
+ }
1133
+ }
1134
+
1135
+ /**
1136
+ * Execute the existing probe policy without blocking the Runtime event loop.
1137
+ * Replay pure policy decisions from per-call command observations, yielding for
1138
+ * every missing observation. This reuses the exact auth/model/brake parsers,
1139
+ * deduplicates repeated detection, and never caches auth across assignments.
1140
+ */
1141
+ export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
1142
+ const observed = new Map<string, HarnessCommandResult>();
1143
+ const run = options.runCommand ?? (async (command, args, timeoutMs) => {
1144
+ const result = await defaultSpawn(command, args, { env: options.env, timeoutMs, signal: options.signal });
1145
+ const error = result.error?.message ?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
1146
+ return { ok: result.code === 0 && !error, code: result.code, stdout: result.stdout, stderr: result.stderr,
1147
+ ...(error ? { error } : {}) };
1148
+ });
1149
+ for (let commands = 0; commands <= 32; commands++) {
1150
+ try {
1151
+ return probeHarnessAssignment({ ...options, runCommand(command, args, timeoutMs) {
1152
+ const key = JSON.stringify([command, args, timeoutMs]);
1153
+ const result = observed.get(key);
1154
+ if (result) return result;
1155
+ throw new PendingHarnessCommand(command, args, timeoutMs, key);
1156
+ } });
1157
+ } catch (pending) {
1158
+ if (!(pending instanceof PendingHarnessCommand)) throw pending;
1159
+ if (commands === 32) throw new Error("auth_probe_command_limit");
1160
+ observed.set(pending.key, await run(pending.command, pending.args, pending.timeoutMs));
1161
+ }
1162
+ }
1163
+ throw new Error("auth_probe_command_limit");
986
1164
  }
987
1165
 
988
1166
  export function probeHarnessesForModel(
@@ -991,6 +1169,7 @@ export function probeHarnessesForModel(
991
1169
  ): HarnessInventory {
992
1170
  const timeoutMs = options.timeoutMs ?? 3000;
993
1171
  const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
1172
+ const platform = options.platform ?? process.platform;
994
1173
  return {
995
1174
  defaultHarness: DEFAULT_HARNESS,
996
1175
  harnesses: BUILTIN_HARNESSES.map((entry) =>
@@ -1000,6 +1179,9 @@ export function probeHarnessesForModel(
1000
1179
  model: modelSpec.model,
1001
1180
  runCommand,
1002
1181
  timeoutMs,
1182
+ platform,
1183
+ env: options.env,
1184
+ existsSync: options.existsSync,
1003
1185
  })
1004
1186
  ),
1005
1187
  };