@martinloop/mcp 0.3.9 → 0.5.1

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 (113) hide show
  1. package/README.md +82 -141
  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 +62 -98
  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 +192 -15
  10. package/dist/tools/doctor.js +5 -4
  11. package/dist/tools/get-status.d.ts +2 -0
  12. package/dist/tools/get-status.js +1 -0
  13. package/dist/tools/preflight.d.ts +2 -0
  14. package/dist/tools/preflight.js +43 -3
  15. package/dist/tools/run-dossier.d.ts +3 -1
  16. package/dist/tools/run-dossier.js +40 -2
  17. package/dist/tools/run-loop.d.ts +5 -3
  18. package/dist/tools/run-loop.js +15 -9
  19. package/dist/tools/tool-errors.js +1 -1
  20. package/dist/tools/tool-response.js +9 -2
  21. package/dist/tools/tool-support.d.ts +4 -2
  22. package/dist/tools/tool-support.js +6 -15
  23. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  24. package/dist/vendor/adapters/claude-cli.js +97 -69
  25. package/dist/vendor/adapters/cli-bridge.d.ts +9 -1
  26. package/dist/vendor/adapters/cli-bridge.js +54 -13
  27. package/dist/vendor/adapters/codex-launcher.d.ts +61 -1
  28. package/dist/vendor/adapters/codex-launcher.js +118 -19
  29. package/dist/vendor/adapters/index.d.ts +2 -4
  30. package/dist/vendor/adapters/index.js +2 -4
  31. package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
  32. package/dist/vendor/adapters/openai-compatible.js +80 -15
  33. package/dist/vendor/adapters/verifier-only.js +9 -4
  34. package/dist/vendor/contracts/adapter-capabilities.d.ts +1 -1
  35. package/dist/vendor/contracts/context-handoff.d.ts +100 -0
  36. package/dist/vendor/contracts/context-handoff.js +11 -0
  37. package/dist/vendor/contracts/context-manifest.d.ts +195 -0
  38. package/dist/vendor/contracts/context-manifest.js +17 -0
  39. package/dist/vendor/contracts/context-shadow.d.ts +75 -0
  40. package/dist/vendor/contracts/context-shadow.js +8 -0
  41. package/dist/vendor/contracts/delivery.d.ts +47 -0
  42. package/dist/vendor/contracts/delivery.js +18 -0
  43. package/dist/vendor/contracts/exits.d.ts +126 -0
  44. package/dist/vendor/contracts/exits.js +32 -0
  45. package/dist/vendor/contracts/index.d.ts +55 -6
  46. package/dist/vendor/contracts/index.js +24 -0
  47. package/dist/vendor/contracts/mission.d.ts +102 -0
  48. package/dist/vendor/contracts/mission.js +64 -0
  49. package/dist/vendor/contracts/operator.d.ts +3 -1
  50. package/dist/vendor/contracts/operator.js +2 -1
  51. package/dist/vendor/contracts/verified-handoff.d.ts +106 -0
  52. package/dist/vendor/contracts/verified-handoff.js +40 -0
  53. package/dist/vendor/core/context-chain-gate.d.ts +89 -0
  54. package/dist/vendor/core/context-chain-gate.js +173 -0
  55. package/dist/vendor/core/context-compiler.d.ts +53 -0
  56. package/dist/vendor/core/context-compiler.js +261 -0
  57. package/dist/vendor/core/context-handoff.d.ts +42 -0
  58. package/dist/vendor/core/context-handoff.js +167 -0
  59. package/dist/vendor/core/context-shadow.d.ts +39 -0
  60. package/dist/vendor/core/context-shadow.js +200 -0
  61. package/dist/vendor/core/delivery/index.d.ts +6 -0
  62. package/dist/vendor/core/delivery/index.js +4 -0
  63. package/dist/vendor/core/delivery/message-client.d.ts +18 -0
  64. package/dist/vendor/core/delivery/message-client.js +38 -0
  65. package/dist/vendor/core/delivery/message-ledger.d.ts +35 -0
  66. package/dist/vendor/core/delivery/message-ledger.js +97 -0
  67. package/dist/vendor/core/delivery/message-schema.d.ts +16 -0
  68. package/dist/vendor/core/delivery/message-schema.js +111 -0
  69. package/dist/vendor/core/delivery/update-check.d.ts +19 -0
  70. package/dist/vendor/core/delivery/update-check.js +107 -0
  71. package/dist/vendor/core/exit-signal.d.ts +88 -0
  72. package/dist/vendor/core/exit-signal.js +339 -0
  73. package/dist/vendor/core/exits.d.ts +41 -0
  74. package/dist/vendor/core/exits.js +258 -0
  75. package/dist/vendor/core/grounding.d.ts +2 -1
  76. package/dist/vendor/core/grounding.js +40 -5
  77. package/dist/vendor/core/index.d.ts +47 -6
  78. package/dist/vendor/core/index.js +1102 -670
  79. package/dist/vendor/core/mission/aggregate.d.ts +27 -0
  80. package/dist/vendor/core/mission/aggregate.js +42 -0
  81. package/dist/vendor/core/mission/index.d.ts +2 -0
  82. package/dist/vendor/core/mission/index.js +1 -0
  83. package/dist/vendor/core/persistence/compiler.d.ts +8 -0
  84. package/dist/vendor/core/persistence/compiler.js +156 -0
  85. package/dist/vendor/core/persistence/context-handoff-store.d.ts +68 -0
  86. package/dist/vendor/core/persistence/context-handoff-store.js +112 -0
  87. package/dist/vendor/core/persistence/index.d.ts +4 -0
  88. package/dist/vendor/core/persistence/index.js +2 -0
  89. package/dist/vendor/core/persistence/ledger.d.ts +1 -1
  90. package/dist/vendor/core/persistence/mission-store.d.ts +56 -0
  91. package/dist/vendor/core/persistence/mission-store.js +281 -0
  92. package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
  93. package/dist/vendor/core/policy.d.ts +2 -0
  94. package/dist/vendor/core/policy.js +11 -8
  95. package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
  96. package/dist/vendor/core/red-blue/red-phase.js +3 -3
  97. package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
  98. package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
  99. package/dist/vendor/core/rollback.js +15 -1
  100. package/dist/vendor/core/routing.d.ts +0 -42
  101. package/dist/vendor/core/routing.js +4 -154
  102. package/dist/vendor/core/savings.d.ts +13 -0
  103. package/dist/vendor/core/savings.js +22 -0
  104. package/dist/vendor/core/termination-store.d.ts +15 -0
  105. package/dist/vendor/core/termination-store.js +44 -0
  106. package/dist/vendor/core/verified-handoff.d.ts +72 -0
  107. package/dist/vendor/core/verified-handoff.js +238 -0
  108. package/package.json +4 -10
  109. package/server.json +3 -3
  110. package/dist/vendor/adapters/stub-agent-cli.d.ts +0 -8
  111. package/dist/vendor/adapters/stub-agent-cli.js +0 -40
  112. package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
  113. package/dist/vendor/adapters/stub-direct-provider.js +0 -12
@@ -1,4 +1,4 @@
1
- import { createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, probeCodexLaunch, resolveCliCommandAvailability, createVerifierOnlyAdapter } from "../vendor/adapters/index.js";
1
+ import { createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, createVerifierOnlyAdapter, probeCodexLaunch, resolveCliCommandAvailability } from "../vendor/adapters/index.js";
2
2
  import { createFileRunStore, evaluateCostGovernor, resolveRunsRoot, runMartin } from "../vendor/core/index.js";
3
3
  import { normalizeSafePathPatterns, resolveSafeRepoRoot } from "../server-validation.js";
4
4
  import { MartinToolError } from "./tool-errors.js";
@@ -6,16 +6,12 @@ import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, build
6
6
  import { normalizeLoopBudget } from "./workflow-governance.js";
7
7
  let proofModeVerifierSpawnImpl;
8
8
  let runStoreOverrideForTests;
9
- let runMartinImpl = runMartin;
10
9
  export function __setProofModeVerifierSpawnImplForTests(spawnImpl) {
11
10
  proofModeVerifierSpawnImpl = spawnImpl;
12
11
  }
13
12
  export function __setRunStoreOverrideForTests(store) {
14
13
  runStoreOverrideForTests = store;
15
14
  }
16
- export function __setRunMartinImplForTests(impl) {
17
- runMartinImpl = impl ?? runMartin;
18
- }
19
15
  export async function runLoopTool(input) {
20
16
  const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
21
17
  const engine = input.engine ?? "claude";
@@ -23,6 +19,9 @@ export async function runLoopTool(input) {
23
19
  const allowedPaths = normalizeSafePathPatterns(input.allowedPaths, "allowedPaths");
24
20
  const deniedPaths = normalizeSafePathPatterns(input.deniedPaths, "deniedPaths");
25
21
  const executionMode = resolveExecutionMode();
22
+ const evidenceExecutionMode = executionMode.liveMode
23
+ ? "governed"
24
+ : "verification_only";
26
25
  const workspaceRoot = resolveSafeRepoRoot();
27
26
  const runsRoot = resolveRunsRoot(process.env);
28
27
  const receiptScope = {
@@ -38,7 +37,7 @@ export async function runLoopTool(input) {
38
37
  if (!engineAvailability.available) {
39
38
  throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
40
39
  category: "environment",
41
- suggestion: "Install the requested CLI or set MARTIN_LIVE=false for a no-spend proof run.",
40
+ suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
42
41
  retryable: false
43
42
  });
44
43
  }
@@ -60,7 +59,7 @@ export async function runLoopTool(input) {
60
59
  if (!engineAvailability.available) {
61
60
  throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
62
61
  category: "environment",
63
- suggestion: "Install the requested CLI or set MARTIN_LIVE=false for a no-spend proof run.",
62
+ suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
64
63
  retryable: false
65
64
  });
66
65
  }
@@ -69,7 +68,7 @@ export async function runLoopTool(input) {
69
68
  const adapter = !executionMode.liveMode
70
69
  ? createVerifierOnlyAdapter({
71
70
  workingDirectory,
72
- label: "Proof mode adapter (MARTIN_LIVE=false)",
71
+ label: "Verification-only adapter (MARTIN_LIVE=false)",
73
72
  ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
74
73
  ...(proofModeVerifierSpawnImpl ? { spawnImpl: proofModeVerifierSpawnImpl } : {})
75
74
  })
@@ -102,7 +101,7 @@ export async function runLoopTool(input) {
102
101
  partialBudget.maxTokens = input.maxTokens;
103
102
  }
104
103
  const budget = normalizeLoopBudget(partialBudget);
105
- const result = await runMartinImpl({
104
+ const result = await runMartin({
106
105
  workspaceId: input.workspaceId ?? "ws_mcp",
107
106
  projectId: input.projectId ?? "proj_mcp",
108
107
  store: runStoreOverrideForTests ?? createFileRunStore({ runsRoot }),
@@ -119,6 +118,10 @@ export async function runLoopTool(input) {
119
118
  ...(deniedPaths ? { deniedPaths } : {})
120
119
  },
121
120
  budget,
121
+ metadata: {
122
+ executionMode: evidenceExecutionMode,
123
+ governanceClaimEligible: evidenceExecutionMode === "governed" ? "true" : "false"
124
+ },
122
125
  adapter
123
126
  });
124
127
  const lastAttempt = result.loop.attempts.at(-1);
@@ -142,7 +145,10 @@ export async function runLoopTool(input) {
142
145
  reason: result.decision.reason,
143
146
  attempts: result.loop.attempts.length,
144
147
  costUsd: result.loop.cost.actualUsd,
148
+ costProvenance: result.loop.cost.provenance ?? "unavailable",
145
149
  verificationPassed,
150
+ executionMode: evidenceExecutionMode,
151
+ governanceClaimEligible: evidenceExecutionMode === "governed",
146
152
  loopId: result.loop.loopId,
147
153
  pressure: costState.pressure,
148
154
  shouldStop: costState.shouldStop,
@@ -92,7 +92,7 @@ export function toToolFailure(error) {
92
92
  code: "engine_unavailable",
93
93
  category: "environment",
94
94
  message,
95
- suggestion: "Install the requested CLI or set MARTIN_LIVE=false for a no-spend proof run.",
95
+ suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
96
96
  retryable: false
97
97
  };
98
98
  }
@@ -4,13 +4,20 @@ function createJsonBlock(output) {
4
4
  text: JSON.stringify(output, null, 2)
5
5
  };
6
6
  }
7
+ function resolveHumanArtifact(output, summary) {
8
+ const rendered = "rendered" in output ? output.rendered : undefined;
9
+ return typeof rendered === "string" && rendered.trim().length > 0 ? rendered : summary;
10
+ }
7
11
  export function createToolSuccessResult(output, summary) {
8
12
  return {
9
- content: [createJsonBlock(output), { type: "text", text: summary }],
13
+ content: [
14
+ { type: "text", text: resolveHumanArtifact(output, summary) },
15
+ createJsonBlock(output)
16
+ ],
10
17
  structuredContent: output,
11
18
  _meta: {
12
19
  "martinloop/summary": summary,
13
- "martinloop/contentVersion": "2026-05-15"
20
+ "martinloop/contentVersion": "2026-08-18"
14
21
  }
15
22
  };
16
23
  }
@@ -1,4 +1,4 @@
1
- import type { LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope } from "../vendor/contracts/index.js";
1
+ import type { CostProvenance, LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope, TerminationEnvelopeV1 } from "../vendor/contracts/index.js";
2
2
  import { type LedgerEvent, type LoopAttemptRecord, type LoopRunRecord } from "../vendor/core/index.js";
3
3
  export declare const MARTIN_ENGINE_VALUES: readonly ["claude", "codex", "gemini", "openai"];
4
4
  export type MartinEngine = (typeof MARTIN_ENGINE_VALUES)[number];
@@ -14,6 +14,7 @@ export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" |
14
14
  metadata?: Record<string, string>;
15
15
  receiptIntegrity?: ReceiptIntegritySummary;
16
16
  receiptScope?: ReceiptScope;
17
+ terminationEnvelope?: TerminationEnvelopeV1;
17
18
  routingEconomics?: import("../vendor/contracts/index.js").RoutingEconomics;
18
19
  }
19
20
  export interface LoopPreview {
@@ -26,6 +27,7 @@ export interface LoopPreview {
26
27
  updatedAt?: string;
27
28
  attempts: number;
28
29
  costUsd: number;
30
+ costProvenance: CostProvenance;
29
31
  avoidedUsd: number;
30
32
  pressure: string;
31
33
  shouldStop: boolean;
@@ -127,7 +129,7 @@ export declare function inspectRunsRoot(runsRoot?: string): Promise<RunStoreInsp
127
129
  export declare function buildRunRecordPaths(runsRoot: string, loopId: string): CanonicalRunPaths;
128
130
  export declare function buildAttemptArtifactDirectory(runsRoot: string, loopId: string, attemptIndex: number): string;
129
131
  export declare function buildAttemptArtifactsReference(runsRoot: string, loopId: string, attemptIndex: number): Promise<AttemptArtifactFiles>;
130
- export declare function buildCostSnapshot(cost: Pick<LoopCost, "actualUsd" | "tokensIn" | "tokensOut"> & {
132
+ export declare function buildCostSnapshot(cost: Pick<LoopCost, "actualUsd" | "tokensIn" | "tokensOut" | "provenance"> & {
131
133
  avoidedUsd?: number;
132
134
  }): LoopCost;
133
135
  export declare function buildBudgetSnapshot(budget: LoopBudget): LoopBudget;
@@ -15,7 +15,7 @@ export function resolveExecutionMode() {
15
15
  mode: liveMode ? "live" : "proof",
16
16
  detail: liveMode
17
17
  ? "Live CLI execution is enabled."
18
- : "Proof mode is active because MARTIN_LIVE=false."
18
+ : "Verification-only mode is active because MARTIN_LIVE=false; governed VERIFIED is unavailable."
19
19
  };
20
20
  }
21
21
  export function detectCliAvailability(command) {
@@ -105,11 +105,6 @@ function discoverCommonInstallDirectories(command) {
105
105
  if (localAppData) {
106
106
  dirs.push(join(localAppData, "OpenAI", "Codex", "bin"));
107
107
  }
108
- // Claude Code native installer places binary at %USERPROFILE%\.local\bin
109
- const userProfile = process.env.USERPROFILE ?? process.env.HOMEPATH;
110
- if (userProfile) {
111
- dirs.push(join(userProfile, ".local", "bin"));
112
- }
113
108
  // Scoop
114
109
  if (home) {
115
110
  dirs.push(join(home, "scoop", "shims"));
@@ -166,13 +161,8 @@ function isOnPathDirectly(command, resolvedPath) {
166
161
  * Returns a platform-specific one-liner install command for a known CLI.
167
162
  */
168
163
  function suggestInstallCommand(command) {
169
- if (command === "claude") {
170
- const installCmd = process.platform === "win32"
171
- ? "irm https://claude.ai/install.ps1 | iex"
172
- : "curl -fsSL https://claude.ai/install.sh | bash";
173
- return `Install with: ${installCmd}`;
174
- }
175
164
  const npmInstalls = {
165
+ claude: "npm install -g @anthropic-ai/claude-code",
176
166
  codex: "npm install -g @openai/codex",
177
167
  gemini: "npm install -g @google/gemini-cli"
178
168
  };
@@ -185,7 +175,7 @@ function suggestInstallCommand(command) {
185
175
  export function getEngineAvailability(engine) {
186
176
  return detectCliAvailability(engine);
187
177
  }
188
- export function createSkippedCliAvailability(command, detail = "Proof mode skipped live CLI availability detection.") {
178
+ export function createSkippedCliAvailability(command, detail = "Verification-only mode skipped live CLI availability detection; governed VERIFIED is unavailable.") {
189
179
  return {
190
180
  command,
191
181
  available: false,
@@ -218,6 +208,7 @@ export function buildLoopPreview(loop) {
218
208
  ...(loop.updatedAt ? { updatedAt: loop.updatedAt } : {}),
219
209
  attempts: loop.attempts.length,
220
210
  costUsd: loop.cost.actualUsd,
211
+ costProvenance: loop.cost.provenance ?? "unavailable",
221
212
  avoidedUsd: loop.cost.avoidedUsd ?? 0,
222
213
  pressure: costState.pressure,
223
214
  shouldStop: costState.shouldStop,
@@ -421,7 +412,8 @@ export function buildCostSnapshot(cost) {
421
412
  actualUsd: cost.actualUsd,
422
413
  avoidedUsd: cost.avoidedUsd ?? 0,
423
414
  tokensIn: cost.tokensIn,
424
- tokensOut: cost.tokensOut
415
+ tokensOut: cost.tokensOut,
416
+ provenance: cost.provenance ?? "unavailable"
425
417
  };
426
418
  }
427
419
  export function buildBudgetSnapshot(budget) {
@@ -439,7 +431,6 @@ export function buildSuggestedResourceUris(loopId) {
439
431
  "martin://runs/triage",
440
432
  "martin://runs/latest",
441
433
  "martin://runs/latest/summary",
442
- "martin://runs/latest/receipt",
443
434
  "martin://runs/latest/proof-card",
444
435
  "martin://runs/latest/budget-status",
445
436
  "martin://runs/latest/verifier-evidence",
@@ -96,7 +96,7 @@ export interface GeminiCliAdapterOptions {
96
96
  timeoutMs?: number;
97
97
  verifyTimeoutMs?: number;
98
98
  label?: string;
99
- /** Override the model passed via --model flag. Defaults to the Gemini `flash` alias. */
99
+ /** Explicit model override passed via --model. Omitted to preserve Gemini Auto. */
100
100
  model?: string;
101
101
  /** Approval mode for headless Gemini runs. Defaults to yolo for autonomous execution. */
102
102
  approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
@@ -135,7 +135,7 @@ export declare function createClaudeCliAdapter(options?: ClaudeCliAdapterOptions
135
135
  */
136
136
  export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): MartinAdapter;
137
137
  /**
138
- * Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
138
+ * Spawns `gemini [--model <explicit-model>] --prompt "" --approval-mode <mode> --output-format json [...]`.
139
139
  *
140
140
  * The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
141
141
  * which keeps large MartinLoop prompts off the command line on Windows.
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * MCP tools and integration tests use the same factories.
13
13
  */
14
- import { readGitExecutionArtifacts, resolveGitRepositoryRoot, runSubprocess, runVerification } from "./cli-bridge.js";
15
- import { buildCodexExecArgs, DEFAULT_CODEX_CHATGPT_MODEL } from "./codex-launcher.js";
14
+ import { readGitChangedFiles, readGitExecutionArtifacts, resolveGitRepositoryRoot, runSubprocess, runVerification } from "./cli-bridge.js";
15
+ import { buildCodexExecArgs } from "./codex-launcher.js";
16
16
  import { createAdapterCapabilities, normalizeStructuredErrors, normalizeUsage } from "./runtime-support.js";
17
17
  // ---------------------------------------------------------------------------
18
18
  // Cost estimation
@@ -68,7 +68,22 @@ function calculateUsageCost(usage, pricing) {
68
68
  (usage.cacheCreationInputTokens / 1000) * (pricing.cacheCreationInputPer1K ?? pricing.inputPer1K) +
69
69
  (usage.outputTokens / 1000) * pricing.outputPer1K);
70
70
  }
71
- function extractUsage(parsed, modelLabel) {
71
+ const EMBEDDED_PRICING_VERSION = "embedded-v1";
72
+ function extractClaudeObservedModel(stdout) {
73
+ for (const line of stdout.split(/\r?\n/u)) {
74
+ try {
75
+ const event = JSON.parse(line);
76
+ if (event.type === "system" && event.subtype === "init" && typeof event.model === "string") {
77
+ return event.model;
78
+ }
79
+ }
80
+ catch {
81
+ // Ignore non-JSON stream fragments.
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ function extractUsage(parsed, modelLabel, modelSource) {
72
87
  if (!parsed?.usage) {
73
88
  return normalizeUsage({
74
89
  actualUsd: 0,
@@ -102,15 +117,24 @@ function extractUsage(parsed, modelLabel) {
102
117
  tokensIn,
103
118
  tokensOut,
104
119
  cachedInputTokens,
105
- provenance: hasAuthoritativeCost ? "actual" : pricingResolution.pricing ? "estimated" : "unavailable",
120
+ provenance: hasAuthoritativeCost ? "actual" : pricingResolution.pricing ? "calculated" : "unavailable",
106
121
  providerSettlement: {
107
122
  providerId: "claude",
108
- model: modelLabel ?? "claude",
123
+ ...(modelLabel ? { model: modelLabel } : {}),
109
124
  transport: "cli",
110
125
  source: "claude_json",
111
126
  inputTokens: promptTokens + cacheCreationInputTokens,
112
127
  cachedInputTokens,
128
+ cacheCreationInputTokens,
113
129
  outputTokens: tokensOut,
130
+ billingMode: "unknown",
131
+ modelSource,
132
+ pricingSource: hasAuthoritativeCost
133
+ ? "provider_reported_total"
134
+ : pricingResolution.pricing
135
+ ? "static_catalog"
136
+ : "none",
137
+ ...(pricingResolution.pricing ? { pricingVersion: EMBEDDED_PRICING_VERSION } : {}),
114
138
  rawUsageAvailable: true,
115
139
  settledAt: new Date().toISOString()
116
140
  }
@@ -152,7 +176,7 @@ function extractCodexJsonlResult(stdout, modelLabel) {
152
176
  provenance: "unavailable",
153
177
  providerSettlement: {
154
178
  providerId: "codex",
155
- model: modelLabel ?? "codex",
179
+ ...(modelLabel ? { model: modelLabel } : {}),
156
180
  transport: "cli",
157
181
  source: "unavailable",
158
182
  inputTokens: 0,
@@ -169,8 +193,8 @@ function extractCodexJsonlResult(stdout, modelLabel) {
169
193
  const reasoningOutputTokens = latestTurnCompleted.usage.reasoning_output_tokens ?? 0;
170
194
  const tokensIn = promptTokens + cachedInputTokens;
171
195
  const tokensOut = outputTokens + reasoningOutputTokens;
172
- const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
173
- MODEL_PRICING["codex"] ??
196
+ const exactPricing = modelLabel ? MODEL_PRICING[modelLabel] : undefined;
197
+ const pricing = exactPricing ?? MODEL_PRICING["codex"] ??
174
198
  { inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
175
199
  const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
176
200
  (cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
@@ -183,16 +207,20 @@ function extractCodexJsonlResult(stdout, modelLabel) {
183
207
  tokensOut,
184
208
  cachedInputTokens,
185
209
  reasoningTokensOut: reasoningOutputTokens,
186
- provenance: "actual",
210
+ provenance: exactPricing ? "calculated" : "estimated",
187
211
  providerSettlement: {
188
212
  providerId: "codex",
189
- model: modelLabel ?? "codex",
213
+ ...(modelLabel ? { model: modelLabel } : {}),
190
214
  transport: "cli",
191
215
  source: "codex_jsonl",
192
216
  inputTokens: promptTokens,
193
217
  cachedInputTokens,
194
218
  outputTokens,
195
219
  reasoningOutputTokens,
220
+ billingMode: "unknown",
221
+ modelSource: modelLabel ? "explicit_override" : "agent_default",
222
+ pricingSource: exactPricing ? "static_catalog" : "blended_fallback",
223
+ pricingVersion: EMBEDDED_PRICING_VERSION,
196
224
  rawUsageAvailable: true,
197
225
  settledAt: new Date().toISOString()
198
226
  }
@@ -228,7 +256,7 @@ function extractGeminiJsonResult(stdout, modelLabel) {
228
256
  provenance: "unavailable",
229
257
  providerSettlement: {
230
258
  providerId: "gemini",
231
- model: modelLabel ?? "flash",
259
+ ...(modelLabel ? { model: modelLabel } : {}),
232
260
  transport: "cli",
233
261
  source: "unavailable",
234
262
  inputTokens: 0,
@@ -254,16 +282,20 @@ function extractGeminiJsonResult(stdout, modelLabel) {
254
282
  tokensOut,
255
283
  cachedInputTokens,
256
284
  reasoningTokensOut: reasoningOutputTokens,
257
- provenance: "actual",
285
+ provenance: "estimated",
258
286
  providerSettlement: {
259
287
  providerId: "gemini",
260
- model: modelLabel ?? "flash",
288
+ ...(modelLabel ? { model: modelLabel } : {}),
261
289
  transport: "cli",
262
290
  source: "gemini_json",
263
291
  inputTokens: promptTokens,
264
292
  cachedInputTokens,
265
293
  outputTokens,
266
294
  reasoningOutputTokens,
295
+ billingMode: "unknown",
296
+ modelSource: modelLabel ? "explicit_override" : "agent_default",
297
+ pricingSource: "blended_fallback",
298
+ pricingVersion: EMBEDDED_PRICING_VERSION,
267
299
  rawUsageAvailable: true,
268
300
  settledAt: new Date().toISOString()
269
301
  }
@@ -381,14 +413,10 @@ function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate)
381
413
  catch {
382
414
  return;
383
415
  }
384
- // result events contain aggregate usage that duplicates previously streamed
385
- // assistant-message usage events — skip to avoid double-counting.
416
+ extractUsageFromEvent(event, terminate);
386
417
  if (event.type === "result") {
387
418
  finalResult = event;
388
419
  }
389
- else {
390
- extractUsageFromEvent(event, terminate);
391
- }
392
420
  };
393
421
  return {
394
422
  onChunk: (chunk, terminate) => {
@@ -501,7 +529,7 @@ export function createAgentCliAdapter(options) {
501
529
  label: options.label ?? `${options.command} CLI adapter`,
502
530
  metadata: {
503
531
  providerId: options.command,
504
- model: options.model ?? options.command,
532
+ ...(options.model ? { model: options.model } : {}),
505
533
  transport: "cli",
506
534
  capabilities: createAdapterCapabilities({
507
535
  preflight: true,
@@ -514,6 +542,14 @@ export function createAgentCliAdapter(options) {
514
542
  async execute(request) {
515
543
  const prompt = buildPrompt(request);
516
544
  const estimatedUsage = estimateUsage(prompt, options.model ?? options.command, options.command);
545
+ const repoRoot = request.context.repoRoot;
546
+ const gitRepoRoot = repoRoot ? resolveGitRepositoryRoot(repoRoot) : undefined;
547
+ // A governed run may begin in a deliberately dirty workspace. Capture that
548
+ // baseline so existing operator work is neither reported as this run's
549
+ // execution nor treated as scope creep.
550
+ const baselineChangedFiles = gitRepoRoot
551
+ ? new Set(await readGitChangedFiles(gitRepoRoot, 5_000, options.spawnImpl))
552
+ : new Set();
517
553
  // Preflight: bail if projected cost exceeds remaining budget
518
554
  if (request.context.remainingBudgetUsd > 0) {
519
555
  const projected = estimatePromptCost(prompt, options.model ?? "", options.command);
@@ -549,7 +585,8 @@ export function createAgentCliAdapter(options) {
549
585
  timeoutMs,
550
586
  spawnImpl: options.spawnImpl,
551
587
  ...(stdinData === undefined ? {} : { stdinData }),
552
- ...(streamingUsage ? { onStdoutChunk: streamingUsage.onChunk } : {})
588
+ ...(streamingUsage ? { onStdoutChunk: streamingUsage.onChunk } : {}),
589
+ ...(request.signal !== undefined ? { signal: request.signal } : {})
553
590
  });
554
591
  if (agentResult.terminationReason) {
555
592
  const snapshot = streamingUsage?.snapshot();
@@ -660,8 +697,15 @@ export function createAgentCliAdapter(options) {
660
697
  parsed?.result ??
661
698
  agentResult.stdout.trim();
662
699
  const summary = truncate(agentText, 2000);
700
+ const observedClaudeModel = options.streamingUsageCap
701
+ ? extractClaudeObservedModel(agentResult.stdout)
702
+ : undefined;
663
703
  const usage = parsed?.usage
664
- ? extractUsage(parsed, options.model)
704
+ ? extractUsage(parsed, observedClaudeModel ?? options.model, observedClaudeModel
705
+ ? "provider_reported"
706
+ : options.model
707
+ ? "explicit_override"
708
+ : "agent_default")
665
709
  : codexJsonlResult?.usage ??
666
710
  geminiJsonResult?.usage ??
667
711
  normalizeUsage({
@@ -673,7 +717,7 @@ export function createAgentCliAdapter(options) {
673
717
  providerSettlement: options.command === "codex"
674
718
  ? {
675
719
  providerId: "codex",
676
- model: options.model ?? "codex",
720
+ ...(options.model ? { model: options.model } : {}),
677
721
  transport: "cli",
678
722
  source: "estimated_fallback",
679
723
  inputTokens: estimatedUsage.tokensIn,
@@ -684,7 +728,7 @@ export function createAgentCliAdapter(options) {
684
728
  : options.command === "gemini"
685
729
  ? {
686
730
  providerId: "gemini",
687
- model: options.model ?? "flash",
731
+ ...(options.model ? { model: options.model } : {}),
688
732
  transport: "cli",
689
733
  source: "estimated_fallback",
690
734
  inputTokens: estimatedUsage.tokensIn,
@@ -695,30 +739,36 @@ export function createAgentCliAdapter(options) {
695
739
  : undefined
696
740
  });
697
741
  const verificationStack = request.context.verificationStack;
698
- const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, verificationStack, options.spawnImpl);
742
+ const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, verificationStack, options.spawnImpl, {
743
+ runId: request.loopId,
744
+ workspaceId: request.workspaceId,
745
+ cwd: workingDirectory,
746
+ });
699
747
  // Check for zero-diff (agent ran but made no file changes)
700
- const repoRoot = request.context.repoRoot;
701
- const gitRepoRoot = repoRoot ? resolveGitRepositoryRoot(repoRoot) : undefined;
702
- let noDiff = false;
703
- if (gitRepoRoot) {
704
- noDiff = await checkNoDiff(gitRepoRoot, options.spawnImpl);
705
- }
748
+ const postRunChangedFiles = gitRepoRoot
749
+ ? await readGitChangedFiles(gitRepoRoot, 5_000, options.spawnImpl)
750
+ : [];
751
+ const agentChangedFiles = postRunChangedFiles.filter((file) => !baselineChangedFiles.has(file));
752
+ const noDiff = gitRepoRoot !== undefined && agentChangedFiles.length === 0;
706
753
  // Extract structured errors from stderr/stdout for better failure context
707
754
  const structuredErrors = normalizeStructuredErrors(extractStructuredErrors(agentResult.stderr, agentResult.stdout));
708
- const executionArtifacts = gitRepoRoot
755
+ const rawExecutionArtifacts = gitRepoRoot
709
756
  ? await readGitExecutionArtifacts(gitRepoRoot, 5000, options.spawnImpl)
710
757
  : undefined;
758
+ const executionArtifacts = rawExecutionArtifacts
759
+ ? {
760
+ ...(agentChangedFiles.length > 0 ? { changedFiles: agentChangedFiles } : {}),
761
+ ...(baselineChangedFiles.size === 0 && rawExecutionArtifacts.diffStats
762
+ ? { diffStats: rawExecutionArtifacts.diffStats }
763
+ : {})
764
+ }
765
+ : undefined;
711
766
  // Scope contract enforcement: check touched files against allowedPaths/deniedPaths
712
767
  let scopeViolations = [];
713
768
  const scopeCtx = request.context;
714
769
  if (gitRepoRoot && (scopeCtx.allowedPaths?.length || scopeCtx.deniedPaths?.length)) {
715
- const diffResult = await runSubprocess("git", ["diff", "--name-only", "HEAD"], {
716
- cwd: gitRepoRoot,
717
- timeoutMs: 5000,
718
- spawnImpl: options.spawnImpl
719
- });
720
- if (diffResult.exitCode === 0 && diffResult.stdout.trim()) {
721
- const touchedFiles = diffResult.stdout.trim().split("\n").filter(Boolean);
770
+ if (agentChangedFiles.length > 0) {
771
+ const touchedFiles = agentChangedFiles;
722
772
  const allowed = scopeCtx.allowedPaths ?? [];
723
773
  const denied = scopeCtx.deniedPaths ?? [];
724
774
  for (const file of touchedFiles) {
@@ -741,7 +791,7 @@ export function createAgentCliAdapter(options) {
741
791
  status: "completed",
742
792
  summary,
743
793
  usage,
744
- verification: { passed: true, summary: verification.summary },
794
+ verification,
745
795
  ...(executionArtifacts
746
796
  ? {
747
797
  execution: {
@@ -786,7 +836,7 @@ export function createAgentCliAdapter(options) {
786
836
  }
787
837
  // Reset tracked files to HEAD so next attempt starts from clean state
788
838
  try {
789
- if (gitRepoRoot) {
839
+ if (gitRepoRoot && baselineChangedFiles.size === 0) {
790
840
  await runSubprocess("git", ["restore", "--staged", "--worktree", "."], {
791
841
  cwd: gitRepoRoot,
792
842
  timeoutMs: 5000
@@ -803,7 +853,7 @@ export function createAgentCliAdapter(options) {
803
853
  ? `${summary}${errorBlock}${scopeViolations.length > 0 ? `\nScope violations: ${scopeViolations.join(", ")}` : ""}`
804
854
  : summary,
805
855
  usage,
806
- verification: { passed: false, summary: verification.summary },
856
+ verification,
807
857
  ...(executionArtifacts
808
858
  ? {
809
859
  execution: {
@@ -819,18 +869,6 @@ export function createAgentCliAdapter(options) {
819
869
  ...(classHint ? { classHint } : {})
820
870
  }
821
871
  };
822
- },
823
- /**
824
- * Return a new adapter instance with a different model.
825
- * Used by run-martin.ts when a change_model intervention fires.
826
- * Model escalation order: haiku → sonnet → opus (cheapest-first, escalate on repeated failure).
827
- */
828
- withModel(newModel) {
829
- return createAgentCliAdapter({
830
- ...options,
831
- model: newModel,
832
- adapterIdSuffix: `${options.adapterIdSuffix ?? options.command}:${newModel}`
833
- });
834
872
  }
835
873
  };
836
874
  return adapter;
@@ -859,7 +897,7 @@ export function createClaudeCliAdapter(options = {}) {
859
897
  return createAgentCliAdapter({
860
898
  command: "claude",
861
899
  adapterIdSuffix: "claude",
862
- model: options.model ?? "claude-sonnet-4-6",
900
+ model: options.model,
863
901
  label: options.label ?? "Claude CLI adapter",
864
902
  workingDirectory: options.workingDirectory,
865
903
  timeoutMs: options.timeoutMs,
@@ -906,11 +944,11 @@ export function createCodexCliAdapter(options = {}) {
906
944
  const sandbox = options.sandbox ?? "workspace-write";
907
945
  const workingDirectory = options.workingDirectory ?? process.cwd();
908
946
  const command = options.command ?? "codex";
909
- const launchModel = options.model ?? DEFAULT_CODEX_CHATGPT_MODEL;
947
+ const launchModel = options.model;
910
948
  return createAgentCliAdapter({
911
949
  command,
912
950
  adapterIdSuffix: "codex",
913
- model: options.model ?? "codex",
951
+ model: options.model,
914
952
  label: options.label ?? "Codex CLI adapter",
915
953
  workingDirectory,
916
954
  timeoutMs: options.timeoutMs,
@@ -931,7 +969,7 @@ export function createCodexCliAdapter(options = {}) {
931
969
  // Pre-configured: Gemini CLI
932
970
  // ---------------------------------------------------------------------------
933
971
  /**
934
- * Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
972
+ * Spawns `gemini [--model <explicit-model>] --prompt "" --approval-mode <mode> --output-format json [...]`.
935
973
  *
936
974
  * The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
937
975
  * which keeps large MartinLoop prompts off the command line on Windows.
@@ -940,13 +978,12 @@ export function createCodexCliAdapter(options = {}) {
940
978
  * npm install -g @google/gemini-cli
941
979
  */
942
980
  export function createGeminiCliAdapter(options = {}) {
943
- const model = options.model ?? "flash";
944
981
  const approvalMode = options.approvalMode ?? "yolo";
945
982
  const extraArgs = options.extraArgs ?? [];
946
983
  return createAgentCliAdapter({
947
984
  command: "gemini",
948
985
  adapterIdSuffix: "gemini",
949
- model,
986
+ model: options.model,
950
987
  label: options.label ?? "Gemini CLI adapter",
951
988
  workingDirectory: options.workingDirectory,
952
989
  timeoutMs: options.timeoutMs,
@@ -954,8 +991,7 @@ export function createGeminiCliAdapter(options = {}) {
954
991
  supportsJsonOutput: false,
955
992
  spawnImpl: options.spawnImpl,
956
993
  argsBuilder: () => [
957
- "--model",
958
- model,
994
+ ...(options.model ? ["--model", options.model] : []),
959
995
  "--prompt",
960
996
  "",
961
997
  "--approval-mode",
@@ -1152,11 +1188,3 @@ function extractStructuredErrors(stderr, stdout) {
1152
1188
  }
1153
1189
  return errors.slice(0, 10); // cap at 10 to avoid bloating prompts
1154
1190
  }
1155
- async function checkNoDiff(repoRoot, spawnImpl) {
1156
- const result = await runSubprocess("git", ["diff", "--name-only", "HEAD"], {
1157
- cwd: repoRoot,
1158
- timeoutMs: 5000,
1159
- spawnImpl
1160
- });
1161
- return result.exitCode === 0 && result.stdout.trim().length === 0;
1162
- }