@martinloop/mcp 0.3.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +90 -106
  2. package/dist/discovery-metadata.d.ts +1 -1
  3. package/dist/discovery-metadata.js +1 -1
  4. package/dist/resources.d.ts +1 -2
  5. package/dist/resources.js +190 -238
  6. package/dist/server-validation.d.ts +1 -1
  7. package/dist/server-validation.js +16 -11
  8. package/dist/server.d.ts +23 -0
  9. package/dist/server.js +152 -7
  10. package/dist/tools/run-dossier.d.ts +3 -1
  11. package/dist/tools/run-dossier.js +40 -2
  12. package/dist/tools/run-loop.d.ts +1 -4
  13. package/dist/tools/run-loop.js +6 -15
  14. package/dist/tools/tool-support.d.ts +2 -1
  15. package/dist/tools/tool-support.js +1 -12
  16. package/dist/vendor/adapters/claude-cli.js +38 -35
  17. package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
  18. package/dist/vendor/adapters/cli-bridge.js +54 -13
  19. package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
  20. package/dist/vendor/adapters/codex-launcher.js +118 -19
  21. package/dist/vendor/adapters/index.d.ts +1 -3
  22. package/dist/vendor/adapters/index.js +1 -3
  23. package/dist/vendor/adapters/openai-compatible.js +57 -18
  24. package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
  25. package/dist/vendor/contracts/context-handoff.d.ts +100 -0
  26. package/dist/vendor/contracts/context-handoff.js +11 -0
  27. package/dist/vendor/contracts/context-manifest.d.ts +195 -0
  28. package/dist/vendor/contracts/context-manifest.js +17 -0
  29. package/dist/vendor/contracts/context-shadow.d.ts +75 -0
  30. package/dist/vendor/contracts/context-shadow.js +8 -0
  31. package/dist/vendor/contracts/exits.d.ts +126 -0
  32. package/dist/vendor/contracts/exits.js +32 -0
  33. package/dist/vendor/contracts/index.d.ts +39 -2
  34. package/dist/vendor/contracts/index.js +22 -0
  35. package/dist/vendor/contracts/mission.d.ts +102 -0
  36. package/dist/vendor/contracts/mission.js +64 -0
  37. package/dist/vendor/contracts/operator.d.ts +3 -1
  38. package/dist/vendor/contracts/operator.js +2 -1
  39. package/dist/vendor/contracts/verified-handoff.d.ts +100 -0
  40. package/dist/vendor/contracts/verified-handoff.js +35 -0
  41. package/dist/vendor/core/context-chain-gate.d.ts +89 -0
  42. package/dist/vendor/core/context-chain-gate.js +173 -0
  43. package/dist/vendor/core/context-compiler.d.ts +53 -0
  44. package/dist/vendor/core/context-compiler.js +261 -0
  45. package/dist/vendor/core/context-handoff.d.ts +42 -0
  46. package/dist/vendor/core/context-handoff.js +167 -0
  47. package/dist/vendor/core/context-shadow.d.ts +39 -0
  48. package/dist/vendor/core/context-shadow.js +200 -0
  49. package/dist/vendor/core/exit-signal.d.ts +88 -0
  50. package/dist/vendor/core/exit-signal.js +339 -0
  51. package/dist/vendor/core/exits.d.ts +41 -0
  52. package/dist/vendor/core/exits.js +258 -0
  53. package/dist/vendor/core/index.d.ts +42 -1
  54. package/dist/vendor/core/index.js +1086 -662
  55. package/dist/vendor/core/mission/aggregate.d.ts +27 -0
  56. package/dist/vendor/core/mission/aggregate.js +42 -0
  57. package/dist/vendor/core/mission/index.d.ts +2 -0
  58. package/dist/vendor/core/mission/index.js +1 -0
  59. package/dist/vendor/core/persistence/compiler.d.ts +8 -0
  60. package/dist/vendor/core/persistence/compiler.js +156 -0
  61. package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
  62. package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
  63. package/dist/vendor/core/persistence/index.d.ts +4 -0
  64. package/dist/vendor/core/persistence/index.js +2 -0
  65. package/dist/vendor/core/persistence/ledger.d.ts +1 -1
  66. package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
  67. package/dist/vendor/core/persistence/mission-store.js +281 -0
  68. package/dist/vendor/core/policy.d.ts +2 -0
  69. package/dist/vendor/core/policy.js +5 -2
  70. package/dist/vendor/core/rollback.js +15 -1
  71. package/dist/vendor/core/savings.d.ts +13 -0
  72. package/dist/vendor/core/savings.js +22 -0
  73. package/dist/vendor/core/termination-store.d.ts +15 -0
  74. package/dist/vendor/core/termination-store.js +44 -0
  75. package/dist/vendor/core/verified-handoff.d.ts +67 -0
  76. package/dist/vendor/core/verified-handoff.js +210 -0
  77. package/package.json +76 -83
  78. package/server.json +21 -21
  79. package/LICENSE +0 -216
  80. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
  81. package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
  82. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
  83. package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
  84. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
  85. package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
  86. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
  87. package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
  88. package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
  89. package/dist/vendor/adapters/adapters/src/index.js +0 -8
  90. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
  91. package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
  92. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
  93. package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
  94. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
  95. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
  96. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
  97. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
  98. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
  99. package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
  100. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
  101. package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
  102. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
  103. package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
  104. package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
  105. package/dist/vendor/adapters/contracts/src/governance.js +0 -11
  106. package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
  107. package/dist/vendor/adapters/contracts/src/identity.js +0 -30
  108. package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
  109. package/dist/vendor/adapters/contracts/src/index.js +0 -225
  110. package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
  111. package/dist/vendor/adapters/contracts/src/operator.js +0 -11
  112. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
  113. package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
  114. package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
  115. package/dist/vendor/adapters/core/src/compiler.js +0 -52
  116. package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
  117. package/dist/vendor/adapters/core/src/context-graph.js +0 -371
  118. package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
  119. package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
  120. package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
  121. package/dist/vendor/adapters/core/src/grounding.js +0 -273
  122. package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
  123. package/dist/vendor/adapters/core/src/identity.js +0 -86
  124. package/dist/vendor/adapters/core/src/index.d.ts +0 -186
  125. package/dist/vendor/adapters/core/src/index.js +0 -1495
  126. package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
  127. package/dist/vendor/adapters/core/src/leash.js +0 -497
  128. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
  129. package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
  130. package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
  131. package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
  132. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
  133. package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
  134. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
  135. package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
  136. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
  137. package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
  138. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
  139. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
  140. package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
  141. package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
  142. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
  143. package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
  144. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
  145. package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
  146. package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
  147. package/dist/vendor/adapters/core/src/policy.js +0 -704
  148. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
  149. package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
  150. package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
  151. package/dist/vendor/adapters/core/src/rollback.js +0 -253
  152. package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
  153. package/dist/vendor/adapters/core/src/routing.js +0 -336
  154. package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
  155. package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
  156. package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
  157. package/dist/vendor/adapters/core/src/trajectory.js +0 -295
  158. package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
  159. package/dist/vendor/adapters/stub-agent-cli.js +0 -40
  160. package/dist/vendor/adapters/verifier-only.d.ts +0 -9
  161. package/dist/vendor/adapters/verifier-only.js +0 -63
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
2
+ import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { diffStatsFromNumstat } from "./runtime-support.js";
5
5
  const gitRepositoryRootCache = new Map();
@@ -32,7 +32,7 @@ export async function runSubprocess(command, args, options) {
32
32
  }
33
33
  catch (error) {
34
34
  const message = error instanceof Error ? error.message : String(error);
35
- resolveOnce({ exitCode: 1, stdout: "", stderr: message, launched: false });
35
+ resolveOnce({ exitCode: 1, stdout: "", stderr: message, launched: false, completed: false, crashed: true });
36
36
  return;
37
37
  }
38
38
  const trackOutput = (chunks, chunk) => {
@@ -53,6 +53,18 @@ export async function runSubprocess(command, args, options) {
53
53
  terminationReason = reason;
54
54
  proc.kill("SIGTERM");
55
55
  };
56
+ // Honour the harness abort signal — kill the subprocess immediately
57
+ if (options.signal !== undefined) {
58
+ const sig = options.signal;
59
+ if (sig.aborted) {
60
+ proc.kill("SIGTERM");
61
+ }
62
+ else {
63
+ const onAbort = () => { proc.kill("SIGTERM"); };
64
+ sig.addEventListener("abort", onAbort, { once: true });
65
+ proc.on("close", () => { sig.removeEventListener("abort", onAbort); });
66
+ }
67
+ }
56
68
  proc.stdout?.on("data", (chunk) => {
57
69
  if (outputCapped || timedOut || terminationReason) {
58
70
  return;
@@ -93,15 +105,18 @@ export async function runSubprocess(command, args, options) {
93
105
  });
94
106
  proc.on("error", (error) => {
95
107
  clearTimeout(timer);
96
- resolveOnce({ exitCode: 1, stdout: "", stderr: error.message, launched: false });
108
+ resolveOnce({ exitCode: 1, stdout: "", stderr: error.message, launched: false, completed: false, crashed: true });
97
109
  });
98
110
  proc.on("close", (code) => {
99
111
  clearTimeout(timer);
112
+ const completed = code !== null && !timedOut && !outputCapped && !terminationReason;
100
113
  resolveOnce({
101
114
  exitCode: code ?? 1,
102
115
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
103
116
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
104
- launched: true
117
+ launched: true,
118
+ completed,
119
+ crashed: !completed && !timedOut && !outputCapped && !terminationReason,
105
120
  });
106
121
  });
107
122
  if (options.stdinData !== undefined && proc.stdin) {
@@ -116,22 +131,30 @@ export async function runSubprocess(command, args, options) {
116
131
  exitCode: 1,
117
132
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
118
133
  stderr: stdinError.message,
119
- launched: false
134
+ launched: false,
135
+ completed: false,
136
+ crashed: true,
120
137
  });
121
138
  }
122
139
  }
123
140
  }
124
141
  });
125
142
  }
126
- export async function runVerification(commands, cwd, timeoutMs, verificationStack, spawnImpl) {
143
+ export async function runVerification(commands, cwd, timeoutMs, verificationStack, spawnImpl, binding) {
127
144
  const steps = verificationStack && verificationStack.length > 0
128
145
  ? verificationStack.map((step) => ({
129
146
  command: step.command,
130
147
  fastFail: step.fastFail !== false
131
148
  }))
132
149
  : commands.map((command) => ({ command, fastFail: true }));
150
+ const executionBinding = {
151
+ runId: binding?.runId ?? "unbound",
152
+ workspaceId: binding?.workspaceId ?? "unbound",
153
+ cwd: binding?.cwd ?? cwd,
154
+ commands: steps.map((step) => step.command),
155
+ };
133
156
  if (steps.length === 0) {
134
- return { passed: true, summary: "No verification commands specified.", steps: [] };
157
+ return { passed: true, summary: "No verification commands specified.", steps: [], binding: executionBinding };
135
158
  }
136
159
  const failedSteps = [];
137
160
  const stepOutcomes = [];
@@ -169,6 +192,8 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
169
192
  stepOutcomes.push({
170
193
  command: step.command,
171
194
  launched: result.launched,
195
+ completed: result.completed,
196
+ crashed: result.crashed,
172
197
  exitCode: result.exitCode,
173
198
  timedOut: result.timedOut,
174
199
  fastFail: step.fastFail,
@@ -179,6 +204,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
179
204
  passed: false,
180
205
  summary: `Verification timed out: ${step.command}`,
181
206
  steps: stepOutcomes,
207
+ binding: executionBinding,
182
208
  ...(warnings.length ? { warnings } : {})
183
209
  };
184
210
  }
@@ -188,7 +214,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
188
214
  warnings.push(`Verifier never launched: ${step.command}`);
189
215
  }
190
216
  if (step.fastFail) {
191
- return { passed: false, summary, steps: stepOutcomes, ...(warnings.length ? { warnings } : {}) };
217
+ return { passed: false, summary, steps: stepOutcomes, binding: executionBinding, ...(warnings.length ? { warnings } : {}) };
192
218
  }
193
219
  failedSteps.push(step.command);
194
220
  }
@@ -198,6 +224,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
198
224
  passed: false,
199
225
  summary: `Failed steps: ${failedSteps.join(", ")}`,
200
226
  steps: stepOutcomes,
227
+ binding: executionBinding,
201
228
  ...(warnings.length ? { warnings } : {})
202
229
  };
203
230
  }
@@ -205,6 +232,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
205
232
  passed: true,
206
233
  summary: `All ${String(steps.length)} verification step(s) passed.`,
207
234
  steps: stepOutcomes,
235
+ binding: executionBinding,
208
236
  ...(warnings.length ? { warnings } : {})
209
237
  };
210
238
  }
@@ -365,19 +393,32 @@ export function resolveNpmShimScript(shimPath) {
365
393
  }
366
394
  const shimDir = dirname(shimPath);
367
395
  const scriptPathPattern = /["']?(?:%~?dp0%?|\$basedir)[\\/]([^"'\s]+\.[cm]?js)["']?/gi;
368
- const matches = contents.matchAll(scriptPathPattern);
396
+ const matches = [...contents.matchAll(scriptPathPattern)];
397
+ // Collect all candidate scripts that exist on disk.
398
+ const candidates = [];
369
399
  for (const match of matches) {
370
400
  const relativeScript = match[1];
371
- if (!relativeScript) {
401
+ if (!relativeScript)
372
402
  continue;
373
- }
374
403
  const segments = relativeScript.split(/[\\/]+/u).filter(Boolean);
375
404
  const resolvedScript = resolve(shimDir, ...segments);
376
405
  if (existsSync(resolvedScript)) {
377
- return resolvedScript;
406
+ candidates.push(resolvedScript);
378
407
  }
379
408
  }
380
- return undefined;
409
+ if (candidates.length === 0)
410
+ return undefined;
411
+ // npm.cmd / npm.ps1 / npm.bat shims reference both npm-prefix.js and npm-cli.js.
412
+ // npm-cli.js is the semantic npm CLI entry point — never pick npm-prefix.js for
413
+ // these. Fail closed: if npm-cli.js is not among the resolved candidates, return
414
+ // undefined so the caller falls back to wrapper-shell behavior.
415
+ const shimBasename = basename(shimPath).toLowerCase();
416
+ if (/^npm(\.(cmd|ps1|bat))?$/.test(shimBasename)) {
417
+ return candidates.find((p) => basename(p).toLowerCase() === "npm-cli.js");
418
+ }
419
+ // For all other npm-installed executables (codex.cmd, claude.ps1, etc.), the
420
+ // shim wraps exactly one package bin target — use the first resolving candidate.
421
+ return candidates[0];
381
422
  }
382
423
  /**
383
424
  * Resolve a reliable `node` executable for spawning CLI scripts on Windows.
@@ -43,7 +43,67 @@ export interface CodexExecArgsOptions {
43
43
  mode?: "prompt" | "probe";
44
44
  }
45
45
  type SpawnSyncLike = typeof spawnSync;
46
- export declare const DEFAULT_CODEX_CHATGPT_MODEL = "gpt-5.4";
46
+ /**
47
+ * Outcome when the preflight probe confirms the working directory is writable.
48
+ * capabilitySource is always "probe" — result is measured, not assumed.
49
+ */
50
+ export interface CodexSandboxPreflightOk {
51
+ ok: true;
52
+ effectiveSandbox: "read-only" | "workspace-write";
53
+ capabilitySource: "probe";
54
+ writableRoot: string;
55
+ }
56
+ /**
57
+ * Outcome when the working directory cannot be written but workspace-write
58
+ * was requested. This is a first-class typed failure — distinct from a
59
+ * provider-unavailable or environment-mismatch error. No model call has
60
+ * been attempted when this is returned.
61
+ */
62
+ export interface CodexSandboxPreflightReadOnly {
63
+ ok: false;
64
+ code: "provider_sandbox_read_only";
65
+ requestedCapability: "workspace-write";
66
+ detectedCapability: "read-only";
67
+ effectiveSandbox: "read-only";
68
+ affectedPath: string;
69
+ writableRoot: string;
70
+ capabilitySource: "probe";
71
+ remediation: string;
72
+ }
73
+ export type CodexSandboxPreflightOutcome = CodexSandboxPreflightOk | CodexSandboxPreflightReadOnly;
74
+ /**
75
+ * Probes whether the given directory is writable by the current process.
76
+ *
77
+ * Strategy: create a uniquely named temp file inside the directory, write a
78
+ * sentinel byte, then remove it. This is a real filesystem action — not an
79
+ * inference from binary metadata or launch-probe output.
80
+ *
81
+ * The probe leaves no file behind on either success or failure.
82
+ *
83
+ * Exported for unit testing with a real tmp directory.
84
+ */
85
+ export declare function probeFilesystemWriteCapability(directory: string): {
86
+ writable: true;
87
+ } | {
88
+ writable: false;
89
+ reason: string;
90
+ };
91
+ /**
92
+ * Checks whether the requested sandbox mode is achievable for the given
93
+ * working directory. The adapter receives `requestedSandbox` from CLI/core —
94
+ * it does not decide the mode itself.
95
+ *
96
+ * When `requestedSandbox` is "workspace-write" and the working directory is not
97
+ * writable, this function returns `provider_sandbox_read_only` before any model
98
+ * execution is attempted.
99
+ *
100
+ * When `requestedSandbox` is "read-only" no write probe is performed; the
101
+ * outcome is `ok: true, effectiveSandbox: "read-only"` immediately.
102
+ */
103
+ export declare function checkCodexSandboxPreflight(input: {
104
+ requestedSandbox: "read-only" | "workspace-write";
105
+ workingDirectory: string;
106
+ }): CodexSandboxPreflightOutcome;
47
107
  export interface CodexProbeCandidateResult {
48
108
  path: string;
49
109
  installKind: CodexInstallKind;
@@ -1,9 +1,100 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, extname, join, resolve } from "node:path";
4
4
  import { resolveNpmShimScript } from "./cli-bridge.js";
5
5
  const codexLaunchProbeCache = new Map();
6
- export const DEFAULT_CODEX_CHATGPT_MODEL = "gpt-5.4";
6
+ /**
7
+ * Probes whether the given directory is writable by the current process.
8
+ *
9
+ * Strategy: create a uniquely named temp file inside the directory, write a
10
+ * sentinel byte, then remove it. This is a real filesystem action — not an
11
+ * inference from binary metadata or launch-probe output.
12
+ *
13
+ * The probe leaves no file behind on either success or failure.
14
+ *
15
+ * Exported for unit testing with a real tmp directory.
16
+ */
17
+ export function probeFilesystemWriteCapability(directory) {
18
+ // Ensure the directory exists before probing.
19
+ try {
20
+ mkdirSync(directory, { recursive: true });
21
+ }
22
+ catch (err) {
23
+ return {
24
+ writable: false,
25
+ reason: `Could not create directory ${directory}: ${err instanceof Error ? err.message : String(err)}`
26
+ };
27
+ }
28
+ // Use mkdtempSync so the filename is guaranteed unique even under concurrent runs.
29
+ let tempDir;
30
+ try {
31
+ tempDir = mkdtempSync(join(directory, ".ml-write-probe-"));
32
+ const tempFile = join(tempDir, "capability.tmp");
33
+ writeFileSync(tempFile, "\x01", { encoding: "binary", flag: "wx" });
34
+ unlinkSync(tempFile);
35
+ return { writable: true };
36
+ }
37
+ catch (err) {
38
+ return {
39
+ writable: false,
40
+ reason: err instanceof Error ? err.message : String(err)
41
+ };
42
+ }
43
+ finally {
44
+ if (tempDir) {
45
+ try {
46
+ rmSync(tempDir, { recursive: true, force: true });
47
+ }
48
+ catch { /* ignore cleanup errors */ }
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Checks whether the requested sandbox mode is achievable for the given
54
+ * working directory. The adapter receives `requestedSandbox` from CLI/core —
55
+ * it does not decide the mode itself.
56
+ *
57
+ * When `requestedSandbox` is "workspace-write" and the working directory is not
58
+ * writable, this function returns `provider_sandbox_read_only` before any model
59
+ * execution is attempted.
60
+ *
61
+ * When `requestedSandbox` is "read-only" no write probe is performed; the
62
+ * outcome is `ok: true, effectiveSandbox: "read-only"` immediately.
63
+ */
64
+ export function checkCodexSandboxPreflight(input) {
65
+ const dir = resolve(input.workingDirectory);
66
+ if (input.requestedSandbox === "read-only") {
67
+ return {
68
+ ok: true,
69
+ effectiveSandbox: "read-only",
70
+ capabilitySource: "probe",
71
+ writableRoot: dir
72
+ };
73
+ }
74
+ // workspace-write: run the actual filesystem probe.
75
+ const probeResult = probeFilesystemWriteCapability(dir);
76
+ if (probeResult.writable) {
77
+ return {
78
+ ok: true,
79
+ effectiveSandbox: "workspace-write",
80
+ capabilitySource: "probe",
81
+ writableRoot: dir
82
+ };
83
+ }
84
+ return {
85
+ ok: false,
86
+ code: "provider_sandbox_read_only",
87
+ requestedCapability: "workspace-write",
88
+ detectedCapability: "read-only",
89
+ effectiveSandbox: "read-only",
90
+ affectedPath: dir,
91
+ writableRoot: dir,
92
+ capabilitySource: "probe",
93
+ remediation: `The working directory ${dir} is not writable by the current process. ` +
94
+ "Launch MartinLoop in a session with write access to that directory, or use " +
95
+ "`--sandbox read-only` for inspection-only work."
96
+ };
97
+ }
7
98
  const CODEX_LAUNCH_PROBE_PROMPT = [
8
99
  "You are validating MartinLoop Codex host readiness.",
9
100
  "Do not edit files.",
@@ -17,7 +108,7 @@ function buildProbeCacheKey(input) {
17
108
  workingDirectory: resolve(input.workingDirectory),
18
109
  platform: input.platform,
19
110
  candidatePaths: input.candidatePaths,
20
- model: input.model ?? DEFAULT_CODEX_CHATGPT_MODEL
111
+ model: input.model
21
112
  });
22
113
  }
23
114
  function isInsideGitRepository(workingDirectory) {
@@ -89,6 +180,17 @@ function codexProbePreference(diagnosis) {
89
180
  }
90
181
  return 3;
91
182
  }
183
+ function codexProbeCandidatePreference(path, diagnosis, platform) {
184
+ const hostPreference = codexProbePreference(diagnosis) * 10;
185
+ if (platform !== "win32" || diagnosis.installKind !== "windows_shim") {
186
+ return hostPreference;
187
+ }
188
+ // `where codex` commonly returns npm's extensionless POSIX shim before
189
+ // `codex.cmd`. Node cannot spawn that text shim directly on Windows, while
190
+ // the .cmd/.ps1 shim has a supported invocation path (or can be unwrapped).
191
+ const extension = extname(path).toLowerCase();
192
+ return hostPreference + (extension === ".cmd" || extension === ".bat" || extension === ".ps1" ? 0 : 1);
193
+ }
92
194
  function buildProbeCandidates(input) {
93
195
  const pathCandidates = normalizeCandidates(input.availability.candidatePaths ?? [input.availability.resolvedPath ?? input.availability.command]);
94
196
  const desktopCandidates = input.platform === "win32" && input.includeDesktopCandidates
@@ -106,7 +208,9 @@ function buildProbeCandidates(input) {
106
208
  return {
107
209
  path,
108
210
  diagnosis,
109
- preference: input.platform === "win32" ? codexProbePreference(diagnosis) : 0,
211
+ preference: input.platform === "win32"
212
+ ? codexProbeCandidatePreference(path, diagnosis, input.platform)
213
+ : 0,
110
214
  discoveryIndex
111
215
  };
112
216
  })
@@ -205,11 +309,11 @@ function classifyProbeFailure(stderr, stdout, diagnosis) {
205
309
  warnings.push(unsupportedModelWarning);
206
310
  }
207
311
  return {
208
- summary: `Codex launched with a model that is not supported for ChatGPT-account authentication. Use an explicit supported model such as \`${DEFAULT_CODEX_CHATGPT_MODEL}\` for governed Codex work.`,
312
+ summary: "Codex launched with a model that is not supported for ChatGPT-account authentication. Pass an explicit model that your ChatGPT account supports for governed Codex work.",
209
313
  diagnosis: {
210
314
  ...diagnosis,
211
315
  warnings,
212
- remediation: `Override the Codex launch model to a ChatGPT-account-supported option such as \`${DEFAULT_CODEX_CHATGPT_MODEL}\` before running governed Codex work.`
316
+ remediation: "Override the Codex launch model to a ChatGPT-account-supported option before running governed Codex work."
213
317
  }
214
318
  };
215
319
  }
@@ -275,14 +379,18 @@ export function buildCodexExecArgs(options) {
275
379
  const sandbox = options.sandbox ?? "workspace-write";
276
380
  const modelArgs = options.model ? ["--model", options.model] : [];
277
381
  const extraArgs = options.extraArgs ?? [];
382
+ const sandboxArgs = sandbox === "workspace-write"
383
+ // In Codex CLI, --approve-for-me is the write-enabled automatic-review
384
+ // mode and is mutually exclusive with --sandbox workspace-write.
385
+ ? ["--approve-for-me"]
386
+ : ["--sandbox", sandbox];
278
387
  return [
279
388
  "exec",
280
389
  // Governed MartinLoop runs should not inherit auth-sensitive default MCP/plugin state.
281
390
  "--ignore-user-config",
282
391
  "--cd",
283
392
  options.workingDirectory,
284
- "--sandbox",
285
- sandbox,
393
+ ...sandboxArgs,
286
394
  "--json",
287
395
  "--color",
288
396
  "never",
@@ -357,10 +465,6 @@ function discoverCommandOffPath(command, platform, env) {
357
465
  const localAppData = env.LOCALAPPDATA;
358
466
  if (localAppData)
359
467
  dirs.push(join(localAppData, "OpenAI", "Codex", "bin"));
360
- // Claude Code native installer places binary at %USERPROFILE%\.local\bin
361
- const userProfile = env.USERPROFILE ?? env.HOMEPATH;
362
- if (userProfile)
363
- dirs.push(join(userProfile, ".local", "bin"));
364
468
  if (home)
365
469
  dirs.push(join(home, "scoop", "shims"));
366
470
  }
@@ -387,13 +491,8 @@ function discoverCommandOffPath(command, platform, env) {
387
491
  return undefined;
388
492
  }
389
493
  function suggestInstall(command) {
390
- if (command === "claude") {
391
- const installCmd = process.platform === "win32"
392
- ? "irm https://claude.ai/install.ps1 | iex"
393
- : "curl -fsSL https://claude.ai/install.sh | bash";
394
- return `Install with: ${installCmd}`;
395
- }
396
494
  const installs = {
495
+ claude: "Install with: npm install -g @anthropic-ai/claude-code",
397
496
  codex: "Install with: npm install -g @openai/codex",
398
497
  gemini: "Install with: npm install -g @google/gemini-cli"
399
498
  };
@@ -471,7 +570,7 @@ export function probeCodexLaunch(input) {
471
570
  });
472
571
  const args = buildCodexExecArgs({
473
572
  workingDirectory: input.workingDirectory,
474
- model: input.model ?? DEFAULT_CODEX_CHATGPT_MODEL,
573
+ model: input.model,
475
574
  mode: "probe"
476
575
  });
477
576
  if (!availability.available) {
@@ -1,8 +1,6 @@
1
1
  export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js";
2
2
  export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
3
- export { createStubAgentCliAdapter, type StubAgentCliAdapterOptions } from "./stub-agent-cli.js";
4
3
  export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
5
- export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
6
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
7
- export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult } from "./codex-launcher.js";
5
+ export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js";
8
6
  export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
@@ -1,8 +1,6 @@
1
1
  export { createDirectProviderAdapter } from "./direct-provider.js";
2
2
  export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
3
- export { createStubAgentCliAdapter } from "./stub-agent-cli.js";
4
3
  export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
5
- export { createVerifierOnlyAdapter } from "./verifier-only.js";
6
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
7
- export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability } from "./codex-launcher.js";
5
+ export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
8
6
  export { createSpawnPlan } from "./cli-bridge.js";
@@ -69,15 +69,32 @@ function estimateCost(model, inputChars, outputChars) {
69
69
  const actualUsd = (tokensIn / 1000) * pricing.inputPer1K + (tokensOut / 1000) * pricing.outputPer1K;
70
70
  return { tokensIn, tokensOut, actualUsd };
71
71
  }
72
+ function normalizeOpenAiCompatibleUsage(input) {
73
+ const hasKnownPricing = KNOWN_MODEL_PRICING[input.model] !== undefined;
74
+ const pricing = KNOWN_MODEL_PRICING[input.model] ?? {
75
+ inputPer1K: FALLBACK_INPUT_PER_1K,
76
+ outputPer1K: FALLBACK_OUTPUT_PER_1K
77
+ };
78
+ const actualUsd = (input.tokensIn / 1000) * pricing.inputPer1K +
79
+ (input.tokensOut / 1000) * pricing.outputPer1K;
80
+ const provenance = input.usageWasFullyProviderReported && hasKnownPricing ? "actual" : "estimated";
81
+ return normalizeUsage({
82
+ actualUsd,
83
+ ...(provenance === "estimated" ? { estimatedUsd: actualUsd } : {}),
84
+ tokensIn: input.tokensIn,
85
+ tokensOut: input.tokensOut,
86
+ provenance
87
+ });
88
+ }
72
89
  // ---------------------------------------------------------------------------
73
90
  // Prompt builder
74
91
  // ---------------------------------------------------------------------------
75
- const DEFAULT_SYSTEM_PROMPT = `You are an expert software engineer executing a governed coding task.
76
- Follow these rules exactly:
77
- - Read the task description carefully and implement only what is asked.
78
- - Do not add features, refactors, or improvements beyond the stated task.
79
- - If the task asks you to write or modify code, output the complete file content with changes applied.
80
- - Be precise, minimal, and test-backed in all changes.
92
+ const DEFAULT_SYSTEM_PROMPT = `You are an expert software engineer executing a governed coding task.
93
+ Follow these rules exactly:
94
+ - Read the task description carefully and implement only what is asked.
95
+ - Do not add features, refactors, or improvements beyond the stated task.
96
+ - If the task asks you to write or modify code, output the complete file content with changes applied.
97
+ - Be precise, minimal, and test-backed in all changes.
81
98
  - State what you changed and why at the end of your response.`;
82
99
  export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
83
100
  export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
@@ -172,10 +189,12 @@ export function createOpenAiCompatibleAdapter(options) {
172
189
  // indicate a permanent configuration problem.
173
190
  const MAX_RETRIES = 3;
174
191
  const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
192
+ const NON_RETRYABLE_STATUS = new Set([400, 401, 403]);
175
193
  const endpoint = `${baseUrl}/v1/chat/completions`;
176
194
  let responseText = "";
177
195
  let tokensIn = estimated.tokensIn;
178
196
  let tokensOut = 0;
197
+ let usageWasFullyProviderReported = false;
179
198
  const headers = { "Content-Type": "application/json" };
180
199
  if (apiKey)
181
200
  headers["Authorization"] = `Bearer ${apiKey}`;
@@ -207,6 +226,17 @@ export function createOpenAiCompatibleAdapter(options) {
207
226
  const body = (await res.json());
208
227
  if (!res.ok || body.error) {
209
228
  const errMsg = body.error?.message ?? `HTTP ${res.status}`;
229
+ // Fail immediately on non-retryable errors (auth, bad request)
230
+ if (NON_RETRYABLE_STATUS.has(res.status)) {
231
+ return {
232
+ status: "failed",
233
+ summary: `${model} API error: ${errMsg}`,
234
+ usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
235
+ verification: { passed: false, summary: "API call failed before verifier." },
236
+ failure: { message: errMsg, classHint: "infrastructure_error" }
237
+ };
238
+ }
239
+ // Retry on transient errors
210
240
  if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES - 1) {
211
241
  lastError = errMsg;
212
242
  // Exponential backoff: 1s, 2s, 4s
@@ -223,8 +253,13 @@ export function createOpenAiCompatibleAdapter(options) {
223
253
  }
224
254
  responseText = body.choices?.[0]?.message?.content ?? "";
225
255
  if (body.usage) {
226
- tokensIn = body.usage.prompt_tokens ?? tokensIn;
227
- tokensOut = body.usage.completion_tokens ?? 0;
256
+ const providerPromptTokens = body.usage.prompt_tokens;
257
+ const providerCompletionTokens = body.usage.completion_tokens;
258
+ usageWasFullyProviderReported =
259
+ typeof providerPromptTokens === "number" &&
260
+ typeof providerCompletionTokens === "number";
261
+ tokensIn = providerPromptTokens ?? tokensIn;
262
+ tokensOut = providerCompletionTokens ?? Math.ceil(responseText.length / CHARS_PER_TOKEN);
228
263
  }
229
264
  else {
230
265
  tokensOut = Math.ceil(responseText.length / CHARS_PER_TOKEN);
@@ -267,33 +302,37 @@ export function createOpenAiCompatibleAdapter(options) {
267
302
  return {
268
303
  status: "failed",
269
304
  summary: `${model} returned an empty response.`,
270
- usage: normalizeUsage({ actualUsd: 0, tokensIn, tokensOut: 0, provenance: "actual" }),
305
+ usage: normalizeOpenAiCompatibleUsage({
306
+ model,
307
+ tokensIn,
308
+ tokensOut: 0,
309
+ usageWasFullyProviderReported
310
+ }),
271
311
  verification: { passed: false, summary: "Empty response — nothing to verify." },
272
312
  failure: { message: "empty_response" }
273
313
  };
274
314
  }
275
315
  // Run verification
276
- const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack);
316
+ const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, undefined, {
317
+ runId: request.loopId,
318
+ workspaceId: request.workspaceId,
319
+ cwd: workingDirectory,
320
+ });
277
321
  const execution = {
278
322
  changedFiles: hasVerificationSteps
279
323
  ? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
280
324
  : []
281
325
  };
282
- const pricing = KNOWN_MODEL_PRICING[model] ?? {
283
- inputPer1K: FALLBACK_INPUT_PER_1K,
284
- outputPer1K: FALLBACK_OUTPUT_PER_1K
285
- };
286
- const actualUsd = (tokensIn / 1000) * pricing.inputPer1K + (tokensOut / 1000) * pricing.outputPer1K;
287
326
  return {
288
327
  status: verification.passed ? "completed" : "failed",
289
328
  summary: verification.passed
290
329
  ? `${model} completed the task. Verifier passed.`
291
330
  : `${model} completed but verifier failed: ${verification.summary}`,
292
- usage: normalizeUsage({
293
- actualUsd,
331
+ usage: normalizeOpenAiCompatibleUsage({
332
+ model,
294
333
  tokensIn,
295
334
  tokensOut,
296
- provenance: "actual"
335
+ usageWasFullyProviderReported
297
336
  }),
298
337
  verification,
299
338
  execution,
@@ -1,5 +1,5 @@
1
1
  export type DiffVisibilityLevel = "none" | "git" | "adapter_reported";
2
- export type VerifierCompatibility = "full" | "verify_only" | "unsupported";
2
+ export type VerifierCompatibility = "full" | "proof" | "unsupported";
3
3
  export type SandboxExpectation = "host_process" | "workspace_write" | "provider_managed" | "not_applicable";
4
4
  export type LaunchReadiness = "path_lookup" | "configured_endpoint" | "built_in";
5
5
  export type AdapterUsageSettlement = "actual" | "estimated" | "unavailable";