@martinloop/mcp 0.5.0 → 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 (42) hide show
  1. package/dist/resources.js +15 -3
  2. package/dist/server.js +40 -8
  3. package/dist/tools/doctor.js +5 -4
  4. package/dist/tools/get-status.d.ts +2 -0
  5. package/dist/tools/get-status.js +1 -0
  6. package/dist/tools/preflight.js +1 -1
  7. package/dist/tools/run-loop.d.ts +6 -1
  8. package/dist/tools/run-loop.js +22 -7
  9. package/dist/tools/tool-errors.js +1 -1
  10. package/dist/tools/tool-response.js +9 -2
  11. package/dist/tools/tool-support.d.ts +3 -2
  12. package/dist/tools/tool-support.js +5 -3
  13. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  14. package/dist/vendor/adapters/claude-cli.js +59 -34
  15. package/dist/vendor/adapters/index.d.ts +1 -1
  16. package/dist/vendor/adapters/index.js +1 -1
  17. package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
  18. package/dist/vendor/adapters/openai-compatible.js +33 -7
  19. package/dist/vendor/adapters/verifier-only.d.ts +9 -0
  20. package/dist/vendor/adapters/verifier-only.js +68 -0
  21. package/dist/vendor/contracts/index.d.ts +16 -6
  22. package/dist/vendor/contracts/index.js +1 -1
  23. package/dist/vendor/contracts/verified-handoff.d.ts +6 -0
  24. package/dist/vendor/contracts/verified-handoff.js +5 -0
  25. package/dist/vendor/core/grounding.d.ts +2 -1
  26. package/dist/vendor/core/grounding.js +40 -5
  27. package/dist/vendor/core/index.d.ts +4 -6
  28. package/dist/vendor/core/index.js +25 -19
  29. package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
  30. package/dist/vendor/core/policy.js +6 -6
  31. package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
  32. package/dist/vendor/core/red-blue/red-phase.js +3 -3
  33. package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
  34. package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
  35. package/dist/vendor/core/routing.d.ts +0 -42
  36. package/dist/vendor/core/routing.js +4 -154
  37. package/dist/vendor/core/verified-handoff.d.ts +6 -1
  38. package/dist/vendor/core/verified-handoff.js +28 -0
  39. package/package.json +6 -2
  40. package/server.json +2 -2
  41. package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
  42. package/dist/vendor/adapters/stub-direct-provider.js +0 -12
@@ -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
  }
@@ -497,7 +529,7 @@ export function createAgentCliAdapter(options) {
497
529
  label: options.label ?? `${options.command} CLI adapter`,
498
530
  metadata: {
499
531
  providerId: options.command,
500
- model: options.model ?? options.command,
532
+ ...(options.model ? { model: options.model } : {}),
501
533
  transport: "cli",
502
534
  capabilities: createAdapterCapabilities({
503
535
  preflight: true,
@@ -665,8 +697,15 @@ export function createAgentCliAdapter(options) {
665
697
  parsed?.result ??
666
698
  agentResult.stdout.trim();
667
699
  const summary = truncate(agentText, 2000);
700
+ const observedClaudeModel = options.streamingUsageCap
701
+ ? extractClaudeObservedModel(agentResult.stdout)
702
+ : undefined;
668
703
  const usage = parsed?.usage
669
- ? extractUsage(parsed, options.model)
704
+ ? extractUsage(parsed, observedClaudeModel ?? options.model, observedClaudeModel
705
+ ? "provider_reported"
706
+ : options.model
707
+ ? "explicit_override"
708
+ : "agent_default")
670
709
  : codexJsonlResult?.usage ??
671
710
  geminiJsonResult?.usage ??
672
711
  normalizeUsage({
@@ -678,7 +717,7 @@ export function createAgentCliAdapter(options) {
678
717
  providerSettlement: options.command === "codex"
679
718
  ? {
680
719
  providerId: "codex",
681
- model: options.model ?? "codex",
720
+ ...(options.model ? { model: options.model } : {}),
682
721
  transport: "cli",
683
722
  source: "estimated_fallback",
684
723
  inputTokens: estimatedUsage.tokensIn,
@@ -689,7 +728,7 @@ export function createAgentCliAdapter(options) {
689
728
  : options.command === "gemini"
690
729
  ? {
691
730
  providerId: "gemini",
692
- model: options.model ?? "flash",
731
+ ...(options.model ? { model: options.model } : {}),
693
732
  transport: "cli",
694
733
  source: "estimated_fallback",
695
734
  inputTokens: estimatedUsage.tokensIn,
@@ -830,18 +869,6 @@ export function createAgentCliAdapter(options) {
830
869
  ...(classHint ? { classHint } : {})
831
870
  }
832
871
  };
833
- },
834
- /**
835
- * Return a new adapter instance with a different model.
836
- * Used by run-martin.ts when a change_model intervention fires.
837
- * Model escalation order: haiku → sonnet → opus (cheapest-first, escalate on repeated failure).
838
- */
839
- withModel(newModel) {
840
- return createAgentCliAdapter({
841
- ...options,
842
- model: newModel,
843
- adapterIdSuffix: `${options.adapterIdSuffix ?? options.command}:${newModel}`
844
- });
845
872
  }
846
873
  };
847
874
  return adapter;
@@ -870,7 +897,7 @@ export function createClaudeCliAdapter(options = {}) {
870
897
  return createAgentCliAdapter({
871
898
  command: "claude",
872
899
  adapterIdSuffix: "claude",
873
- model: options.model ?? "claude-sonnet-4-6",
900
+ model: options.model,
874
901
  label: options.label ?? "Claude CLI adapter",
875
902
  workingDirectory: options.workingDirectory,
876
903
  timeoutMs: options.timeoutMs,
@@ -921,7 +948,7 @@ export function createCodexCliAdapter(options = {}) {
921
948
  return createAgentCliAdapter({
922
949
  command,
923
950
  adapterIdSuffix: "codex",
924
- model: options.model ?? "codex",
951
+ model: options.model,
925
952
  label: options.label ?? "Codex CLI adapter",
926
953
  workingDirectory,
927
954
  timeoutMs: options.timeoutMs,
@@ -942,7 +969,7 @@ export function createCodexCliAdapter(options = {}) {
942
969
  // Pre-configured: Gemini CLI
943
970
  // ---------------------------------------------------------------------------
944
971
  /**
945
- * Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
972
+ * Spawns `gemini [--model <explicit-model>] --prompt "" --approval-mode <mode> --output-format json [...]`.
946
973
  *
947
974
  * The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
948
975
  * which keeps large MartinLoop prompts off the command line on Windows.
@@ -951,13 +978,12 @@ export function createCodexCliAdapter(options = {}) {
951
978
  * npm install -g @google/gemini-cli
952
979
  */
953
980
  export function createGeminiCliAdapter(options = {}) {
954
- const model = options.model ?? "flash";
955
981
  const approvalMode = options.approvalMode ?? "yolo";
956
982
  const extraArgs = options.extraArgs ?? [];
957
983
  return createAgentCliAdapter({
958
984
  command: "gemini",
959
985
  adapterIdSuffix: "gemini",
960
- model,
986
+ model: options.model,
961
987
  label: options.label ?? "Gemini CLI adapter",
962
988
  workingDirectory: options.workingDirectory,
963
989
  timeoutMs: options.timeoutMs,
@@ -965,8 +991,7 @@ export function createGeminiCliAdapter(options = {}) {
965
991
  supportsJsonOutput: false,
966
992
  spawnImpl: options.spawnImpl,
967
993
  argsBuilder: () => [
968
- "--model",
969
- model,
994
+ ...(options.model ? ["--model", options.model] : []),
970
995
  "--prompt",
971
996
  "",
972
997
  "--approval-mode",
@@ -1,6 +1,6 @@
1
1
  export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js";
2
- export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
3
2
  export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
4
3
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
4
+ export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
5
5
  export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js";
6
6
  export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
@@ -1,6 +1,6 @@
1
1
  export { createDirectProviderAdapter } from "./direct-provider.js";
2
- export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
3
2
  export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
4
3
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
4
+ export { createVerifierOnlyAdapter } from "./verifier-only.js";
5
5
  export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
6
6
  export { createSpawnPlan } from "./cli-bridge.js";
@@ -14,7 +14,7 @@
14
14
  * Usage:
15
15
  * # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
16
16
  * MARTIN_OPENAI_API_KEY=sk-...
17
- * MARTIN_OPENAI_MODEL=gpt-4.1-mini
17
+ * MARTIN_OPENAI_MODEL=<provider-model-id>
18
18
  * martin-loop run "fix the bug" --engine openai
19
19
  *
20
20
  * # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
@@ -51,10 +51,9 @@ export interface OpenAiCompatibleAdapterOptions {
51
51
  fetchImpl?: typeof fetch;
52
52
  }
53
53
  export declare const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
54
- export declare const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
55
54
  export declare function resolveOpenAiCompatibleRuntimeConfig(env?: NodeJS.ProcessEnv): {
56
55
  baseUrl: string;
57
- model: string;
56
+ model?: string;
58
57
  apiKey: string;
59
58
  apiKeyConfigured: boolean;
60
59
  authPosture: "api_key" | "anonymous_or_local";
@@ -14,7 +14,7 @@
14
14
  * Usage:
15
15
  * # Defaults to OpenAI's hosted endpoint when MARTIN_OPENAI_BASE_URL is unset.
16
16
  * MARTIN_OPENAI_API_KEY=sk-...
17
- * MARTIN_OPENAI_MODEL=gpt-4.1-mini
17
+ * MARTIN_OPENAI_MODEL=<provider-model-id>
18
18
  * martin-loop run "fix the bug" --engine openai
19
19
  *
20
20
  * # Or route to a third-party / self-hosted OpenAI-compatible endpoint:
@@ -77,13 +77,27 @@ function normalizeOpenAiCompatibleUsage(input) {
77
77
  };
78
78
  const actualUsd = (input.tokensIn / 1000) * pricing.inputPer1K +
79
79
  (input.tokensOut / 1000) * pricing.outputPer1K;
80
- const provenance = input.usageWasFullyProviderReported && hasKnownPricing ? "actual" : "estimated";
80
+ const provenance = input.usageWasFullyProviderReported && hasKnownPricing ? "calculated" : "estimated";
81
81
  return normalizeUsage({
82
82
  actualUsd,
83
83
  ...(provenance === "estimated" ? { estimatedUsd: actualUsd } : {}),
84
84
  tokensIn: input.tokensIn,
85
85
  tokensOut: input.tokensOut,
86
- provenance
86
+ provenance,
87
+ providerSettlement: {
88
+ providerId: "openai-compatible",
89
+ model: input.model,
90
+ transport: "http",
91
+ source: input.usageWasFullyProviderReported ? "openai_compatible_json" : "estimated_fallback",
92
+ inputTokens: input.tokensIn,
93
+ outputTokens: input.tokensOut,
94
+ billingMode: input.billingMode,
95
+ modelSource: input.modelSource,
96
+ pricingSource: hasKnownPricing ? "static_catalog" : "blended_fallback",
97
+ pricingVersion: "embedded-v1",
98
+ rawUsageAvailable: input.usageWasFullyProviderReported,
99
+ settledAt: new Date().toISOString()
100
+ }
87
101
  });
88
102
  }
89
103
  // ---------------------------------------------------------------------------
@@ -97,12 +111,11 @@ Follow these rules exactly:
97
111
  - Be precise, minimal, and test-backed in all changes.
98
112
  - State what you changed and why at the end of your response.`;
99
113
  export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
100
- export const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
101
114
  export function resolveOpenAiCompatibleRuntimeConfig(env = process.env) {
102
115
  const apiKey = env["MARTIN_OPENAI_API_KEY"] ?? "";
103
116
  return {
104
117
  baseUrl: env["MARTIN_OPENAI_BASE_URL"] ?? DEFAULT_OPENAI_BASE_URL,
105
- model: env["MARTIN_OPENAI_MODEL"] ?? DEFAULT_OPENAI_MODEL,
118
+ ...(env["MARTIN_OPENAI_MODEL"] ? { model: env["MARTIN_OPENAI_MODEL"] } : {}),
106
119
  apiKey,
107
120
  apiKeyConfigured: apiKey.length > 0,
108
121
  authPosture: apiKey.length > 0 ? "api_key" : "anonymous_or_local"
@@ -143,7 +156,16 @@ export function createOpenAiCompatibleAdapter(options) {
143
156
  const runtimeConfig = resolveOpenAiCompatibleRuntimeConfig();
144
157
  const baseUrl = (options.baseUrl ?? runtimeConfig.baseUrl).replace(/\/$/, "");
145
158
  const model = options.model ?? runtimeConfig.model;
159
+ if (!model) {
160
+ throw new Error("MODEL_CONFIGURATION_REQUIRED: set --model or MARTIN_OPENAI_MODEL for direct OpenAI-compatible execution.");
161
+ }
146
162
  const apiKey = options.apiKey ?? runtimeConfig.apiKey;
163
+ const modelSource = options.model ? "explicit_override" : "provider_configured";
164
+ const billingMode = /^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|\[::1\])/iu.test(baseUrl)
165
+ ? "local_unmetered"
166
+ : apiKey
167
+ ? "metered_api"
168
+ : "unknown";
147
169
  return {
148
170
  adapterId: `openai-compatible:${model}`,
149
171
  kind: "direct-provider",
@@ -306,7 +328,9 @@ export function createOpenAiCompatibleAdapter(options) {
306
328
  model,
307
329
  tokensIn,
308
330
  tokensOut: 0,
309
- usageWasFullyProviderReported
331
+ usageWasFullyProviderReported,
332
+ modelSource,
333
+ billingMode
310
334
  }),
311
335
  verification: { passed: false, summary: "Empty response — nothing to verify." },
312
336
  failure: { message: "empty_response" }
@@ -332,7 +356,9 @@ export function createOpenAiCompatibleAdapter(options) {
332
356
  model,
333
357
  tokensIn,
334
358
  tokensOut,
335
- usageWasFullyProviderReported
359
+ usageWasFullyProviderReported,
360
+ modelSource,
361
+ billingMode
336
362
  }),
337
363
  verification,
338
364
  execution,
@@ -0,0 +1,9 @@
1
+ import type { MartinAdapter } from "../core/index.js";
2
+ import { type SpawnLike } from "./cli-bridge.js";
3
+ export interface VerifierOnlyAdapterOptions {
4
+ workingDirectory?: string;
5
+ verifyTimeoutMs?: number;
6
+ label?: string;
7
+ spawnImpl?: SpawnLike;
8
+ }
9
+ export declare function createVerifierOnlyAdapter(options?: VerifierOnlyAdapterOptions): MartinAdapter;
@@ -0,0 +1,68 @@
1
+ import { readGitChangedFiles, runVerification } from "./cli-bridge.js";
2
+ import { createAdapterCapabilities, normalizeUsage } from "./runtime-support.js";
3
+ export function createVerifierOnlyAdapter(options = {}) {
4
+ const workingDirectory = options.workingDirectory ?? process.cwd();
5
+ const verifyTimeoutMs = options.verifyTimeoutMs ?? 120_000;
6
+ return {
7
+ adapterId: "direct:verifier:verify-only",
8
+ kind: "direct-provider",
9
+ label: options.label ?? "Verifier-only adapter",
10
+ metadata: {
11
+ providerId: "verifier",
12
+ model: "verify-only",
13
+ transport: "cli",
14
+ capabilities: createAdapterCapabilities({
15
+ usageSettlement: true,
16
+ diffArtifacts: true,
17
+ workspaceMutations: false
18
+ })
19
+ },
20
+ async execute(request) {
21
+ const shouldTrackVerifierWrites = request.context.verificationPlan.length > 0 ||
22
+ (request.context.verificationStack?.length ?? 0) > 0;
23
+ const baselineChangedFiles = shouldTrackVerifierWrites
24
+ ? new Set(await readGitChangedFiles(workingDirectory, 5_000))
25
+ : new Set();
26
+ const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl, {
27
+ runId: request.loopId,
28
+ workspaceId: request.workspaceId,
29
+ cwd: workingDirectory
30
+ });
31
+ const changedFiles = shouldTrackVerifierWrites
32
+ ? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
33
+ : [];
34
+ const execution = { changedFiles };
35
+ if (verification.passed) {
36
+ return {
37
+ status: "completed",
38
+ summary: changedFiles.length > 0
39
+ ? `Verifier-only run completed but modified files: ${changedFiles.join(", ")}`
40
+ : "Verifier-only run completed without file edits.",
41
+ usage: normalizeUsage({
42
+ actualUsd: 0,
43
+ tokensIn: 0,
44
+ tokensOut: 0,
45
+ provenance: "actual"
46
+ }),
47
+ verification,
48
+ execution
49
+ };
50
+ }
51
+ return {
52
+ status: "failed",
53
+ summary: "Verifier-only run failed.",
54
+ usage: normalizeUsage({
55
+ actualUsd: 0,
56
+ tokensIn: 0,
57
+ tokensOut: 0,
58
+ provenance: "actual"
59
+ }),
60
+ verification,
61
+ execution,
62
+ failure: {
63
+ message: verification.summary
64
+ }
65
+ };
66
+ }
67
+ };
68
+ }
@@ -66,17 +66,27 @@ export interface LoopCost {
66
66
  estimatedUsd?: number;
67
67
  provenance?: CostProvenance;
68
68
  providerSettlement?: ProviderUsageSettlement;
69
+ savingsBaseline?: {
70
+ usd: number;
71
+ source: "measured_control" | "operator_supplied" | "fixture";
72
+ provenance: CostProvenance;
73
+ };
69
74
  }
70
- export type UsageSettlementSource = "claude_json" | "codex_jsonl" | "gemini_json" | "estimated_fallback" | "unavailable";
75
+ export type UsageSettlementSource = "claude_json" | "codex_jsonl" | "gemini_json" | "openai_compatible_json" | "estimated_fallback" | "unavailable";
71
76
  export interface ProviderUsageSettlement {
72
77
  providerId: string;
73
- model: string;
78
+ model?: string;
74
79
  transport?: "cli" | "http" | "routed_http";
75
80
  source: UsageSettlementSource;
76
81
  inputTokens: number;
77
82
  cachedInputTokens?: number;
78
83
  outputTokens: number;
79
84
  reasoningOutputTokens?: number;
85
+ cacheCreationInputTokens?: number;
86
+ billingMode?: "metered_api" | "subscription" | "local_unmetered" | "unknown";
87
+ modelSource?: "provider_reported" | "explicit_override" | "provider_configured" | "agent_default" | "unavailable";
88
+ pricingSource?: "provider_reported_total" | "static_catalog" | "blended_fallback" | "none";
89
+ pricingVersion?: string;
80
90
  rawUsageAvailable: boolean;
81
91
  settledAt: string;
82
92
  }
@@ -141,7 +151,7 @@ export interface LoopAttempt {
141
151
  attemptId: string;
142
152
  index: number;
143
153
  adapterId: string;
144
- model: string;
154
+ model?: string;
145
155
  startedAt: string;
146
156
  completedAt?: string;
147
157
  summary?: string;
@@ -335,7 +345,7 @@ export interface MachineState {
335
345
  * Cost provenance label — every budget metric must carry this.
336
346
  * Never conflate actual with estimated or unavailable.
337
347
  */
338
- export type CostProvenance = "actual" | "estimated" | "unavailable";
348
+ export type CostProvenance = "actual" | "calculated" | "estimated" | "unavailable";
339
349
  /**
340
350
  * Per-attempt budget preflight estimate produced before admission.
341
351
  * Used to gate attempts before any tokens are spent.
@@ -443,8 +453,8 @@ export { CONTEXT_SHADOW_MANIFEST_VERSION, CONTEXT_C5_VERSION } from "./context-s
443
453
  export type { ContextC5EnvelopeV1, ContextEvidence, ContextShadowDecisionV1, ContextShadowManifestV1, ContextShadowSegmentInput, ContextShadowSegmentKind } from "./context-shadow.js";
444
454
  export { CONTEXT_MANIFEST_VERSION, CONTEXT_LEDGER_VERSION } from "./context-manifest.js";
445
455
  export type { ContinuationCheckpoint, ContextBudget, ContextCandidateDecision, ContextFaultRequest, ContextFaultResult, ContextKind, ContextLedgerEntry, ContextManifest, ContextObject, ContextPolicy, ContextPriority, ContextSensitivity, ContextTrust, TaskItem, UsageEvidence } from "./context-manifest.js";
446
- export { EVIDENCE_STATUSES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
447
- export type { EvidenceStatus, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffCheckV1, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1, } from "./verified-handoff.js";
456
+ export { EVIDENCE_STATUSES, EXECUTION_MODES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
457
+ export type { EvidenceStatus, ExecutionMode, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffCheckV1, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1, } from "./verified-handoff.js";
448
458
  export { HANDOFF_SCHEMA_VERSION } from "./context-handoff.js";
449
459
  export type { ChainIntegrityState, ContextCircuitBreakResult, ContextExclusionDecision, ContextHandoffArtifact, ContextHandoffClaim, ContextHandoffReceipt, ContextHandoffVerification, HandoffClaimState } from "./context-handoff.js";
450
460
  export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
@@ -243,7 +243,7 @@ export { CONTEXT_SHADOW_MANIFEST_VERSION, CONTEXT_C5_VERSION } from "./context-s
243
243
  // ─── Context Runtime — A-CTX-1 ───────────────────────────────────────────────
244
244
  export { CONTEXT_MANIFEST_VERSION, CONTEXT_LEDGER_VERSION } from "./context-manifest.js";
245
245
  // ─── Track A — Verified Handoff ───────────────────────────────────────────────
246
- export { EVIDENCE_STATUSES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
246
+ export { EVIDENCE_STATUSES, EXECUTION_MODES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
247
247
  // ─── Context Handoff — A-CTX-2 ───────────────────────────────────────────────
248
248
  export { HANDOFF_SCHEMA_VERSION } from "./context-handoff.js";
249
249
  export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
@@ -1,6 +1,8 @@
1
1
  import type { CostProvenance, LoopLifecycleState, LoopStatus, ReceiptIntegritySummary } from "./index.js";
2
2
  export declare const VERIFIED_HANDOFF_OUTCOMES: readonly ["VERIFIED", "STOPPED", "NEEDS_REVIEW"];
3
3
  export type VerifiedHandoffOutcome = (typeof VERIFIED_HANDOFF_OUTCOMES)[number];
4
+ export declare const EXECUTION_MODES: readonly ["governed", "verification_only", "simulated"];
5
+ export type ExecutionMode = (typeof EXECUTION_MODES)[number];
4
6
  export declare const EVIDENCE_STATUSES: readonly ["PASSED", "FAILED", "CONTRADICTED", "NOT_RUN", "NOT_EVALUATED"];
5
7
  export type EvidenceStatus = (typeof EVIDENCE_STATUSES)[number];
6
8
  /**
@@ -71,6 +73,10 @@ export interface VerifiedHandoffV1 {
71
73
  verificationPlan: string[];
72
74
  };
73
75
  outcome: VerifiedHandoffOutcome;
76
+ /** Added compatibly in 1.0.0; missing legacy values must be treated as non-governed. */
77
+ executionMode?: ExecutionMode;
78
+ /** Only an explicit true value is eligible for a governed VERIFIED claim. */
79
+ governanceClaimEligible?: boolean;
74
80
  sourceStatus: {
75
81
  status: LoopStatus;
76
82
  lifecycleState: LoopLifecycleState;
@@ -5,6 +5,11 @@ export const VERIFIED_HANDOFF_OUTCOMES = [
5
5
  "STOPPED",
6
6
  "NEEDS_REVIEW",
7
7
  ];
8
+ export const EXECUTION_MODES = [
9
+ "governed",
10
+ "verification_only",
11
+ "simulated",
12
+ ];
8
13
  export const EVIDENCE_STATUSES = [
9
14
  "PASSED",
10
15
  "FAILED",
@@ -9,6 +9,7 @@ export interface RepoGroundingIndex {
9
9
  createdAt: string;
10
10
  fileCount: number;
11
11
  files: RepoGroundingFile[];
12
+ repositoryFingerprint?: string;
12
13
  }
13
14
  export interface RepoGroundingHit {
14
15
  path: string;
@@ -18,7 +19,7 @@ export interface RepoGroundingHit {
18
19
  }
19
20
  export declare function resolveGroundingRoot(env?: NodeJS.ProcessEnv): string;
20
21
  export declare function loadOrBuildRepoGroundingIndex(repoRoot: string): Promise<RepoGroundingIndex>;
21
- export declare function buildRepoGroundingIndex(repoRoot: string): Promise<RepoGroundingIndex>;
22
+ export declare function buildRepoGroundingIndex(repoRoot: string, repositoryFingerprint?: string): Promise<RepoGroundingIndex>;
22
23
  export declare function queryRepoGroundingIndex(index: RepoGroundingIndex, query: string, limit?: number): RepoGroundingHit[];
23
24
  export type GroundingViolationKind = "file_not_found" | "symbol_not_found" | "import_not_found" | "patch_outside_allowed_paths";
24
25
  export interface GroundingViolation {
@@ -1,4 +1,5 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import { extname, join, relative } from "node:path";
4
5
  const TEXT_EXTENSIONS = new Set([
@@ -18,14 +19,16 @@ export function resolveGroundingRoot(env = process.env) {
18
19
  }
19
20
  export async function loadOrBuildRepoGroundingIndex(repoRoot) {
20
21
  const cachePath = getGroundingCachePath(repoRoot);
22
+ const repositoryFingerprint = await computeRepositoryFingerprint(repoRoot);
21
23
  try {
22
24
  const cached = JSON.parse(await readFile(cachePath, "utf8"));
23
- if (cached?.schemaVersion === "martin.grounding.v1") {
25
+ if (cached?.schemaVersion === "martin.grounding.v1" &&
26
+ cached.repositoryFingerprint === repositoryFingerprint) {
24
27
  return cached;
25
28
  }
26
29
  }
27
30
  catch { }
28
- const index = await buildRepoGroundingIndex(repoRoot);
31
+ const index = await buildRepoGroundingIndex(repoRoot, repositoryFingerprint);
29
32
  try {
30
33
  await mkdir(resolveGroundingRoot(), { recursive: true });
31
34
  await writeFile(cachePath, JSON.stringify(index, null, 2), "utf8");
@@ -36,7 +39,7 @@ export async function loadOrBuildRepoGroundingIndex(repoRoot) {
36
39
  }
37
40
  return index;
38
41
  }
39
- export async function buildRepoGroundingIndex(repoRoot) {
42
+ export async function buildRepoGroundingIndex(repoRoot, repositoryFingerprint) {
40
43
  const files = [];
41
44
  const discovered = await walk(repoRoot, repoRoot, files, { count: 0 });
42
45
  return {
@@ -44,9 +47,41 @@ export async function buildRepoGroundingIndex(repoRoot) {
44
47
  repoRoot,
45
48
  createdAt: new Date().toISOString(),
46
49
  fileCount: discovered.count,
47
- files
50
+ files,
51
+ repositoryFingerprint: repositoryFingerprint ?? await computeRepositoryFingerprint(repoRoot)
48
52
  };
49
53
  }
54
+ async function computeRepositoryFingerprint(repoRoot) {
55
+ const entries = [];
56
+ await collectFingerprintEntries(repoRoot, repoRoot, entries, { count: 0 });
57
+ entries.sort();
58
+ return createHash("sha256").update(entries.join("\n")).digest("hex");
59
+ }
60
+ async function collectFingerprintEntries(repoRoot, currentDir, entries, state) {
61
+ if (state.count >= MAX_FILES)
62
+ return;
63
+ const directoryEntries = await readdir(currentDir, { withFileTypes: true });
64
+ for (const entry of directoryEntries) {
65
+ if (state.count >= MAX_FILES)
66
+ break;
67
+ const absPath = join(currentDir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ if (!IGNORED_DIRS.has(entry.name)) {
70
+ await collectFingerprintEntries(repoRoot, absPath, entries, state);
71
+ }
72
+ continue;
73
+ }
74
+ if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
75
+ continue;
76
+ try {
77
+ const metadata = await stat(absPath);
78
+ const relPath = relative(repoRoot, absPath).replace(/\\/g, "/");
79
+ entries.push(`${relPath}:${metadata.size}:${metadata.mtimeMs}`);
80
+ state.count += 1;
81
+ }
82
+ catch { }
83
+ }
84
+ }
50
85
  export function queryRepoGroundingIndex(index, query, limit = 6) {
51
86
  const terms = tokenize(query);
52
87
  if (terms.length === 0)