@kontextmind/kxm 0.6.0 → 0.7.4
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 +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +4 -0
- package/docs/agent-skills.md +121 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +21 -0
- package/docs/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -0
- package/docs/skills/repo-work-delivery.md +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- 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 +8955 -3934
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +5637 -1125
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5808 -565
- 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 +43 -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 +42 -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 +1637 -73
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- 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/protocol.ts +111 -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 +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/store.ts +1 -1
- 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-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +267 -83
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +166 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +179 -224
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- 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/plugins/kxm/src/workflow.ts +144 -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-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -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,18 @@ 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
|
+
agy: Object.freeze(["--mode", "plan", "--sandbox", "--disable-slash-commands"]),
|
|
418
|
+
kimi: Object.freeze(["--plan"]),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
export function oneShotReadOnlyArgs(harness: string): readonly string[] | undefined {
|
|
422
|
+
return Object.hasOwn(READ_ONLY_ONESHOT_ARGS, harness) ? READ_ONLY_ONESHOT_ARGS[harness as keyof typeof READ_ONLY_ONESHOT_ARGS] : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
384
425
|
export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
385
426
|
{
|
|
386
427
|
id: "pi",
|
|
@@ -414,7 +455,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
414
455
|
extensions: ["plugin", "update", "kxm", "-y"],
|
|
415
456
|
},
|
|
416
457
|
oneShot: {
|
|
417
|
-
argv: ["-p", "--output-format", "json"],
|
|
458
|
+
argv: ["-p", ...oneShotReadOnlyArgs("claude")!, "--output-format", "json"],
|
|
418
459
|
promptVia: "stdin",
|
|
419
460
|
outputFormat: "json",
|
|
420
461
|
usageParser: parseClaudeOneShotUsage,
|
|
@@ -430,7 +471,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
430
471
|
authArgs: ["provider", "list"],
|
|
431
472
|
update: { self: ["upgrade"] },
|
|
432
473
|
oneShot: {
|
|
433
|
-
argv: ["--output-format", "stream-json", "-p"],
|
|
474
|
+
argv: [...oneShotReadOnlyArgs("kimi")!, "--output-format", "stream-json", "-p"],
|
|
434
475
|
promptVia: "arg",
|
|
435
476
|
outputFormat: "stream-json",
|
|
436
477
|
usageParser: parseKimiOneShotUsage,
|
|
@@ -446,21 +487,12 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
446
487
|
authArgs: ["login", "status"],
|
|
447
488
|
update: { self: ["update"] },
|
|
448
489
|
oneShot: {
|
|
449
|
-
argv: ["exec", "--json", "-"],
|
|
490
|
+
argv: ["exec", ...oneShotReadOnlyArgs("codex")!, "--json", "-"],
|
|
450
491
|
promptVia: "stdin",
|
|
451
492
|
outputFormat: "json",
|
|
452
493
|
usageParser: parseCodexOneShotUsage,
|
|
453
494
|
},
|
|
454
495
|
},
|
|
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
496
|
{
|
|
465
497
|
id: "deepseek",
|
|
466
498
|
label: "DeepSeek",
|
|
@@ -486,10 +518,10 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
486
518
|
authArgs: ["models"],
|
|
487
519
|
update: { self: ["update"] },
|
|
488
520
|
oneShot: {
|
|
489
|
-
argv: ["--output-format", "json"],
|
|
490
|
-
promptVia: "
|
|
521
|
+
argv: [...oneShotReadOnlyArgs("grok")!, "--output-format", "json", "--single"],
|
|
522
|
+
promptVia: "arg",
|
|
491
523
|
outputFormat: "json",
|
|
492
|
-
usageParser:
|
|
524
|
+
usageParser: parseGrokOneShotUsage,
|
|
493
525
|
},
|
|
494
526
|
},
|
|
495
527
|
{
|
|
@@ -502,7 +534,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
502
534
|
authArgs: ["models"],
|
|
503
535
|
update: { self: ["update"] },
|
|
504
536
|
oneShot: {
|
|
505
|
-
argv: ["--output-format", "json", "-p"],
|
|
537
|
+
argv: [...oneShotReadOnlyArgs("agy")!, "--output-format", "json", "-p"],
|
|
506
538
|
promptVia: "arg",
|
|
507
539
|
outputFormat: "json",
|
|
508
540
|
usageParser: parseAgyOneShotUsage,
|
|
@@ -518,6 +550,54 @@ export function isKnownHarnessId(id: string, extra: Iterable<string> = []): bool
|
|
|
518
550
|
return false;
|
|
519
551
|
}
|
|
520
552
|
|
|
553
|
+
/** Bare catalog command ids only; never a path or shell metacharacter. */
|
|
554
|
+
export const SAFE_HARNESS_COMMAND_ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
555
|
+
|
|
556
|
+
export function harnessCommandCandidates(
|
|
557
|
+
command: string,
|
|
558
|
+
platform: NodeJS.Platform = process.platform,
|
|
559
|
+
): readonly string[] {
|
|
560
|
+
if (platform !== "win32" || !SAFE_HARNESS_COMMAND_ID.test(command)) return [command];
|
|
561
|
+
return [command, `${command}.exe`, `${command}.cmd`];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function isWindowsHarnessShim(command: string): boolean {
|
|
565
|
+
return /\.(cmd|bat)$/i.test(command.replace(/\\/g, "/").split("/").pop() ?? command);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Shell only for allowlisted `name.cmd` on win32. Bare ids stay spawn-without-shell. */
|
|
569
|
+
export function harnessSpawnUsesShell(command: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
570
|
+
if (platform !== "win32") return false;
|
|
571
|
+
const base = command.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
572
|
+
return /^[A-Za-z][A-Za-z0-9_-]*\.cmd$/i.test(base);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Allowlisted npm-global inner `.exe`, relative to the directory that contains `name.cmd`. */
|
|
576
|
+
export const WIN_NPM_INNER_EXE: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
577
|
+
claude: Object.freeze(["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"]),
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
export function findWinNpmInnerExe(
|
|
581
|
+
cliId: string,
|
|
582
|
+
options: {
|
|
583
|
+
platform?: NodeJS.Platform | undefined;
|
|
584
|
+
pathEnv?: string | undefined;
|
|
585
|
+
existsSync?: ((path: string) => boolean) | undefined;
|
|
586
|
+
} = {},
|
|
587
|
+
): string | undefined {
|
|
588
|
+
if ((options.platform ?? process.platform) !== "win32") return undefined;
|
|
589
|
+
const segments = WIN_NPM_INNER_EXE[cliId];
|
|
590
|
+
if (!segments) return undefined;
|
|
591
|
+
const exists = options.existsSync ?? existsSync;
|
|
592
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
593
|
+
for (const dir of pathEnv.split(";").filter(Boolean)) {
|
|
594
|
+
if (!exists(win32Path.join(dir, `${cliId}.cmd`))) continue;
|
|
595
|
+
const inner = win32Path.join(dir, ...segments);
|
|
596
|
+
if (exists(inner)) return inner;
|
|
597
|
+
}
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
521
601
|
function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult {
|
|
522
602
|
return (command, args, timeoutMs) => {
|
|
523
603
|
try {
|
|
@@ -526,6 +606,7 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
|
|
|
526
606
|
timeout: timeoutMs,
|
|
527
607
|
windowsHide: true,
|
|
528
608
|
env,
|
|
609
|
+
shell: harnessSpawnUsesShell(command),
|
|
529
610
|
});
|
|
530
611
|
if (result.error) {
|
|
531
612
|
const code = (result.error as NodeJS.ErrnoException).code;
|
|
@@ -588,7 +669,7 @@ function isCodexApiKeyLine(line: string): boolean {
|
|
|
588
669
|
return line === CODEX_API_KEY_PREFIX || line.startsWith(`${CODEX_API_KEY_PREFIX} - `);
|
|
589
670
|
}
|
|
590
671
|
|
|
591
|
-
function interpretAuth(id: string, result: HarnessCommandResult):
|
|
672
|
+
function interpretAuth(id: string, result: HarnessCommandResult): Pick<HarnessStatus, "authenticated" | "authMethod"> & { issues: string[] } {
|
|
592
673
|
const conflict = authConflictIssue(result);
|
|
593
674
|
if (conflict) return { authenticated: null, issues: [conflict] };
|
|
594
675
|
if (result.error) return { authenticated: null, issues: ["auth_probe_error"] };
|
|
@@ -597,7 +678,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
|
|
|
597
678
|
if (id === "claude") {
|
|
598
679
|
const payload = parseJsonObject(result.stdout);
|
|
599
680
|
if (payload && payload.loggedIn === false) return { authenticated: false, issues: ["not_authenticated"] };
|
|
600
|
-
if (commandSucceeded(result) && payload && payload.loggedIn === true) return {
|
|
681
|
+
if (commandSucceeded(result) && payload && payload.loggedIn === true) return {
|
|
682
|
+
authenticated: true, issues: [],
|
|
683
|
+
...(payload.authMethod === "claude.ai" || payload.authMethod === "api-key" ? { authMethod: payload.authMethod } : {}),
|
|
684
|
+
};
|
|
601
685
|
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
602
686
|
}
|
|
603
687
|
if (id === "codex") {
|
|
@@ -605,10 +689,10 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
|
|
|
605
689
|
return { authenticated: false, issues: ["not_authenticated"] };
|
|
606
690
|
}
|
|
607
691
|
if (commandSucceeded(result) && lines.some((line) => line === CODEX_CHATGPT_LINE)) {
|
|
608
|
-
return { authenticated: true, issues: [] };
|
|
692
|
+
return { authenticated: true, authMethod: "ChatGPT", issues: [] };
|
|
609
693
|
}
|
|
610
694
|
if (commandSucceeded(result) && lines.some((line) => isCodexApiKeyLine(line))) {
|
|
611
|
-
return { authenticated: true, issues: ["auth_api_key"] };
|
|
695
|
+
return { authenticated: true, authMethod: "api-key", issues: ["auth_api_key"] };
|
|
612
696
|
}
|
|
613
697
|
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
614
698
|
}
|
|
@@ -621,7 +705,7 @@ function interpretAuth(id: string, result: HarnessCommandResult): { authenticate
|
|
|
621
705
|
if (!commandSucceeded(result) || !text.trim()) {
|
|
622
706
|
return { authenticated: false, issues: ["not_authenticated"] };
|
|
623
707
|
}
|
|
624
|
-
if (AGY_MODEL_ROW.test(text)) return { authenticated: true, issues: [] };
|
|
708
|
+
if (AGY_MODEL_ROW.test(text)) return { authenticated: true, authMethod: "antigravity-oauth", issues: [] };
|
|
625
709
|
return { authenticated: null, issues: ["auth_unparsed"] };
|
|
626
710
|
}
|
|
627
711
|
if (id === "kimi") {
|
|
@@ -652,37 +736,81 @@ export function resolveDispatchStatus(
|
|
|
652
736
|
return {
|
|
653
737
|
status: "no",
|
|
654
738
|
supported: false,
|
|
655
|
-
reason:
|
|
739
|
+
reason: "no_headless_mode",
|
|
656
740
|
};
|
|
657
741
|
}
|
|
742
|
+
if (entry.id !== "pi" && !oneShotReadOnlyArgs(entry.id)) {
|
|
743
|
+
return { status: "no", supported: false, reason: "permission_profile_unaudited" };
|
|
744
|
+
}
|
|
658
745
|
if (authenticated === false) {
|
|
659
746
|
return { status: "no", supported: false, reason: "not_authenticated" };
|
|
660
747
|
}
|
|
661
748
|
if (authenticated === null) {
|
|
662
|
-
|
|
749
|
+
const authIssue = issues.find((issue) => issue === "auth_context_required" || issue === "auth_unknown" || issue === "auth_unparsed" || issue.startsWith("auth_"));
|
|
750
|
+
return { status: "no", supported: false, reason: authIssue ?? issues[0] ?? "auth_unknown" };
|
|
663
751
|
}
|
|
664
752
|
return { status: "yes", supported: true };
|
|
665
753
|
}
|
|
666
754
|
|
|
755
|
+
function tryHarnessCommand(
|
|
756
|
+
candidate: string,
|
|
757
|
+
entry: HarnessCatalogEntry,
|
|
758
|
+
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
759
|
+
timeoutMs: number,
|
|
760
|
+
): { command: string; version: string | undefined } | undefined {
|
|
761
|
+
const result = runCommand(candidate, entry.versionArgs, timeoutMs);
|
|
762
|
+
if (result.error === "ENOENT") return undefined;
|
|
763
|
+
if (result.error && result.code === null && !result.stdout && !result.stderr) return undefined;
|
|
764
|
+
return {
|
|
765
|
+
command: candidate,
|
|
766
|
+
version: firstLine(result.stdout) ?? firstLine(result.stderr),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function detectHarnessCommand(
|
|
771
|
+
entry: HarnessCatalogEntry,
|
|
772
|
+
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
773
|
+
timeoutMs: number,
|
|
774
|
+
platform: NodeJS.Platform,
|
|
775
|
+
probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
|
|
776
|
+
): { command: string; version: string | undefined } | undefined {
|
|
777
|
+
const candidates = entry.commands.flatMap((base) => [...harnessCommandCandidates(base, platform)]);
|
|
778
|
+
for (const candidate of candidates) {
|
|
779
|
+
if (isWindowsHarnessShim(candidate)) continue;
|
|
780
|
+
const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
|
|
781
|
+
if (found) return found;
|
|
782
|
+
}
|
|
783
|
+
const inner = findWinNpmInnerExe(entry.id, {
|
|
784
|
+
platform,
|
|
785
|
+
pathEnv: probe.env?.PATH ?? process.env.PATH ?? "",
|
|
786
|
+
existsSync: probe.existsSync,
|
|
787
|
+
});
|
|
788
|
+
if (inner) {
|
|
789
|
+
const found = tryHarnessCommand(inner, entry, runCommand, timeoutMs);
|
|
790
|
+
if (found) return found;
|
|
791
|
+
}
|
|
792
|
+
for (const candidate of candidates) {
|
|
793
|
+
if (!isWindowsHarnessShim(candidate)) continue;
|
|
794
|
+
const found = tryHarnessCommand(candidate, entry, runCommand, timeoutMs);
|
|
795
|
+
if (found) return found;
|
|
796
|
+
}
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
|
|
667
800
|
function probeEntry(
|
|
668
801
|
entry: HarnessCatalogEntry,
|
|
669
802
|
runCommand: (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult,
|
|
670
803
|
timeoutMs: number,
|
|
804
|
+
platform: NodeJS.Platform,
|
|
805
|
+
probe: Pick<HarnessProbeOptions, "env" | "existsSync"> = {},
|
|
671
806
|
): HarnessStatus {
|
|
672
807
|
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
|
-
}
|
|
808
|
+
const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, probe);
|
|
809
|
+
const detected = Boolean(found);
|
|
810
|
+
const command = found?.command;
|
|
811
|
+
const version = found?.version;
|
|
685
812
|
let authenticated: boolean | null = null;
|
|
813
|
+
let authMethod: HarnessStatus["authMethod"];
|
|
686
814
|
if (!detected) authenticated = false;
|
|
687
815
|
else if (entry.id === "pi") {
|
|
688
816
|
authenticated = null;
|
|
@@ -691,10 +819,14 @@ function probeEntry(
|
|
|
691
819
|
authenticated = null;
|
|
692
820
|
if (UNKNOWN_AUTH_HARNESSES.has(entry.id)) issues.push("auth_unknown");
|
|
693
821
|
} else if (command) {
|
|
694
|
-
|
|
822
|
+
// Auth CLIs may initialize hooks/OAuth state and exceed the short binary-detection timeout.
|
|
823
|
+
// Keep detection bounded while allowing the documented auth probe to complete.
|
|
824
|
+
const parsed = interpretAuth(entry.id, runCommand(command, entry.authArgs, Math.max(timeoutMs, 10_000)));
|
|
695
825
|
authenticated = parsed.authenticated;
|
|
826
|
+
authMethod = parsed.authMethod;
|
|
696
827
|
issues.push(...parsed.issues);
|
|
697
828
|
}
|
|
829
|
+
if (command && isWindowsHarnessShim(command)) issues.push("windows_shim");
|
|
698
830
|
const dispatch = resolveDispatchStatus(entry, detected, authenticated, issues);
|
|
699
831
|
return {
|
|
700
832
|
id: entry.id,
|
|
@@ -703,6 +835,7 @@ function probeEntry(
|
|
|
703
835
|
mode: entry.mode,
|
|
704
836
|
detected,
|
|
705
837
|
authenticated,
|
|
838
|
+
...(authMethod ? { authMethod } : {}),
|
|
706
839
|
dispatch,
|
|
707
840
|
...(command ? { command } : {}),
|
|
708
841
|
...(version ? { version } : {}),
|
|
@@ -718,15 +851,16 @@ function probeEntry(
|
|
|
718
851
|
export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInventory {
|
|
719
852
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
720
853
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
854
|
+
const platform = options.platform ?? process.platform;
|
|
721
855
|
return {
|
|
722
856
|
defaultHarness: DEFAULT_HARNESS,
|
|
723
|
-
harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs)),
|
|
857
|
+
harnesses: BUILTIN_HARNESSES.map((entry) => probeEntry(entry, runCommand, timeoutMs, platform, options)),
|
|
724
858
|
};
|
|
725
859
|
}
|
|
726
860
|
|
|
727
861
|
export function eligibleHarnesses(inventory: HarnessInventory): readonly string[] {
|
|
728
862
|
const eligible = inventory.harnesses
|
|
729
|
-
.filter((entry) => entry.detected && entry.authenticated === true)
|
|
863
|
+
.filter((entry) => entry !== undefined && entry.detected && entry.authenticated === true)
|
|
730
864
|
.map((entry) => entry.id);
|
|
731
865
|
if (eligible.length === 0) throw new Error("no_authenticated_harness");
|
|
732
866
|
return eligible;
|
|
@@ -785,7 +919,7 @@ export function validateHarnessModelPair(
|
|
|
785
919
|
return { valid: true };
|
|
786
920
|
}
|
|
787
921
|
|
|
788
|
-
if (harnessId === "agy"
|
|
922
|
+
if (harnessId === "agy") {
|
|
789
923
|
if (provider && provider !== "google") {
|
|
790
924
|
return { valid: false, issue: "harness_unhosted_model", message: `harness ${harnessId} does not host provider ${provider}` };
|
|
791
925
|
}
|
|
@@ -799,7 +933,7 @@ export function validateHarnessModelPair(
|
|
|
799
933
|
if (provider && provider !== "moonshot") {
|
|
800
934
|
return { valid: false, issue: "harness_unhosted_model", message: `harness kimi does not host provider ${provider}` };
|
|
801
935
|
}
|
|
802
|
-
if (model && !/^(kimi|moonshot)
|
|
936
|
+
if (model && !/^(kimi|moonshot)(?:-|$)/i.test(model) && !/^kimi-for-coding(?:-|$)/i.test(model)) {
|
|
803
937
|
return { valid: false, issue: "harness_unhosted_model", message: `harness kimi only hosts kimi/moonshot models, received ${model}` };
|
|
804
938
|
}
|
|
805
939
|
return { valid: true };
|
|
@@ -843,11 +977,14 @@ export interface HarnessAssignmentProbeOptions {
|
|
|
843
977
|
env?: NodeJS.ProcessEnv | undefined;
|
|
844
978
|
runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult) | undefined;
|
|
845
979
|
timeoutMs?: number | undefined;
|
|
980
|
+
platform?: NodeJS.Platform | undefined;
|
|
981
|
+
existsSync?: ((path: string) => boolean) | undefined;
|
|
846
982
|
}
|
|
847
983
|
|
|
848
984
|
export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions): HarnessStatus {
|
|
849
985
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
850
986
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
987
|
+
const platform = options.platform ?? process.platform;
|
|
851
988
|
const entry = BUILTIN_HARNESSES.find((candidate) => candidate.id === options.harness);
|
|
852
989
|
if (!entry) {
|
|
853
990
|
return {
|
|
@@ -884,18 +1021,11 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
884
1021
|
}
|
|
885
1022
|
}
|
|
886
1023
|
|
|
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
|
-
}
|
|
1024
|
+
const found = detectHarnessCommand(entry, runCommand, timeoutMs, platform, options);
|
|
1025
|
+
const detected = Boolean(found);
|
|
1026
|
+
const command = found?.command;
|
|
1027
|
+
const version = found?.version;
|
|
1028
|
+
const shimIssue = command && isWindowsHarnessShim(command) ? ["windows_shim"] : [];
|
|
899
1029
|
|
|
900
1030
|
if (!detected || !command) {
|
|
901
1031
|
return {
|
|
@@ -932,7 +1062,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
932
1062
|
extensions: Boolean(entry.update.extensions?.length),
|
|
933
1063
|
models: Boolean(entry.update.models?.length),
|
|
934
1064
|
},
|
|
935
|
-
issues: ["auth_context_required"],
|
|
1065
|
+
issues: ["auth_context_required", ...shimIssue],
|
|
936
1066
|
};
|
|
937
1067
|
}
|
|
938
1068
|
|
|
@@ -960,6 +1090,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
960
1090
|
authenticated = false;
|
|
961
1091
|
issues.push("not_authenticated");
|
|
962
1092
|
}
|
|
1093
|
+
issues.push(...shimIssue);
|
|
963
1094
|
|
|
964
1095
|
const dispatch = resolveDispatchStatus(entry, true, authenticated, issues);
|
|
965
1096
|
|
|
@@ -982,7 +1113,56 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
982
1113
|
};
|
|
983
1114
|
}
|
|
984
1115
|
|
|
985
|
-
return probeEntry(entry, runCommand, timeoutMs);
|
|
1116
|
+
return probeEntry(entry, runCommand, timeoutMs, platform);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
export interface AsyncHarnessAssignmentProbeOptions extends Omit<HarnessAssignmentProbeOptions, "runCommand"> {
|
|
1120
|
+
signal?: AbortSignal | undefined;
|
|
1121
|
+
runCommand?: ((command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult | Promise<HarnessCommandResult>) | undefined;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
class PendingHarnessCommand {
|
|
1125
|
+
command: string;
|
|
1126
|
+
args: readonly string[];
|
|
1127
|
+
timeoutMs: number;
|
|
1128
|
+
key: string;
|
|
1129
|
+
constructor(command: string, args: readonly string[], timeoutMs: number, key: string) {
|
|
1130
|
+
this.command = command;
|
|
1131
|
+
this.args = [...args];
|
|
1132
|
+
this.timeoutMs = timeoutMs;
|
|
1133
|
+
this.key = key;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Execute the existing probe policy without blocking the Runtime event loop.
|
|
1139
|
+
* Replay pure policy decisions from per-call command observations, yielding for
|
|
1140
|
+
* every missing observation. This reuses the exact auth/model/brake parsers,
|
|
1141
|
+
* deduplicates repeated detection, and never caches auth across assignments.
|
|
1142
|
+
*/
|
|
1143
|
+
export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
|
|
1144
|
+
const observed = new Map<string, HarnessCommandResult>();
|
|
1145
|
+
const run = options.runCommand ?? (async (command, args, timeoutMs) => {
|
|
1146
|
+
const result = await defaultSpawn(command, args, { env: options.env, timeoutMs, signal: options.signal });
|
|
1147
|
+
const error = result.error?.message ?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
|
|
1148
|
+
return { ok: result.code === 0 && !error, code: result.code, stdout: result.stdout, stderr: result.stderr,
|
|
1149
|
+
...(error ? { error } : {}) };
|
|
1150
|
+
});
|
|
1151
|
+
for (let commands = 0; commands <= 32; commands++) {
|
|
1152
|
+
try {
|
|
1153
|
+
return probeHarnessAssignment({ ...options, runCommand(command, args, timeoutMs) {
|
|
1154
|
+
const key = JSON.stringify([command, args, timeoutMs]);
|
|
1155
|
+
const result = observed.get(key);
|
|
1156
|
+
if (result) return result;
|
|
1157
|
+
throw new PendingHarnessCommand(command, args, timeoutMs, key);
|
|
1158
|
+
} });
|
|
1159
|
+
} catch (pending) {
|
|
1160
|
+
if (!(pending instanceof PendingHarnessCommand)) throw pending;
|
|
1161
|
+
if (commands === 32) throw new Error("auth_probe_command_limit");
|
|
1162
|
+
observed.set(pending.key, await run(pending.command, pending.args, pending.timeoutMs));
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
throw new Error("auth_probe_command_limit");
|
|
986
1166
|
}
|
|
987
1167
|
|
|
988
1168
|
export function probeHarnessesForModel(
|
|
@@ -991,6 +1171,7 @@ export function probeHarnessesForModel(
|
|
|
991
1171
|
): HarnessInventory {
|
|
992
1172
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
993
1173
|
const runCommand = options.runCommand ?? defaultRunner(options.env ?? process.env);
|
|
1174
|
+
const platform = options.platform ?? process.platform;
|
|
994
1175
|
return {
|
|
995
1176
|
defaultHarness: DEFAULT_HARNESS,
|
|
996
1177
|
harnesses: BUILTIN_HARNESSES.map((entry) =>
|
|
@@ -1000,6 +1181,9 @@ export function probeHarnessesForModel(
|
|
|
1000
1181
|
model: modelSpec.model,
|
|
1001
1182
|
runCommand,
|
|
1002
1183
|
timeoutMs,
|
|
1184
|
+
platform,
|
|
1185
|
+
env: options.env,
|
|
1186
|
+
existsSync: options.existsSync,
|
|
1003
1187
|
})
|
|
1004
1188
|
),
|
|
1005
1189
|
};
|