@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.kxm/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +5 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +2 -0
- package/docs/agent-skills.md +118 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +2 -2
- package/docs/kxm-handbook.md +3 -3
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +102 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +19 -0
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +6875 -3672
- package/plugins/kxm/dist/core.js +214 -34
- package/plugins/kxm/dist/extension.js +7721 -85
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5403 -1014
- package/plugins/kxm/dist/server.js +3008 -2268
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5751 -631
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +73 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +35 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- package/plugins/kxm/src/cli.ts +1077 -11
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +356 -7
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +127 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +375 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/studio-layout.ts +660 -17
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-config.ts +15 -110
- package/plugins/kxm/src/vnext-engine.ts +198 -62
- package/plugins/kxm/src/vnext-harness.ts +263 -81
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +149 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +170 -233
- package/plugins/kxm/src/vnext-runtime-store.ts +35 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-hub.mjs +6 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- 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(["
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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: "
|
|
519
|
+
argv: [...oneShotReadOnlyArgs("grok")!, "--output-format", "json", "--single"],
|
|
520
|
+
promptVia: "arg",
|
|
491
521
|
outputFormat: "json",
|
|
492
|
-
usageParser:
|
|
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):
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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"
|
|
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)
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
};
|