@martinloop/mcp 0.5.0 → 0.5.3

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 (51) hide show
  1. package/README.md +52 -14
  2. package/dist/resources.js +15 -3
  3. package/dist/server.js +40 -8
  4. package/dist/tools/doctor.js +5 -4
  5. package/dist/tools/get-status.d.ts +2 -0
  6. package/dist/tools/get-status.js +1 -0
  7. package/dist/tools/preflight.js +1 -1
  8. package/dist/tools/run-loop.d.ts +6 -1
  9. package/dist/tools/run-loop.js +22 -7
  10. package/dist/tools/tool-errors.js +1 -1
  11. package/dist/tools/tool-response.js +9 -2
  12. package/dist/tools/tool-support.d.ts +3 -2
  13. package/dist/tools/tool-support.js +5 -3
  14. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  15. package/dist/vendor/adapters/claude-cli.js +59 -34
  16. package/dist/vendor/adapters/codex-capabilities.d.ts +60 -0
  17. package/dist/vendor/adapters/codex-capabilities.js +237 -0
  18. package/dist/vendor/adapters/codex-cli.d.ts +22 -0
  19. package/dist/vendor/adapters/codex-cli.js +62 -0
  20. package/dist/vendor/adapters/codex-host.d.ts +104 -0
  21. package/dist/vendor/adapters/codex-host.js +577 -0
  22. package/dist/vendor/adapters/codex-launcher.d.ts +7 -135
  23. package/dist/vendor/adapters/codex-launcher.js +17 -766
  24. package/dist/vendor/adapters/index.d.ts +4 -3
  25. package/dist/vendor/adapters/index.js +4 -3
  26. package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
  27. package/dist/vendor/adapters/openai-compatible.js +33 -7
  28. package/dist/vendor/adapters/verifier-only.d.ts +9 -0
  29. package/dist/vendor/adapters/verifier-only.js +68 -0
  30. package/dist/vendor/contracts/index.d.ts +16 -6
  31. package/dist/vendor/contracts/index.js +1 -1
  32. package/dist/vendor/contracts/verified-handoff.d.ts +6 -0
  33. package/dist/vendor/contracts/verified-handoff.js +5 -0
  34. package/dist/vendor/core/grounding.d.ts +2 -1
  35. package/dist/vendor/core/grounding.js +40 -5
  36. package/dist/vendor/core/index.d.ts +4 -6
  37. package/dist/vendor/core/index.js +25 -19
  38. package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
  39. package/dist/vendor/core/policy.js +6 -6
  40. package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
  41. package/dist/vendor/core/red-blue/red-phase.js +3 -3
  42. package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
  43. package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
  44. package/dist/vendor/core/routing.d.ts +0 -42
  45. package/dist/vendor/core/routing.js +4 -154
  46. package/dist/vendor/core/verified-handoff.d.ts +6 -1
  47. package/dist/vendor/core/verified-handoff.js +28 -0
  48. package/package.json +6 -2
  49. package/server.json +2 -2
  50. package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
  51. package/dist/vendor/adapters/stub-direct-provider.js +0 -12
@@ -1,6 +1,7 @@
1
1
  export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js";
2
- export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
3
- export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
2
+ export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js";
3
+ export { createCodexCliAdapter, type CodexCliAdapterOptions } from "./codex-cli.js";
4
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
5
- export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js";
5
+ export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
6
+ export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexApprovalCapability, type CodexCapabilityFlag, type CodexCapabilityProfile, type CodexFlagScope, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, type CodexPromptTransport, type CodexSandboxCapability, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js";
6
7
  export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
@@ -1,6 +1,7 @@
1
1
  export { createDirectProviderAdapter } from "./direct-provider.js";
2
- export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
3
- export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
2
+ export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter } from "./claude-cli.js";
3
+ export { createCodexCliAdapter } from "./codex-cli.js";
4
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
5
- export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
5
+ export { createVerifierOnlyAdapter } from "./verifier-only.js";
6
+ export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
6
7
  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)
@@ -19,8 +19,8 @@ export { compileExecutionPolicy } from "./policy-compiler.js";
19
19
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
20
20
  export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
21
21
  export type { AvoidedUsdInput } from "./savings.js";
22
- export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
23
- export type { RouteDecision, RouteClassificationInput, AvailableEngine } from "./routing.js";
22
+ export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
23
+ export type { RouteDecision, RouteClassificationInput } from "./routing.js";
24
24
  export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
25
25
  export type { TestIntegrityReport, TestIntegrityFinding } from "./test-integrity.js";
26
26
  export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
@@ -29,7 +29,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
29
29
  export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
30
30
  export { compilePromptPacket } from "./compiler.js";
31
31
  export type { PromptPacket, CompilerAdapterRequest } from "./compiler.js";
32
- export { buildVerifiedHandoff, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
32
+ export { buildVerifiedHandoff, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
33
33
  export type { BoundVerifierEvidence, BuildVerifiedHandoffInput, VerifierExecutionBinding, } from "./verified-handoff.js";
34
34
  export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
35
35
  export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
@@ -142,7 +142,7 @@ export interface MartinAdapter {
142
142
  label: string;
143
143
  metadata: {
144
144
  providerId: string;
145
- model: string;
145
+ model?: string;
146
146
  transport?: "cli" | "http" | "routed_http";
147
147
  capabilities?: {
148
148
  preflight?: boolean;
@@ -155,7 +155,6 @@ export interface MartinAdapter {
155
155
  [key: string]: unknown;
156
156
  };
157
157
  execute(request: MartinAdapterRequest): Promise<MartinAdapterResult>;
158
- withModel?(model: string): MartinAdapter;
159
158
  }
160
159
  export interface DistilledContext {
161
160
  focus: string;
@@ -193,7 +192,6 @@ export interface RunMartinInput {
193
192
  now?: () => string;
194
193
  idFactory?: (prefix: string) => string;
195
194
  maxRecentAttempts?: number;
196
- fallbackModels?: string[];
197
195
  fallbackAdapters?: MartinAdapter[];
198
196
  receiptScope?: ReceiptScope;
199
197
  /** Optional persistence store. When provided, runMartin writes artifacts on each lifecycle event. */
@@ -22,7 +22,7 @@ export { compileExecutionPolicy } from "./policy-compiler.js";
22
22
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
23
23
  export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
24
24
  // ─── Routing economics ─────────────────────────────────────────────────────
25
- export { classifyRoute, evaluatePreworkBurnPolicy, resolveModelForTier, selectBestEngine } from "./routing.js";
25
+ export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
26
26
  // ─── Test Integrity Validation ──────────────────────────────────────────────
27
27
  export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
28
28
  // ─── Repo Style Analyzer ────────────────────────────────────────────────────
@@ -32,7 +32,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
32
32
  // ─── Prompt packet compiler ──────────────────────────────────────────────────
33
33
  export { compilePromptPacket } from "./compiler.js";
34
34
  // ─── Verified Handoff builder ────────────────────────────────────────────────
35
- export { buildVerifiedHandoff, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
35
+ export { buildVerifiedHandoff, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
36
36
  // ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
37
37
  export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
38
38
  export { compileAndPersistContext } from "./persistence/index.js";
@@ -265,11 +265,6 @@ export async function runMartin(input) {
265
265
  }));
266
266
  }
267
267
  }
268
- const DEFAULT_FALLBACK_MODELS = [
269
- "claude-haiku-4-5",
270
- "claude-sonnet-4-6",
271
- "claude-opus-4-6"
272
- ];
273
268
  const adapterChain = [input.adapter, ...(input.fallbackAdapters ?? [])];
274
269
  let currentAdapterIndex = 0;
275
270
  let currentAdapter = adapterChain[currentAdapterIndex] ?? input.adapter;
@@ -803,7 +798,7 @@ export async function runMartin(input) {
803
798
  attemptId,
804
799
  index: currentAttemptIndex,
805
800
  adapterId: executingAdapter.adapterId,
806
- model: executingAdapter.metadata.model,
801
+ ...(executingAdapter.metadata.model ? { model: executingAdapter.metadata.model } : {}),
807
802
  startedAt: attemptStartedAt,
808
803
  completedAt: attemptCompletedAt,
809
804
  summary: result.summary,
@@ -864,13 +859,6 @@ export async function runMartin(input) {
864
859
  adapterSwitched = true;
865
860
  }
866
861
  }
867
- if (failure.recommendedIntervention === "change_model" && currentAdapter.withModel) {
868
- const fallbackModels = input.fallbackModels ?? DEFAULT_FALLBACK_MODELS;
869
- const nextModel = fallbackModels[loop.attempts.length % fallbackModels.length];
870
- if (nextModel) {
871
- currentAdapter = currentAdapter.withModel(nextModel);
872
- }
873
- }
874
862
  loop = appendLoopEvent(loop, {
875
863
  type: "failure.classified",
876
864
  lifecycleState: "running",
@@ -1138,12 +1126,17 @@ export async function runMartin(input) {
1138
1126
  // Uses the task's repoRoot to build/load the grounding index, then scans any diff
1139
1127
  let groundingScanResult;
1140
1128
  const patchDiff = buildPatchDiff(result, changedFiles);
1141
- if (patchDiff && input.task.repoRoot) {
1129
+ const workspaceMutationCapable = executingAdapter.metadata.capabilities?.workspaceMutations !== false;
1130
+ const hasReportedOrObservedChanges = changedFiles.length > 0 || (result.execution?.changedFiles?.length ?? 0) > 0;
1131
+ const groundingEvidenceRequired = result.status === "completed" && workspaceMutationCapable && hasReportedOrObservedChanges;
1132
+ let groundingEvidenceAvailable = !groundingEvidenceRequired;
1133
+ if (groundingEvidenceRequired && patchDiff && input.task.repoRoot) {
1142
1134
  try {
1143
1135
  const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
1144
1136
  groundingScanResult = reconcileExistingGroundingFiles(scanPatchForGroundingViolations(patchDiff, groundingIndex, {
1145
1137
  allowedPaths: input.task.allowedPaths
1146
1138
  }), input.task.repoRoot, rollbackBoundary);
1139
+ groundingEvidenceAvailable = true;
1147
1140
  if (input.store && groundingScanResult.violations.length > 0) {
1148
1141
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1149
1142
  kind: "grounding.violations_found",
@@ -1159,10 +1152,22 @@ export async function runMartin(input) {
1159
1152
  }
1160
1153
  }
1161
1154
  catch {
1162
- // Grounding scan is best-effort — never fail the loop because of a scan error
1155
+ // Availability is persisted below so scanner failures and missing
1156
+ // grounding inputs follow the same recover-forward trust contract.
1163
1157
  }
1164
1158
  }
1165
- const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations;
1159
+ if (groundingEvidenceRequired && !groundingEvidenceAvailable) {
1160
+ // The completed work remains useful, but unavailable grounding evidence
1161
+ // cannot be converted into a zero-violation governance claim.
1162
+ loop = {
1163
+ ...loop,
1164
+ metadata: {
1165
+ ...loop.metadata,
1166
+ groundingEvidenceStatus: "unavailable"
1167
+ }
1168
+ };
1169
+ }
1170
+ const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations && groundingEvidenceAvailable;
1166
1171
  let patchDecision;
1167
1172
  if (shouldEvaluatePatchTruth) {
1168
1173
  patchDecision = evaluatePatchDecision({
@@ -1548,7 +1553,8 @@ function getUsageProvenance(usage) {
1548
1553
  const COST_PROVENANCE_RANK = {
1549
1554
  unavailable: 0,
1550
1555
  estimated: 1,
1551
- actual: 2
1556
+ calculated: 2,
1557
+ actual: 3
1552
1558
  };
1553
1559
  /**
1554
1560
  * Aggregates cost provenance across attempts. The cumulative loop provenance
@@ -6,6 +6,7 @@
6
6
  * - legacy root JSONL files: <runsRoot>/*.jsonl
7
7
  * - canonical run trees: <runsRoot>/<loopId>/loop-record.json
8
8
  */
9
+ import type { CostProvenance } from "../../contracts/index.js";
9
10
  export interface LoopAttemptRecord {
10
11
  index: number;
11
12
  model?: string;
@@ -32,6 +33,7 @@ export interface LoopRunRecord {
32
33
  tokensIn: number;
33
34
  tokensOut: number;
34
35
  avoidedUsd?: number;
36
+ provenance?: CostProvenance;
35
37
  };
36
38
  attempts: LoopAttemptRecord[];
37
39
  task: {
@@ -152,14 +152,14 @@ export function classifyFailure(input) {
152
152
  failureClass: "logic_error",
153
153
  rationale: "Oscillating failure pattern detected — switching strategy to break the cycle.",
154
154
  retryable: true,
155
- recommendedIntervention: "change_model"
155
+ recommendedIntervention: "compress_context"
156
156
  };
157
157
  }
158
158
  return {
159
159
  failureClass: repeatedFailure ?? "logic_error",
160
160
  rationale: "The loop is still failing to produce a correct implementation.",
161
161
  retryable: true,
162
- recommendedIntervention: repeatedFailure === "logic_error" ? "change_model" : "compress_context"
162
+ recommendedIntervention: "compress_context"
163
163
  };
164
164
  }
165
165
  export function evaluateCostGovernor(input) {
@@ -318,7 +318,7 @@ function mapClassHintToAssessment(classHint, attempts) {
318
318
  failureClass: "hallucination",
319
319
  rationale: "Structural evidence: suspiciously short or trivial agent response.",
320
320
  retryable: true,
321
- recommendedIntervention: repeatedFailure === "hallucination" ? "change_model" : "run_verifier"
321
+ recommendedIntervention: repeatedFailure === "hallucination" ? "compress_context" : "run_verifier"
322
322
  };
323
323
  case "verification_failure":
324
324
  case "test_regression":
@@ -333,7 +333,7 @@ function mapClassHintToAssessment(classHint, attempts) {
333
333
  failureClass: classHint,
334
334
  rationale: "Structural hint provided by adapter.",
335
335
  retryable: true,
336
- recommendedIntervention: repeatedFailure ? "change_model" : "compress_context"
336
+ recommendedIntervention: "compress_context"
337
337
  };
338
338
  }
339
339
  }
@@ -472,7 +472,7 @@ export function selectRecoveryRecipe(evidence) {
472
472
  return {
473
473
  recipe: "strategy_swap",
474
474
  rationale: "Very low diff novelty across repeated retries. Swap strategy.",
475
- intervention: "change_model"
475
+ intervention: "compress_context"
476
476
  };
477
477
  }
478
478
  return {
@@ -485,7 +485,7 @@ export function selectRecoveryRecipe(evidence) {
485
485
  return {
486
486
  recipe: "downgrade_model",
487
487
  rationale: "Cost efficiency is degrading or retries are exhausted. Downgrade the model.",
488
- intervention: "change_model"
488
+ intervention: "compress_context"
489
489
  };
490
490
  }
491
491
  return {