@martinloop/mcp 0.7.0 → 0.8.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 (73) hide show
  1. package/dist/package-version.d.ts +1 -1
  2. package/dist/package-version.js +1 -1
  3. package/dist/vendor/adapters/claude-cli.d.ts +11 -1
  4. package/dist/vendor/adapters/claude-cli.js +285 -71
  5. package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
  6. package/dist/vendor/adapters/cli-bridge.js +129 -149
  7. package/dist/vendor/adapters/codex-cli.d.ts +3 -3
  8. package/dist/vendor/adapters/codex-cli.js +10 -24
  9. package/dist/vendor/adapters/codex-host.js +127 -132
  10. package/dist/vendor/adapters/index.d.ts +2 -1
  11. package/dist/vendor/adapters/index.js +2 -1
  12. package/dist/vendor/adapters/openai-compatible.js +88 -6
  13. package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
  14. package/dist/vendor/adapters/process-supervisor.js +541 -0
  15. package/dist/vendor/adapters/verifier-only.d.ts +10 -1
  16. package/dist/vendor/adapters/verifier-only.js +43 -1
  17. package/dist/vendor/contracts/external-outcome.d.ts +102 -0
  18. package/dist/vendor/contracts/external-outcome.js +226 -0
  19. package/dist/vendor/contracts/index.d.ts +8 -1
  20. package/dist/vendor/contracts/index.js +2 -0
  21. package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
  22. package/dist/vendor/contracts/swarm-hosted.js +629 -0
  23. package/dist/vendor/contracts/swarm.d.ts +449 -0
  24. package/dist/vendor/contracts/swarm.js +550 -0
  25. package/dist/vendor/core/external-outcome.d.ts +34 -0
  26. package/dist/vendor/core/external-outcome.js +515 -0
  27. package/dist/vendor/core/grounding.js +1 -1
  28. package/dist/vendor/core/index.d.ts +47 -1
  29. package/dist/vendor/core/index.js +53 -3
  30. package/dist/vendor/core/leash.d.ts +1 -0
  31. package/dist/vendor/core/leash.js +13 -0
  32. package/dist/vendor/core/persistence/integrity.d.ts +4 -0
  33. package/dist/vendor/core/persistence/integrity.js +19 -6
  34. package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
  35. package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
  36. package/dist/vendor/core/swarm/candidates.d.ts +78 -0
  37. package/dist/vendor/core/swarm/candidates.js +810 -0
  38. package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
  39. package/dist/vendor/core/swarm/evidence-projection.js +128 -0
  40. package/dist/vendor/core/swarm/evidence.d.ts +40 -0
  41. package/dist/vendor/core/swarm/evidence.js +575 -0
  42. package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
  43. package/dist/vendor/core/swarm/hosted-export.js +362 -0
  44. package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
  45. package/dist/vendor/core/swarm/hosted-projection.js +258 -0
  46. package/dist/vendor/core/swarm/index.d.ts +110 -0
  47. package/dist/vendor/core/swarm/index.js +1060 -0
  48. package/dist/vendor/core/swarm/integration.d.ts +76 -0
  49. package/dist/vendor/core/swarm/integration.js +615 -0
  50. package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
  51. package/dist/vendor/core/swarm/lifecycle.js +252 -0
  52. package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
  53. package/dist/vendor/core/swarm/live-runtime.js +1482 -0
  54. package/dist/vendor/core/swarm/live-store.d.ts +99 -0
  55. package/dist/vendor/core/swarm/live-store.js +860 -0
  56. package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
  57. package/dist/vendor/core/swarm/operational-reader.js +96 -0
  58. package/dist/vendor/core/swarm/operations.d.ts +29 -0
  59. package/dist/vendor/core/swarm/operations.js +128 -0
  60. package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
  61. package/dist/vendor/core/swarm/parent-receipt.js +489 -0
  62. package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
  63. package/dist/vendor/core/swarm/receipt-projection.js +120 -0
  64. package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
  65. package/dist/vendor/core/swarm/scheduler.js +555 -0
  66. package/dist/vendor/core/swarm/verification.d.ts +70 -0
  67. package/dist/vendor/core/swarm/verification.js +295 -0
  68. package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
  69. package/dist/vendor/core/swarm/workspaces.js +488 -0
  70. package/dist/vendor/core/verified-handoff.d.ts +4 -0
  71. package/dist/vendor/core/verified-handoff.js +4 -0
  72. package/package.json +1 -1
  73. package/server.json +2 -2
@@ -1,6 +1,8 @@
1
- import { type ChildProcess, type SpawnOptions } from "node:child_process";
1
+ import type { ChildProcess, SpawnOptions } from "node:child_process";
2
2
  import { diffStatsFromNumstat } from "./runtime-support.js";
3
3
  import type { VerifierExecutionBinding } from "../core/index.js";
4
+ import type { ExternalOutcomeEvidenceReference } from "../contracts/index.js";
5
+ import { type ProcessTreeCleanupResult } from "./process-supervisor.js";
4
6
  export type SpawnLike = (command: string, args?: readonly string[], options?: SpawnOptions) => ChildProcess;
5
7
  export interface SubprocessResult {
6
8
  exitCode: number;
@@ -27,9 +29,11 @@ export interface SubprocessResult {
27
29
  */
28
30
  terminationReason?: string;
29
31
  launched: boolean;
32
+ cleanup: ProcessTreeCleanupResult;
30
33
  }
31
34
  export interface VerificationOutcome {
32
35
  passed: boolean;
36
+ processCloseState: "closed" | "not_required" | "failed";
33
37
  summary: string;
34
38
  steps: VerificationStepOutcome[];
35
39
  warnings?: string[];
@@ -44,10 +48,12 @@ export interface VerificationStepOutcome {
44
48
  timedOut: boolean;
45
49
  fastFail: boolean;
46
50
  detail?: string;
51
+ evidence?: ExternalOutcomeEvidenceReference;
47
52
  }
48
53
  export declare function runSubprocess(command: string, args: string[], options: {
49
54
  cwd: string;
50
55
  timeoutMs: number;
56
+ env?: NodeJS.ProcessEnv;
51
57
  spawnImpl?: SpawnLike;
52
58
  stdinData?: string;
53
59
  /**
@@ -68,14 +74,17 @@ export declare function runSubprocess(command: string, args: string[], options:
68
74
  onStdoutChunk?: (chunk: Buffer, terminate: (reason: string) => void) => void;
69
75
  /** Optional abort signal — kills the subprocess when aborted. */
70
76
  signal?: AbortSignal;
77
+ /** Require an owned process-tree sweep before successful completion. */
78
+ requireTreeClosureOnSuccess?: boolean;
71
79
  }): Promise<SubprocessResult>;
72
80
  export declare function runVerification(commands: string[], cwd: string, timeoutMs: number, verificationStack?: Array<{
73
81
  command: string;
74
82
  type: string;
75
83
  fastFail?: boolean;
76
- }>, spawnImpl?: SpawnLike, binding?: Omit<VerifierExecutionBinding, "commands">): Promise<VerificationOutcome>;
77
- export declare function readGitExecutionArtifacts(repoRoot: string, timeoutMs: number, spawnImpl?: SpawnLike): Promise<{
84
+ }>, spawnImpl?: SpawnLike, binding?: Omit<VerifierExecutionBinding, "commands">, signal?: AbortSignal): Promise<VerificationOutcome>;
85
+ export declare function readGitExecutionArtifacts(repoRoot: string, timeoutMs: number, spawnImpl?: SpawnLike, requestedChangedFiles?: readonly string[]): Promise<{
78
86
  changedFiles?: string[];
87
+ patch?: string;
79
88
  diffStats?: ReturnType<typeof diffStatsFromNumstat>;
80
89
  }>;
81
90
  export declare function readGitChangedFiles(repoRoot: string, timeoutMs: number, spawnImpl?: SpawnLike): Promise<string[]>;
@@ -1,146 +1,23 @@
1
- import { spawn } from "node:child_process";
2
1
  import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import { diffStatsFromNumstat } from "./runtime-support.js";
4
+ import { spawnSupervisedProcess } from "./process-supervisor.js";
5
5
  const gitRepositoryRootCache = new Map();
6
6
  export async function runSubprocess(command, args, options) {
7
- return new Promise((resolve) => {
8
- let timedOut = false;
9
- let outputCapped = false;
10
- let terminationReason;
11
- let settled = false;
12
- let exited = false;
13
- let outputBytes = 0;
14
- const stdoutChunks = [];
15
- const stderrChunks = [];
16
- const stdinMode = options.stdinData !== undefined ? "pipe" : "ignore";
17
- const resolveOnce = (result) => {
18
- if (settled) {
19
- return;
20
- }
21
- settled = true;
22
- resolve({ ...result, timedOut, outputCapped, ...(terminationReason ? { terminationReason } : {}) });
23
- };
24
- let proc;
25
- try {
26
- const spawnPlan = createSpawnPlan(command, args, options.cwd, options.spawnImpl !== undefined);
27
- proc = (options.spawnImpl ?? spawn)(spawnPlan.command, spawnPlan.args, {
28
- cwd: options.cwd,
29
- stdio: [stdinMode, "pipe", "pipe"],
30
- env: process.env
31
- });
32
- }
33
- catch (error) {
34
- const message = error instanceof Error ? error.message : String(error);
35
- resolveOnce({ exitCode: 1, stdout: "", stderr: message, launched: false, completed: false, crashed: true });
36
- return;
37
- }
38
- const trackOutput = (chunks, chunk) => {
39
- if (outputCapped || timedOut || terminationReason) {
40
- return;
41
- }
42
- chunks.push(chunk);
43
- outputBytes += chunk.byteLength;
44
- if (options.maxOutputBytes !== undefined && outputBytes > options.maxOutputBytes) {
45
- outputCapped = true;
46
- proc.kill("SIGTERM");
47
- }
48
- };
49
- const terminateEarly = (reason) => {
50
- if (terminationReason || timedOut || outputCapped) {
51
- return;
52
- }
53
- terminationReason = reason;
54
- proc.kill("SIGTERM");
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
- }
68
- proc.stdout?.on("data", (chunk) => {
69
- if (outputCapped || timedOut || terminationReason) {
70
- return;
71
- }
72
- trackOutput(stdoutChunks, chunk);
73
- if (options.onStdoutChunk) {
74
- try {
75
- options.onStdoutChunk(chunk, terminateEarly);
76
- }
77
- catch (error) {
78
- terminateEarly(`stdout inspector error: ${error instanceof Error ? error.message : String(error)}`);
79
- }
80
- }
81
- });
82
- proc.stderr?.on("data", (chunk) => {
83
- if (outputCapped || timedOut || terminationReason) {
84
- return;
85
- }
86
- trackOutput(stderrChunks, chunk);
87
- });
88
- proc.stdin?.on("error", (error) => {
89
- // Some CLIs exit before consuming stdin in tests and on fast-fail paths.
90
- // Treat the closed pipe as a handled subprocess lifecycle condition.
91
- if (error.code === "EPIPE") {
92
- return;
93
- }
94
- stderrChunks.push(Buffer.from(`${error.message}\n`, "utf8"));
95
- });
96
- const timer = setTimeout(() => {
97
- if (settled || exited || proc.exitCode !== null) {
98
- return;
99
- }
100
- timedOut = true;
101
- proc.kill("SIGTERM");
102
- }, options.timeoutMs);
103
- proc.on("exit", () => {
104
- exited = true;
105
- });
106
- proc.on("error", (error) => {
107
- clearTimeout(timer);
108
- resolveOnce({ exitCode: 1, stdout: "", stderr: error.message, launched: false, completed: false, crashed: true });
109
- });
110
- proc.on("close", (code) => {
111
- clearTimeout(timer);
112
- const completed = code !== null && !timedOut && !outputCapped && !terminationReason;
113
- resolveOnce({
114
- exitCode: code ?? 1,
115
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
116
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
117
- launched: true,
118
- completed,
119
- crashed: !completed && !timedOut && !outputCapped && !terminationReason,
120
- });
121
- });
122
- if (options.stdinData !== undefined && proc.stdin) {
123
- try {
124
- proc.stdin.end(options.stdinData, "utf8");
125
- }
126
- catch (error) {
127
- const stdinError = error;
128
- if (stdinError.code !== "EPIPE") {
129
- clearTimeout(timer);
130
- resolveOnce({
131
- exitCode: 1,
132
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
133
- stderr: stdinError.message,
134
- launched: false,
135
- completed: false,
136
- crashed: true,
137
- });
138
- }
139
- }
140
- }
7
+ const spawnPlan = createSpawnPlan(command, args, options.cwd, options.spawnImpl !== undefined);
8
+ return spawnSupervisedProcess(spawnPlan.command, spawnPlan.args, {
9
+ cwd: options.cwd,
10
+ timeoutMs: options.timeoutMs,
11
+ ...(options.env ? { env: options.env } : {}),
12
+ ...(options.spawnImpl ? { spawnImpl: options.spawnImpl } : {}),
13
+ ...(options.stdinData !== undefined ? { stdinData: options.stdinData } : {}),
14
+ ...(options.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),
15
+ ...(options.onStdoutChunk ? { onStdoutChunk: options.onStdoutChunk } : {}),
16
+ ...(options.signal ? { signal: options.signal } : {}),
17
+ ...(options.requireTreeClosureOnSuccess ? { requireTreeClosureOnSuccess: true } : {})
141
18
  });
142
19
  }
143
- export async function runVerification(commands, cwd, timeoutMs, verificationStack, spawnImpl, binding) {
20
+ export async function runVerification(commands, cwd, timeoutMs, verificationStack, spawnImpl, binding, signal) {
144
21
  const steps = verificationStack && verificationStack.length > 0
145
22
  ? verificationStack.map((step) => ({
146
23
  command: step.command,
@@ -150,12 +27,29 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
150
27
  const executionBinding = {
151
28
  runId: binding?.runId ?? "unbound",
152
29
  workspaceId: binding?.workspaceId ?? "unbound",
30
+ ...(binding?.attemptId ? { attemptId: binding.attemptId } : {}),
153
31
  cwd: binding?.cwd ?? cwd,
32
+ ...(binding?.runsRoot ? { runsRoot: binding.runsRoot } : {}),
33
+ ...(binding?.executionProfile ? { executionProfile: binding.executionProfile } : {}),
34
+ ...(binding?.allowedNetworkDomains?.length ? { allowedNetworkDomains: [...binding.allowedNetworkDomains] } : {}),
154
35
  commands: steps.map((step) => step.command),
155
36
  };
37
+ const verifierEnv = {
38
+ ...process.env,
39
+ MARTIN_RUN_ID: executionBinding.runId,
40
+ MARTIN_WORKSPACE_ID: executionBinding.workspaceId,
41
+ ...(executionBinding.attemptId ? { MARTIN_ATTEMPT_ID: executionBinding.attemptId } : {}),
42
+ MARTIN_VERIFIER_CWD: executionBinding.cwd,
43
+ ...(executionBinding.runsRoot ? { MARTIN_RUNS_DIR: executionBinding.runsRoot } : {}),
44
+ ...(executionBinding.executionProfile ? { MARTIN_EXECUTION_PROFILE: executionBinding.executionProfile } : {}),
45
+ ...(executionBinding.allowedNetworkDomains?.length
46
+ ? { MARTIN_ALLOWED_NETWORK_DOMAINS: JSON.stringify(executionBinding.allowedNetworkDomains) }
47
+ : {}),
48
+ };
156
49
  if (steps.length === 0) {
157
50
  return {
158
51
  passed: false,
52
+ processCloseState: "not_required",
159
53
  summary: "No verification commands specified; execution is not VERIFIED.",
160
54
  steps: [],
161
55
  warnings: ["Execution completed without verifier evidence."],
@@ -165,6 +59,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
165
59
  const failedSteps = [];
166
60
  const stepOutcomes = [];
167
61
  const warnings = [];
62
+ let processCloseState = "not_required";
168
63
  for (const step of steps) {
169
64
  let bin;
170
65
  let args;
@@ -193,8 +88,17 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
193
88
  if (!bin) {
194
89
  continue;
195
90
  }
196
- const result = await runSubprocess(bin, args, { cwd, timeoutMs, spawnImpl });
91
+ const result = await runSubprocess(bin, args, {
92
+ cwd,
93
+ timeoutMs,
94
+ env: verifierEnv,
95
+ spawnImpl,
96
+ ...(signal ? { signal } : {}),
97
+ requireTreeClosureOnSuccess: true
98
+ });
197
99
  const detail = truncate(result.stderr.trim() || result.stdout.trim(), 500);
100
+ processCloseState = combineProcessCloseState(processCloseState, result.cleanup.state);
101
+ const evidence = parseExternalOutcomeEvidenceReference(result.stdout);
198
102
  stepOutcomes.push({
199
103
  command: step.command,
200
104
  launched: result.launched,
@@ -203,11 +107,23 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
203
107
  exitCode: result.exitCode,
204
108
  timedOut: result.timedOut,
205
109
  fastFail: step.fastFail,
206
- ...(detail ? { detail } : {})
110
+ ...(detail ? { detail } : {}),
111
+ ...(evidence ? { evidence } : {})
207
112
  });
113
+ if (result.cleanup.state === "failed") {
114
+ return {
115
+ passed: false,
116
+ processCloseState,
117
+ summary: `Verifier process-tree closure failed: ${step.command}`,
118
+ steps: stepOutcomes,
119
+ binding: executionBinding,
120
+ warnings: [result.cleanup.message]
121
+ };
122
+ }
208
123
  if (result.timedOut) {
209
124
  return {
210
125
  passed: false,
126
+ processCloseState,
211
127
  summary: `Verification timed out: ${step.command}`,
212
128
  steps: stepOutcomes,
213
129
  binding: executionBinding,
@@ -220,7 +136,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
220
136
  warnings.push(`Verifier never launched: ${step.command}`);
221
137
  }
222
138
  if (step.fastFail) {
223
- return { passed: false, summary, steps: stepOutcomes, binding: executionBinding, ...(warnings.length ? { warnings } : {}) };
139
+ return { passed: false, processCloseState, summary, steps: stepOutcomes, binding: executionBinding, ...(warnings.length ? { warnings } : {}) };
224
140
  }
225
141
  failedSteps.push(step.command);
226
142
  }
@@ -228,6 +144,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
228
144
  if (failedSteps.length > 0) {
229
145
  return {
230
146
  passed: false,
147
+ processCloseState,
231
148
  summary: `Failed steps: ${failedSteps.join(", ")}`,
232
149
  steps: stepOutcomes,
233
150
  binding: executionBinding,
@@ -236,30 +153,93 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
236
153
  }
237
154
  return {
238
155
  passed: true,
156
+ processCloseState,
239
157
  summary: `All ${String(steps.length)} verification step(s) passed.`,
240
158
  steps: stepOutcomes,
241
159
  binding: executionBinding,
242
160
  ...(warnings.length ? { warnings } : {})
243
161
  };
244
162
  }
245
- export async function readGitExecutionArtifacts(repoRoot, timeoutMs, spawnImpl) {
163
+ function parseExternalOutcomeEvidenceReference(stdout) {
164
+ const trimmed = stdout.trim();
165
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
166
+ return undefined;
167
+ try {
168
+ const parsed = JSON.parse(trimmed);
169
+ const evidence = parsed.evidence;
170
+ if (evidence?.kind !== "external_outcome"
171
+ || typeof evidence.contractId !== "string"
172
+ || typeof evidence.path !== "string"
173
+ || typeof evidence.sha256 !== "string"
174
+ || !/^[a-f0-9]{64}$/u.test(evidence.sha256))
175
+ return undefined;
176
+ return {
177
+ kind: "external_outcome",
178
+ contractId: evidence.contractId,
179
+ path: evidence.path,
180
+ sha256: evidence.sha256,
181
+ };
182
+ }
183
+ catch {
184
+ return undefined;
185
+ }
186
+ }
187
+ function combineProcessCloseState(current, next) {
188
+ if (current === "failed" || next === "failed")
189
+ return "failed";
190
+ if (current === "closed" || next === "closed")
191
+ return "closed";
192
+ return "not_required";
193
+ }
194
+ export async function readGitExecutionArtifacts(repoRoot, timeoutMs, spawnImpl, requestedChangedFiles) {
246
195
  if (!resolveGitRepositoryRoot(repoRoot)) {
247
196
  return {};
248
197
  }
249
- const changedFilesResult = await runSubprocess("git", ["diff", "--name-only", "HEAD"], { cwd: repoRoot, timeoutMs, spawnImpl });
250
- const numstatResult = await runSubprocess("git", ["diff", "--numstat", "HEAD"], { cwd: repoRoot, timeoutMs, spawnImpl });
251
- const changedFiles = changedFilesResult.exitCode === 0
252
- ? changedFilesResult.stdout
253
- .split(/\r?\n/u)
254
- .map((entry) => entry.trim())
255
- .filter(Boolean)
256
- : [];
257
- const diffStats = numstatResult.exitCode === 0 ? diffStatsFromNumstat(numstatResult.stdout) : undefined;
198
+ const observedChangedFiles = requestedChangedFiles
199
+ ? [...requestedChangedFiles]
200
+ : await readGitChangedFiles(repoRoot, timeoutMs, spawnImpl);
201
+ const changedFiles = observedChangedFiles.filter(isSafeRepoRelativeGitPath);
202
+ const patchParts = [];
203
+ const numstatParts = [];
204
+ const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
205
+ for (const file of changedFiles) {
206
+ const trackedPatch = await runSubprocess("git", ["diff", "--binary", "--no-ext-diff", "HEAD", "--", file], { cwd: repoRoot, timeoutMs, spawnImpl });
207
+ const trackedNumstat = await runSubprocess("git", ["diff", "--numstat", "HEAD", "--", file], { cwd: repoRoot, timeoutMs, spawnImpl });
208
+ if (trackedPatch.exitCode === 0 && trackedPatch.stdout.length > 0) {
209
+ patchParts.push(trackedPatch.stdout);
210
+ if (trackedNumstat.exitCode === 0 && trackedNumstat.stdout.length > 0) {
211
+ numstatParts.push(trackedNumstat.stdout);
212
+ }
213
+ continue;
214
+ }
215
+ // Git does not include untracked files in `git diff HEAD`. Diff each such
216
+ // path against the null tree so the parent runtime receives its content,
217
+ // not merely a synthetic filename-only patch.
218
+ const untrackedPatch = await runSubprocess("git", ["diff", "--binary", "--no-ext-diff", "--no-index", "--", nullDevice, file], { cwd: repoRoot, timeoutMs, spawnImpl });
219
+ if ((untrackedPatch.exitCode === 0 || untrackedPatch.exitCode === 1) && untrackedPatch.stdout.length > 0) {
220
+ patchParts.push(untrackedPatch.stdout);
221
+ }
222
+ const untrackedNumstat = await runSubprocess("git", ["diff", "--numstat", "--no-index", "--", nullDevice, file], { cwd: repoRoot, timeoutMs, spawnImpl });
223
+ if ((untrackedNumstat.exitCode === 0 || untrackedNumstat.exitCode === 1) && untrackedNumstat.stdout.length > 0) {
224
+ numstatParts.push(untrackedNumstat.stdout);
225
+ }
226
+ }
227
+ const patch = patchParts.join("\n");
228
+ const numstat = numstatParts.join("\n");
229
+ const diffStats = numstat.length > 0 ? diffStatsFromNumstat(numstat) : undefined;
258
230
  return {
259
231
  ...(changedFiles.length > 0 ? { changedFiles } : {}),
232
+ ...(patch.length > 0 ? { patch } : {}),
260
233
  ...(diffStats ? { diffStats } : {})
261
234
  };
262
235
  }
236
+ function isSafeRepoRelativeGitPath(file) {
237
+ if (file.length === 0 || file.includes("\u0000") || isAbsolute(file)) {
238
+ return false;
239
+ }
240
+ const segments = file.replace(/\\/gu, "/").split("/");
241
+ return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
242
+ }
263
243
  export async function readGitChangedFiles(repoRoot, timeoutMs, spawnImpl) {
264
244
  if (!resolveGitRepositoryRoot(repoRoot)) {
265
245
  return [];
@@ -16,8 +16,8 @@ export interface CodexCliAdapterOptions extends Omit<LegacyCodexCliAdapterOption
16
16
  *
17
17
  * Provider identity remains `codex` even when doctor/preflight selected an
18
18
  * absolute native binary or npm shim. The selected binary's cached capability
19
- * profile builds both the real argv and stdin transport, while the spawn router
20
- * sends only the Codex subprocess to that exact executable. Git/verifier
21
- * subprocesses retain their normal commands.
19
+ * profile builds both the real argv and stdin transport. The generic adapter's
20
+ * execution-command boundary sends only the Codex subprocess to that exact
21
+ * executable, while Git/verifier subprocesses retain normal OS-owned spawning.
22
22
  */
23
23
  export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): import("../core/index.js").MartinAdapter;
@@ -1,26 +1,13 @@
1
- import { spawn } from "node:child_process";
2
1
  import { createAgentCliAdapter } from "./claude-cli.js";
3
2
  import { buildCodexExecArgs, buildCodexStdin, isCodexAutonomyResolutionVerifiedByLaunchProbe, probeCodexCapabilities } from "./codex-launcher.js";
4
- import { createSpawnPlan } from "./cli-bridge.js";
5
- function createCodexSpawnRouter(input) {
6
- if (input.injectedSpawn) {
7
- return (command, args = [], options) => input.injectedSpawn?.(command === "codex" ? input.selectedBinary : command, args, options);
8
- }
9
- return (command, args = [], options) => {
10
- const executable = command === "codex" ? input.selectedBinary : command;
11
- const cwd = typeof options?.cwd === "string" ? options.cwd : input.workingDirectory;
12
- const plan = createSpawnPlan(executable, [...args], cwd, false);
13
- return spawn(plan.command, plan.args, options ?? {});
14
- };
15
- }
16
3
  /**
17
4
  * Capability-driven Codex CLI adapter.
18
5
  *
19
6
  * Provider identity remains `codex` even when doctor/preflight selected an
20
7
  * absolute native binary or npm shim. The selected binary's cached capability
21
- * profile builds both the real argv and stdin transport, while the spawn router
22
- * sends only the Codex subprocess to that exact executable. Git/verifier
23
- * subprocesses retain their normal commands.
8
+ * profile builds both the real argv and stdin transport. The generic adapter's
9
+ * execution-command boundary sends only the Codex subprocess to that exact
10
+ * executable, while Git/verifier subprocesses retain normal OS-owned spawning.
24
11
  */
25
12
  export function createCodexCliAdapter(options = {}) {
26
13
  const workingDirectory = options.workingDirectory ?? process.cwd();
@@ -39,16 +26,12 @@ export function createCodexCliAdapter(options = {}) {
39
26
  const sandbox = options.sandbox ?? "workspace-write";
40
27
  const extraArgs = options.extraArgs ?? [];
41
28
  const launchModel = options.model;
42
- const spawnImpl = createCodexSpawnRouter({
43
- selectedBinary,
44
- workingDirectory,
45
- ...(options.spawnImpl ? { injectedSpawn: options.spawnImpl } : {})
46
- });
47
29
  return createAgentCliAdapter({
48
30
  // Keep semantic provider identity stable for usage parsing, pricing,
49
- // diagnostics, and adapter metadata. The spawn router maps this to the
50
- // exact selected binary at process launch time.
31
+ // diagnostics, and adapter metadata. executionCommand selects the exact
32
+ // binary without disguising production OS spawning as a test injection.
51
33
  command: "codex",
34
+ executionCommand: selectedBinary,
52
35
  adapterIdSuffix: "codex",
53
36
  model: options.model,
54
37
  label: options.label ?? "Codex CLI adapter",
@@ -58,7 +41,10 @@ export function createCodexCliAdapter(options = {}) {
58
41
  providerExecutionTimeoutMs: options.providerExecutionTimeoutMs,
59
42
  verifyTimeoutMs: options.verifyTimeoutMs,
60
43
  supportsJsonOutput: false,
61
- spawnImpl,
44
+ streamingUsageCap: true,
45
+ streamingTokenCap: true,
46
+ streamingUsageDetailsIncludedInTotals: true,
47
+ spawnImpl: options.spawnImpl,
62
48
  argsBuilder: (prompt) => buildCodexExecArgs({
63
49
  command: selectedBinary,
64
50
  workingDirectory,