@martinloop/mcp 0.2.7 → 0.3.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 (52) hide show
  1. package/README.md +49 -104
  2. package/dist/package-version.d.ts +1 -1
  3. package/dist/package-version.js +1 -1
  4. package/dist/prompts.d.ts +1 -1
  5. package/dist/resources.d.ts +1 -1
  6. package/dist/resources.js +2 -2
  7. package/dist/server-validation.d.ts +1 -0
  8. package/dist/server-validation.js +8 -0
  9. package/dist/server.js +18 -2
  10. package/dist/tools/doctor.d.ts +12 -1
  11. package/dist/tools/doctor.js +37 -6
  12. package/dist/tools/eval.js +3 -2
  13. package/dist/tools/get-run.d.ts +2 -0
  14. package/dist/tools/get-run.js +2 -1
  15. package/dist/tools/get-verification-results.d.ts +2 -0
  16. package/dist/tools/get-verification-results.js +2 -1
  17. package/dist/tools/pr-tools.js +2 -1
  18. package/dist/tools/preflight.d.ts +14 -1
  19. package/dist/tools/preflight.js +36 -5
  20. package/dist/tools/run-dossier.d.ts +2 -0
  21. package/dist/tools/run-dossier.js +4 -2
  22. package/dist/tools/run-loop.d.ts +3 -2
  23. package/dist/tools/run-loop.js +48 -28
  24. package/dist/tools/tool-errors.js +1 -1
  25. package/dist/tools/tool-support.d.ts +6 -3
  26. package/dist/tools/tool-support.js +12 -5
  27. package/dist/vendor/adapters/claude-cli.d.ts +25 -0
  28. package/dist/vendor/adapters/claude-cli.js +279 -19
  29. package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
  30. package/dist/vendor/adapters/cli-bridge.js +44 -3
  31. package/dist/vendor/adapters/codex-launcher.d.ts +44 -0
  32. package/dist/vendor/adapters/codex-launcher.js +247 -0
  33. package/dist/vendor/adapters/index.d.ts +3 -2
  34. package/dist/vendor/adapters/index.js +3 -2
  35. package/dist/vendor/adapters/openai-compatible.d.ts +19 -4
  36. package/dist/vendor/adapters/openai-compatible.js +44 -19
  37. package/dist/vendor/adapters/runtime-support.d.ts +3 -0
  38. package/dist/vendor/adapters/runtime-support.js +8 -1
  39. package/dist/vendor/adapters/verifier-only.js +4 -3
  40. package/dist/vendor/contracts/index.d.ts +39 -0
  41. package/dist/vendor/contracts/index.js +2 -0
  42. package/dist/vendor/core/index.d.ts +23 -3
  43. package/dist/vendor/core/index.js +88 -15
  44. package/dist/vendor/core/persistence/index.d.ts +2 -0
  45. package/dist/vendor/core/persistence/index.js +1 -0
  46. package/dist/vendor/core/persistence/integrity.d.ts +38 -0
  47. package/dist/vendor/core/persistence/integrity.js +239 -0
  48. package/dist/vendor/core/persistence/store.d.ts +7 -0
  49. package/dist/vendor/core/persistence/store.js +25 -1
  50. package/dist/vendor/core/policy.d.ts +9 -0
  51. package/package.json +1 -1
  52. package/server.json +2 -2
@@ -0,0 +1,247 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, extname, resolve } from "node:path";
4
+ function isInsideGitRepository(workingDirectory) {
5
+ let current = resolve(workingDirectory);
6
+ while (true) {
7
+ if (existsSync(resolve(current, ".git"))) {
8
+ return true;
9
+ }
10
+ const parent = dirname(current);
11
+ if (parent === current) {
12
+ return false;
13
+ }
14
+ current = parent;
15
+ }
16
+ }
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
+ }
42
+ };
43
+ const [firstCandidate, ...remainingCandidates] = cleaned;
44
+ if (firstCandidate === undefined) {
45
+ return undefined;
46
+ }
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;
54
+ }
55
+ }
56
+ return bestCandidate;
57
+ }
58
+ function buildProbeCommand(command, args, platform) {
59
+ if (platform !== "win32") {
60
+ return { command, args };
61
+ }
62
+ const extension = extname(command).toLowerCase();
63
+ switch (extension) {
64
+ case ".cmd":
65
+ case ".bat":
66
+ return {
67
+ command: process.env.ComSpec || "cmd.exe",
68
+ args: ["/d", "/c", command, ...args]
69
+ };
70
+ case ".ps1":
71
+ return {
72
+ command: "powershell.exe",
73
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args]
74
+ };
75
+ default:
76
+ return { command, args };
77
+ }
78
+ }
79
+ export function resolveCliCommandAvailability(command, options = {}) {
80
+ const platform = options.platform ?? process.platform;
81
+ 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
91
+ ? {
92
+ command,
93
+ available: true,
94
+ locator,
95
+ detail: `${command} is available on PATH.`,
96
+ ...(resolvedPath ? { resolvedPath } : {})
97
+ }
98
+ : {
99
+ command,
100
+ available: false,
101
+ locator,
102
+ detail: `${command} is not available on PATH.`
103
+ };
104
+ }
105
+ export function detectCodexHostPlatform(env = process.env, platform = process.platform) {
106
+ if (platform === "win32") {
107
+ return "windows";
108
+ }
109
+ if (platform === "darwin") {
110
+ return "macos";
111
+ }
112
+ if (env["WSL_DISTRO_NAME"] || env["WSL_INTEROP"]) {
113
+ return "wsl";
114
+ }
115
+ return "linux";
116
+ }
117
+ export function diagnoseCodexHost(availability, options = {}) {
118
+ const hostPlatform = detectCodexHostPlatform(options.env ?? process.env, options.platform ?? process.platform);
119
+ const warnings = [];
120
+ if (!availability.available) {
121
+ return {
122
+ hostPlatform,
123
+ nativeInstallValid: false,
124
+ warnings,
125
+ remediation: "Install or expose the Codex CLI on PATH before running governed Codex work."
126
+ };
127
+ }
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)) {
135
+ warnings.push("Codex resolves to a Windows-hosted install from a Linux/WSL environment.");
136
+ return {
137
+ hostPlatform,
138
+ nativeInstallValid: false,
139
+ warnings,
140
+ remediation: "Install Codex natively inside this Linux/WSL environment instead of relying on a Windows PATH shim."
141
+ };
142
+ }
143
+ return {
144
+ hostPlatform,
145
+ nativeInstallValid: true,
146
+ warnings
147
+ };
148
+ }
149
+ export function probeCodexLaunch(input) {
150
+ const availability = input.availability ??
151
+ resolveCliCommandAvailability("codex", {
152
+ platform: input.platform,
153
+ spawnSyncImpl: input.spawnSyncImpl
154
+ });
155
+ const diagnosis = diagnoseCodexHost(availability, {
156
+ env: input.env,
157
+ platform: input.platform
158
+ });
159
+ const args = [
160
+ "exec",
161
+ "--cd",
162
+ input.workingDirectory,
163
+ "--sandbox",
164
+ "workspace-write",
165
+ "--json",
166
+ "--color",
167
+ "never",
168
+ "--help"
169
+ ];
170
+ if (!availability.available) {
171
+ return {
172
+ ok: false,
173
+ summary: availability.detail,
174
+ availability,
175
+ diagnosis,
176
+ command: availability.command,
177
+ args
178
+ };
179
+ }
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
+ if (!isInsideGitRepository(input.workingDirectory)) {
191
+ return {
192
+ ok: false,
193
+ summary: "Working directory is not inside a git repository. Codex exec requires a trusted repo unless --skip-git-repo-check is explicitly enabled.",
194
+ availability,
195
+ diagnosis,
196
+ command: availability.resolvedPath ?? availability.command,
197
+ args
198
+ };
199
+ }
200
+ const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync;
201
+ 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"]
207
+ });
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
+ };
219
+ }
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,
231
+ exitCode: result.status ?? undefined,
232
+ stderr: result.stderr ?? "",
233
+ stdout: result.stdout ?? ""
234
+ };
235
+ }
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,
242
+ args,
243
+ exitCode: result.status ?? undefined,
244
+ stderr: result.stderr ?? "",
245
+ stdout: result.stdout ?? ""
246
+ };
247
+ }
@@ -1,7 +1,8 @@
1
1
  export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js";
2
2
  export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
3
3
  export { createStubAgentCliAdapter, type StubAgentCliAdapterOptions } from "./stub-agent-cli.js";
4
- export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
4
+ export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
5
5
  export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
6
- export { createOpenAiCompatibleAdapter, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
6
+ 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";
7
8
  export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
@@ -1,7 +1,8 @@
1
1
  export { createDirectProviderAdapter } from "./direct-provider.js";
2
2
  export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
3
3
  export { createStubAgentCliAdapter } from "./stub-agent-cli.js";
4
- export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter } from "./claude-cli.js";
4
+ export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
5
5
  export { createVerifierOnlyAdapter } from "./verifier-only.js";
6
- export { createOpenAiCompatibleAdapter } from "./openai-compatible.js";
6
+ export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
7
+ export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability } from "./codex-launcher.js";
7
8
  export { createSpawnPlan } from "./cli-bridge.js";
@@ -12,24 +12,30 @@
12
12
  * Llama 3.x, Mistral 7B, Phi-4, Gemma 3, any GGUF model.
13
13
  *
14
14
  * Usage:
15
+ * # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
16
+ * MARTIN_OPENAI_API_KEY=sk-...
17
+ * MARTIN_OPENAI_MODEL=gpt-4.1-mini
18
+ * martin-loop run "fix the bug" --engine openai
19
+ *
20
+ * # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
15
21
  * MARTIN_OPENAI_BASE_URL=https://openrouter.ai/api
16
22
  * MARTIN_OPENAI_API_KEY=sk-or-...
17
23
  * MARTIN_OPENAI_MODEL=deepseek/deepseek-chat
18
- * martin run "fix the bug" --engine openai
24
+ * martin-loop run "fix the bug" --engine openai
19
25
  *
20
26
  * Or for Ollama:
21
27
  * MARTIN_OPENAI_BASE_URL=http://localhost:11434
22
28
  * MARTIN_OPENAI_MODEL=llama3.3
23
- * martin run "fix the bug" --engine openai
29
+ * martin-loop run "fix the bug" --engine openai
24
30
  */
25
31
  import type { MartinAdapter } from "../core/index.js";
26
32
  export interface OpenAiCompatibleAdapterOptions {
27
33
  /** Base URL of the OpenAI-compatible API. No trailing slash. */
28
- baseUrl: string;
34
+ baseUrl?: string;
29
35
  /** API key. Empty string for local (Ollama/LM Studio) endpoints. */
30
36
  apiKey?: string;
31
37
  /** Model identifier passed as-is to the API (e.g. "deepseek/deepseek-chat"). */
32
- model: string;
38
+ model?: string;
33
39
  /**
34
40
  * System prompt prepended before the MartinLoop task prompt.
35
41
  * Default instructs the model to act as a focused coding assistant.
@@ -44,4 +50,13 @@ export interface OpenAiCompatibleAdapterOptions {
44
50
  /** Optional fetch override for testing. */
45
51
  fetchImpl?: typeof fetch;
46
52
  }
53
+ export declare const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
54
+ export declare const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
55
+ export declare function resolveOpenAiCompatibleRuntimeConfig(env?: NodeJS.ProcessEnv): {
56
+ baseUrl: string;
57
+ model: string;
58
+ apiKey: string;
59
+ apiKeyConfigured: boolean;
60
+ authPosture: "api_key" | "anonymous_or_local";
61
+ };
47
62
  export declare function createOpenAiCompatibleAdapter(options: OpenAiCompatibleAdapterOptions): MartinAdapter;
@@ -12,17 +12,23 @@
12
12
  * Llama 3.x, Mistral 7B, Phi-4, Gemma 3, any GGUF model.
13
13
  *
14
14
  * Usage:
15
+ * # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
16
+ * MARTIN_OPENAI_API_KEY=sk-...
17
+ * MARTIN_OPENAI_MODEL=gpt-4.1-mini
18
+ * martin-loop run "fix the bug" --engine openai
19
+ *
20
+ * # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
15
21
  * MARTIN_OPENAI_BASE_URL=https://openrouter.ai/api
16
22
  * MARTIN_OPENAI_API_KEY=sk-or-...
17
23
  * MARTIN_OPENAI_MODEL=deepseek/deepseek-chat
18
- * martin run "fix the bug" --engine openai
24
+ * martin-loop run "fix the bug" --engine openai
19
25
  *
20
26
  * Or for Ollama:
21
27
  * MARTIN_OPENAI_BASE_URL=http://localhost:11434
22
28
  * MARTIN_OPENAI_MODEL=llama3.3
23
- * martin run "fix the bug" --engine openai
29
+ * martin-loop run "fix the bug" --engine openai
24
30
  */
25
- import { runVerification, readGitExecutionArtifacts } from "./cli-bridge.js";
31
+ import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
26
32
  import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
27
33
  // ---------------------------------------------------------------------------
28
34
  // OpenRouter/OpenAI-compatible model pricing ($/1K tokens)
@@ -73,6 +79,18 @@ Follow these rules exactly:
73
79
  - If the task asks you to write or modify code, output the complete file content with changes applied.
74
80
  - Be precise, minimal, and test-backed in all changes.
75
81
  - State what you changed and why at the end of your response.`;
82
+ export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
83
+ export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
84
+ export function resolveOpenAiCompatibleRuntimeConfig(env = process.env) {
85
+ const apiKey = env["MARTIN_OPENAI_API_KEY"] ?? "";
86
+ return {
87
+ baseUrl: env["MARTIN_OPENAI_BASE_URL"] ?? DEFAULT_OPENAI_BASE_URL,
88
+ model: env["MARTIN_OPENAI_MODEL"] ?? DEFAULT_OPENAI_MODEL,
89
+ apiKey,
90
+ apiKeyConfigured: apiKey.length > 0,
91
+ authPosture: apiKey.length > 0 ? "api_key" : "anonymous_or_local"
92
+ };
93
+ }
76
94
  function buildPrompt(request) {
77
95
  const lines = [
78
96
  `TASK: ${request.context.taskTitle}`,
@@ -105,13 +123,17 @@ export function createOpenAiCompatibleAdapter(options) {
105
123
  const verifyTimeoutMs = options.verifyTimeoutMs ?? 60_000;
106
124
  const systemPrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
107
125
  const fetchFn = options.fetchImpl ?? globalThis.fetch;
126
+ const runtimeConfig = resolveOpenAiCompatibleRuntimeConfig();
127
+ const baseUrl = (options.baseUrl ?? runtimeConfig.baseUrl).replace(/\/$/, "");
128
+ const model = options.model ?? runtimeConfig.model;
129
+ const apiKey = options.apiKey ?? runtimeConfig.apiKey;
108
130
  return {
109
- adapterId: `openai-compatible:${options.model}`,
131
+ adapterId: `openai-compatible:${model}`,
110
132
  kind: "direct-provider",
111
- label: `OpenAI-compatible: ${options.model}`,
133
+ label: `OpenAI-compatible: ${model}`,
112
134
  metadata: {
113
135
  providerId: "openai-compatible",
114
- model: options.model,
136
+ model,
115
137
  transport: "http",
116
138
  capabilities: createAdapterCapabilities({
117
139
  preflight: true,
@@ -121,7 +143,8 @@ export function createOpenAiCompatibleAdapter(options) {
121
143
  },
122
144
  async execute(request) {
123
145
  const prompt = buildPrompt(request);
124
- const estimated = estimateCost(options.model, prompt.length, 2000);
146
+ const estimated = estimateCost(model, prompt.length, 2000);
147
+ const baselineChangedFiles = new Set(await readGitChangedFiles(workingDirectory, 5_000));
125
148
  // Preflight: bail if projected cost exceeds remaining budget
126
149
  if (request.context.remainingBudgetUsd > 0 &&
127
150
  estimated.actualUsd > request.context.remainingBudgetUsd * 0.95) {
@@ -140,7 +163,7 @@ export function createOpenAiCompatibleAdapter(options) {
140
163
  };
141
164
  }
142
165
  // Call the OpenAI-compatible endpoint
143
- const endpoint = `${options.baseUrl.replace(/\/$/, "")}/v1/chat/completions`;
166
+ const endpoint = `${baseUrl}/v1/chat/completions`;
144
167
  let responseText = "";
145
168
  let tokensIn = estimated.tokensIn;
146
169
  let tokensOut = 0;
@@ -148,10 +171,10 @@ export function createOpenAiCompatibleAdapter(options) {
148
171
  const timer = setTimeout(() => controller.abort(), timeoutMs);
149
172
  try {
150
173
  const headers = { "Content-Type": "application/json" };
151
- if (options.apiKey)
152
- headers["Authorization"] = `Bearer ${options.apiKey}`;
174
+ if (apiKey)
175
+ headers["Authorization"] = `Bearer ${apiKey}`;
153
176
  // OpenRouter requires a site URL header for attribution
154
- if (options.baseUrl.includes("openrouter")) {
177
+ if (baseUrl.includes("openrouter")) {
155
178
  headers["HTTP-Referer"] = "https://martinloop.com";
156
179
  headers["X-Title"] = "MartinLoop";
157
180
  }
@@ -159,7 +182,7 @@ export function createOpenAiCompatibleAdapter(options) {
159
182
  method: "POST",
160
183
  headers,
161
184
  body: JSON.stringify({
162
- model: options.model,
185
+ model,
163
186
  messages: [
164
187
  { role: "system", content: systemPrompt },
165
188
  { role: "user", content: prompt }
@@ -174,7 +197,7 @@ export function createOpenAiCompatibleAdapter(options) {
174
197
  const errMsg = body.error?.message ?? `HTTP ${res.status}`;
175
198
  return {
176
199
  status: "failed",
177
- summary: `${options.model} API error: ${errMsg}`,
200
+ summary: `${model} API error: ${errMsg}`,
178
201
  usage: normalizeUsage({ actualUsd: 0, tokensIn: 0, tokensOut: 0, provenance: "unavailable" }),
179
202
  verification: { passed: false, summary: "API call failed before verifier." },
180
203
  failure: { message: errMsg, classHint: "infrastructure_error" }
@@ -191,7 +214,7 @@ export function createOpenAiCompatibleAdapter(options) {
191
214
  }
192
215
  catch (error) {
193
216
  const isAbort = error instanceof Error && error.name === "AbortError";
194
- const message = isAbort ? `${options.model} request timed out after ${timeoutMs}ms` : String(error);
217
+ const message = isAbort ? `${model} request timed out after ${timeoutMs}ms` : String(error);
195
218
  return {
196
219
  status: "failed",
197
220
  summary: message,
@@ -206,7 +229,7 @@ export function createOpenAiCompatibleAdapter(options) {
206
229
  if (!responseText.trim()) {
207
230
  return {
208
231
  status: "failed",
209
- summary: `${options.model} returned an empty response.`,
232
+ summary: `${model} returned an empty response.`,
210
233
  usage: normalizeUsage({ actualUsd: 0, tokensIn, tokensOut: 0, provenance: "actual" }),
211
234
  verification: { passed: false, summary: "Empty response — nothing to verify." },
212
235
  failure: { message: "empty_response" }
@@ -214,8 +237,10 @@ export function createOpenAiCompatibleAdapter(options) {
214
237
  }
215
238
  // Run verification
216
239
  const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack);
217
- const execution = await readGitExecutionArtifacts(workingDirectory, 5_000);
218
- const pricing = KNOWN_MODEL_PRICING[options.model] ?? {
240
+ const execution = {
241
+ changedFiles: (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
242
+ };
243
+ const pricing = KNOWN_MODEL_PRICING[model] ?? {
219
244
  inputPer1K: FALLBACK_INPUT_PER_1K,
220
245
  outputPer1K: FALLBACK_OUTPUT_PER_1K
221
246
  };
@@ -223,8 +248,8 @@ export function createOpenAiCompatibleAdapter(options) {
223
248
  return {
224
249
  status: verification.passed ? "completed" : "failed",
225
250
  summary: verification.passed
226
- ? `${options.model} completed the task. Verifier passed.`
227
- : `${options.model} completed but verifier failed: ${verification.summary}`,
251
+ ? `${model} completed the task. Verifier passed.`
252
+ : `${model} completed but verifier failed: ${verification.summary}`,
228
253
  usage: normalizeUsage({
229
254
  actualUsd,
230
255
  tokensIn,
@@ -7,7 +7,10 @@ export declare function normalizeUsage(input: {
7
7
  estimatedUsd?: number;
8
8
  tokensIn?: number;
9
9
  tokensOut?: number;
10
+ cachedInputTokens?: number;
11
+ reasoningTokensOut?: number;
10
12
  provenance?: MartinAdapterResult["usage"]["provenance"];
13
+ providerSettlement?: MartinAdapterResult["usage"]["providerSettlement"];
11
14
  }): MartinAdapterResult["usage"];
12
15
  export declare function diffStatsFromNumstat(stdout: string): DiffStats | undefined;
13
16
  export declare function normalizeStructuredErrors(errors: StructuredError[] | undefined): StructuredError[];
@@ -21,7 +21,14 @@ export function normalizeUsage(input) {
21
21
  : {}),
22
22
  tokensIn: input.tokensIn ?? 0,
23
23
  tokensOut: input.tokensOut ?? 0,
24
- provenance
24
+ ...(input.cachedInputTokens !== undefined
25
+ ? { cachedInputTokens: input.cachedInputTokens }
26
+ : {}),
27
+ ...(input.reasoningTokensOut !== undefined
28
+ ? { reasoningTokensOut: input.reasoningTokensOut }
29
+ : {}),
30
+ provenance,
31
+ ...(input.providerSettlement ? { providerSettlement: input.providerSettlement } : {})
25
32
  };
26
33
  }
27
34
  export function diffStatsFromNumstat(stdout) {
@@ -1,4 +1,4 @@
1
- import { readGitExecutionArtifacts, runVerification } from "./cli-bridge.js";
1
+ import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
2
2
  import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
3
3
  export function createVerifierOnlyAdapter(options = {}) {
4
4
  const workingDirectory = options.workingDirectory ?? process.cwd();
@@ -17,9 +17,10 @@ export function createVerifierOnlyAdapter(options = {}) {
17
17
  })
18
18
  },
19
19
  async execute(request) {
20
+ const baselineChangedFiles = new Set(await readGitChangedFiles(workingDirectory, 5_000));
20
21
  const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack);
21
- const execution = await readGitExecutionArtifacts(workingDirectory, 5_000);
22
- const changedFiles = execution.changedFiles ?? [];
22
+ const changedFiles = (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file));
23
+ const execution = { changedFiles };
23
24
  if (verification.passed) {
24
25
  return {
25
26
  status: "completed",
@@ -49,6 +49,40 @@ export interface LoopCost {
49
49
  avoidedUsd: number;
50
50
  tokensIn: number;
51
51
  tokensOut: number;
52
+ estimatedUsd?: number;
53
+ provenance?: CostProvenance;
54
+ providerSettlement?: ProviderUsageSettlement;
55
+ }
56
+ export type UsageSettlementSource = "claude_json" | "codex_jsonl" | "gemini_json" | "estimated_fallback" | "unavailable";
57
+ export interface ProviderUsageSettlement {
58
+ providerId: string;
59
+ model: string;
60
+ transport?: "cli" | "http" | "routed_http";
61
+ source: UsageSettlementSource;
62
+ inputTokens: number;
63
+ cachedInputTokens?: number;
64
+ outputTokens: number;
65
+ reasoningOutputTokens?: number;
66
+ rawUsageAvailable: boolean;
67
+ settledAt: string;
68
+ }
69
+ export interface ReceiptScope {
70
+ repoRoot?: string;
71
+ workingDirectory?: string;
72
+ invocationRoot?: string;
73
+ runsRoot?: string;
74
+ }
75
+ export type ReceiptIntegrityState = "verified" | "unsigned" | "tamper_detected";
76
+ export interface ReceiptIntegritySummary {
77
+ state: ReceiptIntegrityState;
78
+ keyId?: string;
79
+ signedAt?: string;
80
+ loopRecordSha256?: string;
81
+ ledgerSha256?: string;
82
+ ledgerHeadHash?: string;
83
+ entryCount?: number;
84
+ reason?: string;
85
+ warnings?: string[];
52
86
  }
53
87
  export interface LoopArtifact {
54
88
  artifactId: string;
@@ -90,6 +124,8 @@ export interface LoopRecord {
90
124
  metadata: Record<string, string>;
91
125
  createdAt: string;
92
126
  updatedAt: string;
127
+ receiptScope?: ReceiptScope;
128
+ receiptIntegrity?: ReceiptIntegritySummary;
93
129
  }
94
130
  export interface LoopRecordDraft {
95
131
  loopId?: string;
@@ -107,6 +143,8 @@ export interface LoopRecordDraft {
107
143
  metadata?: Record<string, string>;
108
144
  createdAt?: string;
109
145
  updatedAt?: string;
146
+ receiptScope?: ReceiptScope;
147
+ receiptIntegrity?: ReceiptIntegritySummary;
110
148
  }
111
149
  export type { MartinErrorCategory, MartinOutputMode, MartinRunListFilters, MartinRunSelector } from "./operator.js";
112
150
  export { MARTIN_ERROR_CATEGORIES } from "./operator.js";
@@ -272,6 +310,7 @@ export interface BudgetSettlement {
272
310
  usd: number;
273
311
  provenance: CostProvenance;
274
312
  };
313
+ providerSettlement?: ProviderUsageSettlement;
275
314
  totalActualUsd: number;
276
315
  preflightEstimateUsd: number;
277
316
  varianceUsd: number;
@@ -36,6 +36,8 @@ export function createLoopRecord(draft, options = {}) {
36
36
  },
37
37
  createdAt: draft.createdAt ?? now,
38
38
  updatedAt: draft.updatedAt ?? now,
39
+ ...(draft.receiptScope ? { receiptScope: draft.receiptScope } : {}),
40
+ ...(draft.receiptIntegrity ? { receiptIntegrity: draft.receiptIntegrity } : {}),
39
41
  ...(draft.teamId ? { teamId: draft.teamId } : {})
40
42
  };
41
43
  }
@@ -1,4 +1,4 @@
1
- import { type ApprovalPolicy, type CostProvenance, type ExecutionProfile, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type MutationMode, type LoopRecord, type LoopTask } from "../contracts/index.js";
1
+ import { type ApprovalPolicy, type CostProvenance, type ExecutionProfile, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type ProviderUsageSettlement, type MutationMode, type LoopRecord, type LoopTask, type ReceiptScope } from "../contracts/index.js";
2
2
  import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, type ExitDecision } from "./policy.js";
3
3
  import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
4
4
  import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
@@ -13,8 +13,8 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
13
13
  export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
14
14
  export { compilePromptPacket } from "./compiler.js";
15
15
  export type { PromptPacket, CompilerAdapterRequest } from "./compiler.js";
16
- export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot } from "./persistence/index.js";
17
- export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, RunContract, RunStore } from "./persistence/index.js";
16
+ export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
17
+ export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
18
18
  export { compileAndPersistContext } from "./persistence/index.js";
19
19
  export type { CompileResult } from "./persistence/index.js";
20
20
  export interface MartinAdapterRequest {
@@ -44,6 +44,20 @@ export interface MartinAdapterRequest {
44
44
  };
45
45
  previousAttempts: LoopAttempt[];
46
46
  }
47
+ export interface MartinVerificationStep {
48
+ command: string;
49
+ launched: boolean;
50
+ exitCode?: number;
51
+ timedOut: boolean;
52
+ fastFail?: boolean;
53
+ detail?: string;
54
+ }
55
+ export interface MartinVerificationOutcome {
56
+ passed: boolean;
57
+ summary: string;
58
+ steps?: MartinVerificationStep[];
59
+ warnings?: string[];
60
+ }
47
61
  export interface MartinAdapterResult {
48
62
  status: "completed" | "failed";
49
63
  summary: string;
@@ -52,11 +66,16 @@ export interface MartinAdapterResult {
52
66
  estimatedUsd?: number;
53
67
  tokensIn: number;
54
68
  tokensOut: number;
69
+ cachedInputTokens?: number;
70
+ reasoningTokensOut?: number;
55
71
  provenance?: CostProvenance;
72
+ providerSettlement?: ProviderUsageSettlement;
56
73
  };
57
74
  verification: {
58
75
  passed: boolean;
59
76
  summary: string;
77
+ steps?: MartinVerificationStep[];
78
+ warnings?: string[];
60
79
  };
61
80
  execution?: {
62
81
  changedFiles?: string[];
@@ -135,6 +154,7 @@ export interface RunMartinInput {
135
154
  maxRecentAttempts?: number;
136
155
  fallbackModels?: string[];
137
156
  fallbackAdapters?: MartinAdapter[];
157
+ receiptScope?: ReceiptScope;
138
158
  /** Optional persistence store. When provided, runMartin writes artifacts on each lifecycle event. */
139
159
  store?: RunStore;
140
160
  }