@martinloop/mcp 0.3.0 → 0.3.2

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 (48) hide show
  1. package/README.md +5 -4
  2. package/dist/package-version.d.ts +1 -1
  3. package/dist/package-version.js +1 -1
  4. package/dist/server-validation.js +2 -2
  5. package/dist/server.js +72 -10
  6. package/dist/tools/doctor.d.ts +27 -0
  7. package/dist/tools/doctor.js +39 -11
  8. package/dist/tools/get-run.d.ts +2 -1
  9. package/dist/tools/get-run.js +1 -0
  10. package/dist/tools/get-verification-results.d.ts +2 -1
  11. package/dist/tools/get-verification-results.js +1 -0
  12. package/dist/tools/plan.js +4 -2
  13. package/dist/tools/preflight.d.ts +27 -0
  14. package/dist/tools/preflight.js +44 -20
  15. package/dist/tools/run-dossier.d.ts +2 -1
  16. package/dist/tools/run-dossier.js +1 -0
  17. package/dist/tools/run-loop.d.ts +5 -1
  18. package/dist/tools/run-loop.js +20 -8
  19. package/dist/tools/run-store.js +67 -15
  20. package/dist/tools/tool-support.d.ts +2 -0
  21. package/dist/tools/tool-support.js +49 -13
  22. package/dist/tools/workflow-governance.d.ts +19 -3
  23. package/dist/tools/workflow-governance.js +107 -55
  24. package/dist/vendor/adapters/claude-cli.d.ts +20 -3
  25. package/dist/vendor/adapters/claude-cli.js +193 -33
  26. package/dist/vendor/adapters/cli-bridge.d.ts +45 -0
  27. package/dist/vendor/adapters/cli-bridge.js +107 -39
  28. package/dist/vendor/adapters/codex-launcher.d.ts +32 -0
  29. package/dist/vendor/adapters/codex-launcher.js +409 -118
  30. package/dist/vendor/adapters/openai-compatible.js +8 -2
  31. package/dist/vendor/adapters/runtime-support.js +1 -0
  32. package/dist/vendor/adapters/stub-direct-provider.js +3 -0
  33. package/dist/vendor/adapters/verifier-only.d.ts +2 -0
  34. package/dist/vendor/adapters/verifier-only.js +9 -3
  35. package/dist/vendor/contracts/index.d.ts +2 -1
  36. package/dist/vendor/contracts/index.js +14 -0
  37. package/dist/vendor/core/context-integrity.js +28 -3
  38. package/dist/vendor/core/grounding.d.ts +1 -0
  39. package/dist/vendor/core/grounding.js +6 -2
  40. package/dist/vendor/core/index.d.ts +1 -0
  41. package/dist/vendor/core/index.js +25 -6
  42. package/dist/vendor/core/leash.js +90 -8
  43. package/dist/vendor/core/persistence/integrity.d.ts +1 -1
  44. package/dist/vendor/core/persistence/integrity.js +15 -6
  45. package/dist/workflow-state.d.ts +9 -0
  46. package/dist/workflow-state.js +44 -3
  47. package/package.json +2 -2
  48. package/server.json +2 -2
@@ -5,11 +5,21 @@ export interface CliCommandAvailability {
5
5
  locator: string;
6
6
  detail: string;
7
7
  resolvedPath?: string;
8
+ candidatePaths?: string[];
8
9
  }
9
10
  export type CodexHostPlatform = "windows" | "linux" | "wsl" | "macos";
11
+ export type CodexInstallKind = "missing" | "native" | "windows_shim" | "windows_mounted_path";
12
+ export type CodexInvocationMode = "direct" | "cmd_shell" | "powershell";
10
13
  export interface CodexHostDiagnosis {
11
14
  hostPlatform: CodexHostPlatform;
12
15
  nativeInstallValid: boolean;
16
+ installKind: CodexInstallKind;
17
+ invocationMode: CodexInvocationMode;
18
+ sandboxMode: "workspace-write";
19
+ sandboxCompatible: boolean;
20
+ resolvedPath?: string;
21
+ nativeDependencyStatus?: "unknown" | "missing";
22
+ nativeDependencyPackage?: string;
13
23
  warnings: string[];
14
24
  remediation?: string;
15
25
  }
@@ -23,10 +33,32 @@ export interface CodexLaunchProbeResult {
23
33
  exitCode?: number;
24
34
  stdout?: string;
25
35
  stderr?: string;
36
+ candidateProbeResults?: CodexProbeCandidateResult[];
37
+ }
38
+ export interface CodexExecArgsOptions {
39
+ workingDirectory: string;
40
+ sandbox?: "read-only" | "workspace-write" | "danger-full-access";
41
+ model?: string;
42
+ extraArgs?: string[];
43
+ mode?: "prompt" | "probe";
26
44
  }
27
45
  type SpawnSyncLike = typeof spawnSync;
46
+ export interface CodexProbeCandidateResult {
47
+ path: string;
48
+ installKind: CodexInstallKind;
49
+ invocationMode: CodexInvocationMode;
50
+ nativeInstallValid: boolean;
51
+ sandboxCompatible: boolean;
52
+ launchReady: boolean;
53
+ summary: string;
54
+ remediation?: string;
55
+ nativeDependencyStatus?: "unknown" | "missing";
56
+ nativeDependencyPackage?: string;
57
+ }
58
+ export declare function buildCodexExecArgs(options: CodexExecArgsOptions): string[];
28
59
  export declare function resolveCliCommandAvailability(command: string, options?: {
29
60
  platform?: NodeJS.Platform;
61
+ env?: NodeJS.ProcessEnv;
30
62
  spawnSyncImpl?: SpawnSyncLike;
31
63
  }): CliCommandAvailability;
32
64
  export declare function detectCodexHostPlatform(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): CodexHostPlatform;
@@ -1,6 +1,22 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { dirname, extname, resolve } from "node:path";
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { dirname, extname, join, resolve } from "node:path";
4
+ const codexLaunchProbeCache = new Map();
5
+ const CODEX_LAUNCH_PROBE_PROMPT = [
6
+ "You are validating MartinLoop Codex host readiness.",
7
+ "Do not edit files.",
8
+ "Use the shell command executor exactly once to run: git status --short -- .",
9
+ "Do not use MCP tools, Node REPL, or any fallback tool if shell execution fails.",
10
+ "If the shell command succeeds, reply with READY only.",
11
+ "If it fails, reply with the exact failure in one sentence."
12
+ ].join("\n");
13
+ function buildProbeCacheKey(input) {
14
+ return JSON.stringify({
15
+ workingDirectory: resolve(input.workingDirectory),
16
+ platform: input.platform,
17
+ candidatePaths: input.candidatePaths
18
+ });
19
+ }
4
20
  function isInsideGitRepository(workingDirectory) {
5
21
  let current = resolve(workingDirectory);
6
22
  while (true) {
@@ -14,50 +30,90 @@ function isInsideGitRepository(workingDirectory) {
14
30
  current = parent;
15
31
  }
16
32
  }
17
- function selectResolvedPath(candidates, platform) {
18
- const cleaned = candidates
19
- .map((line) => line.trim())
20
- .filter(Boolean);
21
- if (cleaned.length === 0) {
22
- return undefined;
23
- }
24
- if (platform !== "win32") {
25
- return cleaned[0];
26
- }
27
- const preference = (candidate) => {
28
- switch (extname(candidate).toLowerCase()) {
29
- case ".cmd":
30
- return 0;
31
- case ".bat":
32
- return 1;
33
- case ".ps1":
34
- return 2;
35
- case ".exe":
36
- return 3;
37
- case "":
38
- return 4;
39
- default:
40
- return 5;
41
- }
33
+ function normalizeCandidates(lines) {
34
+ return [...new Set(lines.map((line) => line.trim()).filter(Boolean))];
35
+ }
36
+ function readLocatorCandidates(command, platform, spawnSyncImpl) {
37
+ const locator = platform === "win32" ? "where.exe" : "which";
38
+ const result = spawnSyncImpl(locator, [command], {
39
+ encoding: "utf8",
40
+ stdio: ["ignore", "pipe", "pipe"]
41
+ });
42
+ return {
43
+ locator,
44
+ candidates: result.status === 0
45
+ ? normalizeCandidates((result.stdout ?? "").split(/\r?\n/u))
46
+ : [],
47
+ foundOnPath: result.status === 0
42
48
  };
43
- const [firstCandidate, ...remainingCandidates] = cleaned;
44
- if (firstCandidate === undefined) {
45
- return undefined;
49
+ }
50
+ function discoverWindowsDesktopCodexCandidates(env) {
51
+ const localAppData = env["LOCALAPPDATA"];
52
+ if (!localAppData) {
53
+ return [];
54
+ }
55
+ const baseDirectory = join(localAppData, "OpenAI", "Codex", "bin");
56
+ if (!existsSync(baseDirectory)) {
57
+ return [];
46
58
  }
47
- let bestCandidate = firstCandidate;
48
- let bestPreference = preference(bestCandidate);
49
- for (const candidate of remainingCandidates) {
50
- const candidatePreference = preference(candidate);
51
- if (candidatePreference < bestPreference) {
52
- bestCandidate = candidate;
53
- bestPreference = candidatePreference;
59
+ const candidates = [];
60
+ const directCandidate = join(baseDirectory, "codex.exe");
61
+ if (existsSync(directCandidate)) {
62
+ candidates.push({ path: directCandidate, mtimeMs: statSync(directCandidate).mtimeMs });
63
+ }
64
+ for (const entry of readdirSync(baseDirectory, { withFileTypes: true })) {
65
+ if (!entry.isDirectory()) {
66
+ continue;
67
+ }
68
+ const candidate = join(baseDirectory, entry.name, "codex.exe");
69
+ if (!existsSync(candidate)) {
70
+ continue;
54
71
  }
72
+ candidates.push({ path: candidate, mtimeMs: statSync(candidate).mtimeMs });
73
+ }
74
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
75
+ return normalizeCandidates(candidates.map((candidate) => candidate.path));
76
+ }
77
+ function codexProbePreference(diagnosis) {
78
+ if (diagnosis.installKind === "native" && diagnosis.invocationMode === "direct") {
79
+ return 0;
55
80
  }
56
- return bestCandidate;
81
+ if (diagnosis.installKind === "native") {
82
+ return 1;
83
+ }
84
+ if (diagnosis.installKind === "windows_shim") {
85
+ return 2;
86
+ }
87
+ return 3;
88
+ }
89
+ function buildProbeCandidates(input) {
90
+ const pathCandidates = normalizeCandidates(input.availability.candidatePaths ?? [input.availability.resolvedPath ?? input.availability.command]);
91
+ const desktopCandidates = input.platform === "win32" && input.includeDesktopCandidates
92
+ ? discoverWindowsDesktopCodexCandidates(input.env).filter((candidatePath) => !pathCandidates.includes(candidatePath))
93
+ : [];
94
+ return [...pathCandidates, ...desktopCandidates]
95
+ .map((path, discoveryIndex) => {
96
+ const diagnosis = diagnoseCodexHost({
97
+ ...input.availability,
98
+ resolvedPath: path
99
+ }, {
100
+ env: input.env,
101
+ platform: input.platform
102
+ });
103
+ return {
104
+ path,
105
+ diagnosis,
106
+ preference: input.platform === "win32" ? codexProbePreference(diagnosis) : 0,
107
+ discoveryIndex
108
+ };
109
+ })
110
+ .sort((left, right) => left.preference === right.preference
111
+ ? left.discoveryIndex - right.discoveryIndex
112
+ : left.preference - right.preference);
57
113
  }
58
114
  function buildProbeCommand(command, args, platform) {
59
115
  if (platform !== "win32") {
60
- return { command, args };
116
+ return { command, args, invocationMode: "direct" };
61
117
  }
62
118
  const extension = extname(command).toLowerCase();
63
119
  switch (extension) {
@@ -65,40 +121,149 @@ function buildProbeCommand(command, args, platform) {
65
121
  case ".bat":
66
122
  return {
67
123
  command: process.env.ComSpec || "cmd.exe",
68
- args: ["/d", "/c", command, ...args]
124
+ args: ["/d", "/c", command, ...args],
125
+ invocationMode: "cmd_shell"
69
126
  };
70
127
  case ".ps1":
71
128
  return {
72
129
  command: "powershell.exe",
73
- args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args]
130
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args],
131
+ invocationMode: "powershell"
74
132
  };
75
133
  default:
76
- return { command, args };
134
+ return { command, args, invocationMode: "direct" };
135
+ }
136
+ }
137
+ function detectInstallKind(resolvedPath, hostPlatform) {
138
+ if (!resolvedPath) {
139
+ return "missing";
140
+ }
141
+ const normalizedPath = resolvedPath.replace(/\\/gu, "/").toLowerCase();
142
+ const looksWindowsShim = normalizedPath.endsWith(".cmd") ||
143
+ normalizedPath.endsWith(".bat") ||
144
+ normalizedPath.endsWith(".ps1") ||
145
+ normalizedPath.includes("/appdata/roaming/npm/");
146
+ const looksMountedWindowsPath = normalizedPath.startsWith("/mnt/c/");
147
+ if ((hostPlatform === "linux" || hostPlatform === "wsl") && looksMountedWindowsPath) {
148
+ return "windows_mounted_path";
77
149
  }
150
+ if (looksWindowsShim) {
151
+ return "windows_shim";
152
+ }
153
+ return "native";
154
+ }
155
+ function detectInvocationMode(resolvedPath, platform) {
156
+ if (platform !== "win32" || !resolvedPath) {
157
+ return "direct";
158
+ }
159
+ const extension = extname(resolvedPath).toLowerCase();
160
+ if (extension === ".ps1") {
161
+ return "powershell";
162
+ }
163
+ if (extension === ".cmd" || extension === ".bat") {
164
+ return "cmd_shell";
165
+ }
166
+ return "direct";
167
+ }
168
+ function classifyProbeFailure(stderr, stdout, diagnosis) {
169
+ const combined = `${stderr}\n${stdout}`;
170
+ if (/@openai\/codex-linux-x64/iu.test(combined) ||
171
+ /cannot find module ['"]@openai\/codex-linux-x64['"]/iu.test(combined)) {
172
+ const warnings = [...diagnosis.warnings];
173
+ const missingDependencyWarning = "Codex is missing the native Linux package '@openai/codex-linux-x64' required for this host.";
174
+ if (!warnings.includes(missingDependencyWarning)) {
175
+ warnings.push(missingDependencyWarning);
176
+ }
177
+ return {
178
+ summary: "Codex native dependency '@openai/codex-linux-x64' is missing for this Linux/WSL environment.",
179
+ diagnosis: {
180
+ ...diagnosis,
181
+ nativeInstallValid: false,
182
+ sandboxCompatible: false,
183
+ warnings,
184
+ nativeDependencyStatus: "missing",
185
+ nativeDependencyPackage: "@openai/codex-linux-x64",
186
+ remediation: "Reinstall Codex natively inside this Linux/WSL environment so the '@openai/codex-linux-x64' package is present before running governed Codex work."
187
+ }
188
+ };
189
+ }
190
+ if (/CreateProcessAsUserW failed:\s*5/iu.test(combined) ||
191
+ /windows sandbox: runner error: CreateProcessAsUserW failed:\s*5/iu.test(combined) ||
192
+ /spawn setup refresh/iu.test(combined)) {
193
+ const warnings = [...diagnosis.warnings];
194
+ const sandboxFailureWarning = "Codex workspace-write sandbox could not launch subprocesses on this Windows host.";
195
+ if (!warnings.includes(sandboxFailureWarning)) {
196
+ warnings.push(sandboxFailureWarning);
197
+ }
198
+ return {
199
+ summary: "Codex workspace-write sandbox could not launch subprocesses on this Windows host.",
200
+ diagnosis: {
201
+ ...diagnosis,
202
+ sandboxCompatible: false,
203
+ warnings,
204
+ remediation: "Update or reinstall Codex on this Windows host until `codex exec --sandbox workspace-write` can launch a simple shell command before running governed Codex work."
205
+ }
206
+ };
207
+ }
208
+ return {};
209
+ }
210
+ export function buildCodexExecArgs(options) {
211
+ const sandbox = options.sandbox ?? "workspace-write";
212
+ const modelArgs = options.model ? ["--model", options.model] : [];
213
+ const extraArgs = options.extraArgs ?? [];
214
+ return [
215
+ "exec",
216
+ "--cd",
217
+ options.workingDirectory,
218
+ "--sandbox",
219
+ sandbox,
220
+ "--json",
221
+ "--color",
222
+ "never",
223
+ ...modelArgs,
224
+ ...extraArgs,
225
+ "-"
226
+ ];
227
+ }
228
+ function parseCodexProbeEvents(stdout) {
229
+ return stdout
230
+ .split(/\r?\n/u)
231
+ .map((line) => line.trim())
232
+ .filter(Boolean)
233
+ .map((line) => {
234
+ try {
235
+ return JSON.parse(line);
236
+ }
237
+ catch {
238
+ return undefined;
239
+ }
240
+ })
241
+ .filter((event) => event !== undefined);
242
+ }
243
+ function hasSuccessfulCommandExecution(events) {
244
+ return events.some((event) => event.type === "item.completed" &&
245
+ event.item?.type === "command_execution" &&
246
+ event.item?.status === "completed" &&
247
+ event.item?.exit_code === 0);
78
248
  }
79
249
  export function resolveCliCommandAvailability(command, options = {}) {
80
250
  const platform = options.platform ?? process.platform;
81
251
  const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
82
- const locator = platform === "win32" ? "where.exe" : "which";
83
- const result = spawnSyncImpl(locator, [command], {
84
- encoding: "utf8",
85
- stdio: ["ignore", "pipe", "pipe"]
86
- });
87
- const resolvedPath = result.status === 0
88
- ? selectResolvedPath((result.stdout ?? "").split(/\r?\n/u), platform)
89
- : undefined;
90
- return result.status === 0
252
+ const discovery = readLocatorCandidates(command, platform, spawnSyncImpl);
253
+ const resolvedPath = discovery.candidates[0];
254
+ return discovery.candidates.length > 0
91
255
  ? {
92
256
  command,
93
257
  available: true,
94
- locator,
258
+ locator: discovery.locator,
95
259
  detail: `${command} is available on PATH.`,
96
- ...(resolvedPath ? { resolvedPath } : {})
260
+ ...(resolvedPath ? { resolvedPath } : {}),
261
+ candidatePaths: discovery.candidates
97
262
  }
98
263
  : {
99
264
  command,
100
265
  available: false,
101
- locator,
266
+ locator: discovery.locator,
102
267
  detail: `${command} is not available on PATH.`
103
268
  };
104
269
  }
@@ -116,26 +281,34 @@ export function detectCodexHostPlatform(env = process.env, platform = process.pl
116
281
  }
117
282
  export function diagnoseCodexHost(availability, options = {}) {
118
283
  const hostPlatform = detectCodexHostPlatform(options.env ?? process.env, options.platform ?? process.platform);
284
+ const resolvedPath = availability.resolvedPath;
285
+ const installKind = detectInstallKind(resolvedPath, hostPlatform);
286
+ const invocationMode = detectInvocationMode(resolvedPath, options.platform ?? process.platform);
119
287
  const warnings = [];
120
288
  if (!availability.available) {
121
289
  return {
122
290
  hostPlatform,
123
291
  nativeInstallValid: false,
292
+ installKind,
293
+ invocationMode,
294
+ sandboxMode: "workspace-write",
295
+ sandboxCompatible: false,
296
+ ...(resolvedPath ? { resolvedPath } : {}),
124
297
  warnings,
125
298
  remediation: "Install or expose the Codex CLI on PATH before running governed Codex work."
126
299
  };
127
300
  }
128
- const resolvedPath = availability.resolvedPath?.replace(/\\/gu, "/").toLowerCase() ?? "";
129
- const looksWindowsShim = resolvedPath.endsWith(".cmd") ||
130
- resolvedPath.endsWith(".bat") ||
131
- resolvedPath.endsWith(".ps1") ||
132
- resolvedPath.includes("/appdata/roaming/npm/");
133
- const looksMountedWindowsPath = resolvedPath.startsWith("/mnt/c/");
134
- if ((hostPlatform === "linux" || hostPlatform === "wsl") && (looksWindowsShim || looksMountedWindowsPath)) {
301
+ if ((hostPlatform === "linux" || hostPlatform === "wsl") &&
302
+ (installKind === "windows_shim" || installKind === "windows_mounted_path")) {
135
303
  warnings.push("Codex resolves to a Windows-hosted install from a Linux/WSL environment.");
136
304
  return {
137
305
  hostPlatform,
138
306
  nativeInstallValid: false,
307
+ installKind,
308
+ invocationMode,
309
+ sandboxMode: "workspace-write",
310
+ sandboxCompatible: false,
311
+ ...(resolvedPath ? { resolvedPath } : {}),
139
312
  warnings,
140
313
  remediation: "Install Codex natively inside this Linux/WSL environment instead of relying on a Windows PATH shim."
141
314
  };
@@ -143,6 +316,14 @@ export function diagnoseCodexHost(availability, options = {}) {
143
316
  return {
144
317
  hostPlatform,
145
318
  nativeInstallValid: true,
319
+ installKind,
320
+ invocationMode,
321
+ sandboxMode: "workspace-write",
322
+ sandboxCompatible: true,
323
+ ...(resolvedPath ? { resolvedPath } : {}),
324
+ ...(hostPlatform === "linux" || hostPlatform === "wsl"
325
+ ? { nativeDependencyStatus: "unknown" }
326
+ : {}),
146
327
  warnings
147
328
  };
148
329
  }
@@ -156,17 +337,10 @@ export function probeCodexLaunch(input) {
156
337
  env: input.env,
157
338
  platform: input.platform
158
339
  });
159
- const args = [
160
- "exec",
161
- "--cd",
162
- input.workingDirectory,
163
- "--sandbox",
164
- "workspace-write",
165
- "--json",
166
- "--color",
167
- "never",
168
- "--help"
169
- ];
340
+ const args = buildCodexExecArgs({
341
+ workingDirectory: input.workingDirectory,
342
+ mode: "probe"
343
+ });
170
344
  if (!availability.available) {
171
345
  return {
172
346
  ok: false,
@@ -177,16 +351,6 @@ export function probeCodexLaunch(input) {
177
351
  args
178
352
  };
179
353
  }
180
- if (!diagnosis.nativeInstallValid) {
181
- return {
182
- ok: false,
183
- summary: diagnosis.remediation ?? "Codex host installation is not valid for this environment.",
184
- availability,
185
- diagnosis,
186
- command: availability.resolvedPath ?? availability.command,
187
- args
188
- };
189
- }
190
354
  if (!isInsideGitRepository(input.workingDirectory)) {
191
355
  return {
192
356
  ok: false,
@@ -199,49 +363,176 @@ export function probeCodexLaunch(input) {
199
363
  }
200
364
  const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync;
201
365
  const platform = input.platform ?? process.platform;
202
- const spawnPlan = buildProbeCommand(availability.resolvedPath ?? availability.command, args, platform);
203
- const result = spawnSyncImpl(spawnPlan.command, spawnPlan.args, {
204
- cwd: input.workingDirectory,
205
- encoding: "utf8",
206
- stdio: ["ignore", "pipe", "pipe"]
366
+ const env = input.env ?? process.env;
367
+ const probeCandidates = buildProbeCandidates({
368
+ availability,
369
+ env,
370
+ platform,
371
+ includeDesktopCandidates: input.spawnSyncImpl === undefined
207
372
  });
208
- if (result.error) {
209
- return {
210
- ok: false,
211
- summary: `Codex launch probe failed: ${result.error.message}`,
212
- availability,
213
- diagnosis,
214
- command: availability.resolvedPath ?? availability.command,
215
- args,
216
- stderr: result.stderr ?? "",
217
- stdout: result.stdout ?? ""
218
- };
373
+ const candidatePaths = probeCandidates.map((candidate) => candidate.path);
374
+ const cacheKey = buildProbeCacheKey({
375
+ workingDirectory: input.workingDirectory,
376
+ platform,
377
+ candidatePaths
378
+ });
379
+ if (input.spawnSyncImpl === undefined) {
380
+ const cached = codexLaunchProbeCache.get(cacheKey);
381
+ if (cached) {
382
+ return cached;
383
+ }
219
384
  }
220
- if (result.status !== 0) {
221
- const stderr = (result.stderr ?? "").trim();
222
- return {
223
- ok: false,
224
- summary: stderr.length > 0
225
- ? `Codex launch probe failed: ${stderr}`
226
- : "Codex launch probe exited non-zero.",
227
- availability,
228
- diagnosis,
229
- command: availability.resolvedPath ?? availability.command,
230
- args,
385
+ const candidateResults = [];
386
+ const probeCandidatePath = (candidate) => {
387
+ const candidateDiagnosis = candidate.diagnosis;
388
+ if (!candidateDiagnosis.nativeInstallValid) {
389
+ candidateResults.push({
390
+ path: candidate.path,
391
+ diagnosis: candidateDiagnosis,
392
+ ok: false,
393
+ summary: candidateDiagnosis.remediation ?? "Codex host installation is not valid for this environment."
394
+ });
395
+ return false;
396
+ }
397
+ const spawnPlan = buildProbeCommand(candidate.path, args, platform);
398
+ const result = spawnSyncImpl(spawnPlan.command, spawnPlan.args, {
399
+ cwd: input.workingDirectory,
400
+ encoding: "utf8",
401
+ stdio: ["pipe", "pipe", "pipe"],
402
+ input: CODEX_LAUNCH_PROBE_PROMPT
403
+ });
404
+ const probedDiagnosis = {
405
+ ...candidateDiagnosis,
406
+ invocationMode: spawnPlan.invocationMode
407
+ };
408
+ if (result.error) {
409
+ const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis);
410
+ candidateResults.push({
411
+ path: candidate.path,
412
+ diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis,
413
+ ok: false,
414
+ summary: classifiedFailure.summary ?? `Codex launch probe failed: ${result.error.message}`,
415
+ stderr: result.stderr ?? "",
416
+ stdout: result.stdout ?? ""
417
+ });
418
+ return false;
419
+ }
420
+ if (result.status !== 0) {
421
+ const stderr = (result.stderr ?? "").trim();
422
+ const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis);
423
+ candidateResults.push({
424
+ path: candidate.path,
425
+ diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis,
426
+ ok: false,
427
+ summary: classifiedFailure.summary ??
428
+ (stderr.length > 0 ? `Codex launch probe failed: ${stderr}` : "Codex launch probe exited non-zero."),
429
+ exitCode: result.status ?? undefined,
430
+ stderr: result.stderr ?? "",
431
+ stdout: result.stdout ?? ""
432
+ });
433
+ return false;
434
+ }
435
+ const events = parseCodexProbeEvents(result.stdout ?? "");
436
+ if (!hasSuccessfulCommandExecution(events)) {
437
+ const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis);
438
+ candidateResults.push({
439
+ path: candidate.path,
440
+ diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis,
441
+ ok: false,
442
+ summary: classifiedFailure.summary ?? "Codex launch probe did not complete a shell command successfully.",
443
+ exitCode: result.status ?? undefined,
444
+ stderr: result.stderr ?? "",
445
+ stdout: result.stdout ?? ""
446
+ });
447
+ return false;
448
+ }
449
+ candidateResults.push({
450
+ path: candidate.path,
451
+ diagnosis: probedDiagnosis,
452
+ ok: true,
453
+ summary: "Codex exec prompt-and-shell probe passed for the current MartinLoop invocation shape.",
231
454
  exitCode: result.status ?? undefined,
232
455
  stderr: result.stderr ?? "",
233
456
  stdout: result.stdout ?? ""
457
+ });
458
+ return true;
459
+ };
460
+ for (const candidate of probeCandidates) {
461
+ if (probeCandidatePath(candidate)) {
462
+ break;
463
+ }
464
+ }
465
+ const successfulCandidates = candidateResults.filter((candidate) => candidate.ok);
466
+ const selectedCandidate = successfulCandidates[0];
467
+ const candidateProbeResults = candidateResults.map((candidate) => ({
468
+ path: candidate.path,
469
+ installKind: candidate.diagnosis.installKind,
470
+ invocationMode: candidate.diagnosis.invocationMode,
471
+ nativeInstallValid: candidate.diagnosis.nativeInstallValid,
472
+ sandboxCompatible: candidate.diagnosis.sandboxCompatible,
473
+ launchReady: candidate.ok,
474
+ summary: candidate.summary,
475
+ ...(candidate.diagnosis.remediation ? { remediation: candidate.diagnosis.remediation } : {}),
476
+ ...(candidate.diagnosis.nativeDependencyStatus
477
+ ? { nativeDependencyStatus: candidate.diagnosis.nativeDependencyStatus }
478
+ : {}),
479
+ ...(candidate.diagnosis.nativeDependencyPackage
480
+ ? { nativeDependencyPackage: candidate.diagnosis.nativeDependencyPackage }
481
+ : {})
482
+ }));
483
+ if (selectedCandidate) {
484
+ const successResult = {
485
+ ok: true,
486
+ summary: selectedCandidate.summary,
487
+ availability: {
488
+ ...availability,
489
+ resolvedPath: selectedCandidate.path,
490
+ candidatePaths
491
+ },
492
+ diagnosis: {
493
+ ...selectedCandidate.diagnosis,
494
+ resolvedPath: selectedCandidate.path
495
+ },
496
+ command: selectedCandidate.path,
497
+ args,
498
+ exitCode: selectedCandidate.exitCode,
499
+ stderr: selectedCandidate.stderr,
500
+ stdout: selectedCandidate.stdout,
501
+ candidateProbeResults
234
502
  };
503
+ if (input.spawnSyncImpl === undefined) {
504
+ codexLaunchProbeCache.set(cacheKey, successResult);
505
+ }
506
+ return successResult;
235
507
  }
236
- return {
237
- ok: true,
238
- summary: "Codex exec launch probe passed for the current MartinLoop invocation shape.",
239
- availability,
240
- diagnosis,
241
- command: availability.resolvedPath ?? availability.command,
508
+ const bestFailure = platform === "win32"
509
+ ? candidateResults.find((candidate) => candidate.diagnosis.nativeInstallValid && candidate.diagnosis.installKind === "native") ??
510
+ candidateResults.find((candidate) => candidate.diagnosis.nativeInstallValid) ??
511
+ candidateResults[0]
512
+ : candidateResults[0];
513
+ const failureResult = {
514
+ ok: false,
515
+ summary: bestFailure?.summary ?? diagnosis.remediation ?? "Codex launch probe failed.",
516
+ availability: {
517
+ ...availability,
518
+ ...(bestFailure ? { resolvedPath: bestFailure.path } : {}),
519
+ ...(candidatePaths.length ? { candidatePaths } : {})
520
+ },
521
+ diagnosis: bestFailure
522
+ ? {
523
+ ...bestFailure.diagnosis,
524
+ resolvedPath: bestFailure.path
525
+ }
526
+ : diagnosis,
527
+ command: bestFailure?.path ?? availability.resolvedPath ?? availability.command,
242
528
  args,
243
- exitCode: result.status ?? undefined,
244
- stderr: result.stderr ?? "",
245
- stdout: result.stdout ?? ""
529
+ exitCode: bestFailure?.exitCode,
530
+ stderr: bestFailure?.stderr,
531
+ stdout: bestFailure?.stdout,
532
+ candidateProbeResults
246
533
  };
534
+ if (input.spawnSyncImpl === undefined) {
535
+ codexLaunchProbeCache.set(cacheKey, failureResult);
536
+ }
537
+ return failureResult;
247
538
  }