@martinloop/mcp 0.8.8 → 0.8.9

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.
@@ -1 +1 @@
1
- export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.8";
1
+ export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.9";
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by build — do not edit by hand.
2
2
  // Source of truth: packages/mcp/package.json#version
3
- export const MARTIN_MCP_PACKAGE_VERSION = "0.8.8";
3
+ export const MARTIN_MCP_PACKAGE_VERSION = "0.8.9";
@@ -145,6 +145,7 @@ function validateRunInput(args) {
145
145
  "workingDirectory",
146
146
  "engine",
147
147
  "model",
148
+ "fallbackEngines",
148
149
  "context",
149
150
  "policyPack",
150
151
  "maxUsd",
@@ -169,6 +170,7 @@ function validateRunInput(args) {
169
170
  : {}),
170
171
  ...(engine ? { engine } : {}),
171
172
  ...optionalString(record.model, "model"),
173
+ ...optionalEnumArrayAsObject(record.fallbackEngines, "fallbackEngines", MARTIN_ENGINE_VALUES),
172
174
  ...optionalString(record.context, "context"),
173
175
  ...optionalEnumAsObject(record.policyPack, "policyPack", [
174
176
  "solo-founder",
@@ -643,3 +645,19 @@ function optionalEnumAsObject(value, name, allowed) {
643
645
  const enumValue = optionalEnum(value, name, allowed);
644
646
  return enumValue ? { [name]: enumValue } : {};
645
647
  }
648
+ function optionalEnumArrayAsObject(value, name, allowed) {
649
+ if (value === undefined)
650
+ return {};
651
+ if (!Array.isArray(value))
652
+ throw invalidArgumentsError(`Invalid ${name}.`);
653
+ const values = value.map((item) => {
654
+ if (typeof item !== "string" || !allowed.includes(item)) {
655
+ throw invalidArgumentsError(`Invalid ${name}.`);
656
+ }
657
+ return item;
658
+ });
659
+ if (new Set(values).size !== values.length) {
660
+ throw invalidArgumentsError(`Invalid ${name}: duplicate engines are not allowed.`);
661
+ }
662
+ return { [name]: values };
663
+ }
package/dist/server.js CHANGED
@@ -935,7 +935,16 @@ export function createMartinMcpServer(serverInfo) {
935
935
  },
936
936
  model: {
937
937
  type: "string",
938
- description: "Optional model override passed to the CLI."
938
+ description: "Optional model override passed to the primary provider."
939
+ },
940
+ fallbackEngines: {
941
+ type: "array",
942
+ uniqueItems: true,
943
+ items: {
944
+ type: "string",
945
+ enum: [...MARTIN_ENGINE_VALUES]
946
+ },
947
+ description: "Explicit ordered provider fallbacks authorized for this run. No provider is added implicitly."
939
948
  },
940
949
  maxUsd: {
941
950
  type: "number",
@@ -7,6 +7,8 @@ export interface RunLoopInput {
7
7
  workingDirectory?: string;
8
8
  engine?: MartinEngine;
9
9
  model?: string;
10
+ /** Explicit ordered provider fallbacks authorized by the caller. */
11
+ fallbackEngines?: MartinEngine[];
10
12
  maxUsd?: number;
11
13
  maxIterations?: number;
12
14
  maxTokens?: number;
@@ -1,4 +1,4 @@
1
- import { createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, probeCodexLaunch, resolveCliCommandAvailability, createVerifierOnlyAdapter } from "../vendor/adapters/index.js";
1
+ import { createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, probeCodexLaunch, resolveCliCommandAvailability, createVerifierOnlyAdapter } 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";
@@ -12,10 +12,131 @@ export function __setProofModeVerifierSpawnImplForTests(spawnImpl) {
12
12
  export function __setRunStoreOverrideForTests(store) {
13
13
  runStoreOverrideForTests = store;
14
14
  }
15
+ function assertAuthorizedFallbackEngines(primary, fallbacks) {
16
+ const seen = new Set();
17
+ for (const fallback of fallbacks) {
18
+ if (fallback === primary) {
19
+ throw new MartinToolError("invalid_arguments", "Fallback chain cannot repeat the primary engine.", {
20
+ category: "invalid_input",
21
+ retryable: false
22
+ });
23
+ }
24
+ if (seen.has(fallback)) {
25
+ throw new MartinToolError("invalid_arguments", `Duplicate fallback engine '${fallback}'.`, {
26
+ category: "invalid_input",
27
+ retryable: false
28
+ });
29
+ }
30
+ seen.add(fallback);
31
+ }
32
+ }
33
+ function ensureLiveEngineReady(engine, workingDirectory, providerExecutionTimeoutMs) {
34
+ if (engine === "openai") {
35
+ const config = resolveOpenAiCompatibleRuntimeConfig();
36
+ const localEndpoint = /^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1|\[::1\])(?::|\/|$)/iu.test(config.baseUrl);
37
+ if (!config.model) {
38
+ throw new MartinToolError("engine_unavailable", "Authorized OpenAI-compatible fallback has no configured model.", {
39
+ category: "environment",
40
+ suggestion: "Set MARTIN_OPENAI_MODEL before authorizing the OpenAI fallback.",
41
+ retryable: false
42
+ });
43
+ }
44
+ if (!config.apiKeyConfigured && !localEndpoint) {
45
+ throw new MartinToolError("engine_unavailable", "Authorized OpenAI-compatible fallback is not authenticated.", {
46
+ category: "environment",
47
+ suggestion: "Set MARTIN_OPENAI_API_KEY, or use an explicitly local OpenAI-compatible endpoint.",
48
+ retryable: false
49
+ });
50
+ }
51
+ return undefined;
52
+ }
53
+ if (engine === "codex") {
54
+ const availability = resolveCliCommandAvailability("codex");
55
+ if (!availability.available) {
56
+ throw new MartinToolError("engine_unavailable", "Engine 'codex' is not available on PATH.", {
57
+ category: "environment",
58
+ suggestion: "Install/authenticate Codex before authorizing it for governed execution.",
59
+ retryable: false
60
+ });
61
+ }
62
+ const probe = probeCodexLaunch({
63
+ workingDirectory,
64
+ availability,
65
+ ...(providerExecutionTimeoutMs !== undefined ? { providerExecutionTimeoutMs } : {})
66
+ });
67
+ if (!probe.ok) {
68
+ throw new MartinToolError("engine_unavailable", probe.summary, {
69
+ category: "environment",
70
+ suggestion: "Run martin_doctor or martin_preflight with engine='codex' before retrying.",
71
+ retryable: false
72
+ });
73
+ }
74
+ return probe;
75
+ }
76
+ const availability = getEngineAvailability(engine);
77
+ if (!availability.available) {
78
+ throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
79
+ category: "environment",
80
+ suggestion: "Install/authenticate every provider explicitly listed in the fallback chain.",
81
+ retryable: false
82
+ });
83
+ }
84
+ return undefined;
85
+ }
86
+ function createLiveEngineAdapter(input) {
87
+ if (input.engine === "codex") {
88
+ return createCodexCliAdapter({
89
+ workingDirectory: input.workingDirectory,
90
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
91
+ ...(input.providerExecutionTimeoutMs !== undefined
92
+ ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
93
+ : {}),
94
+ ...(input.model ? { model: input.model } : {}),
95
+ ...(input.codexProbe
96
+ ? {
97
+ command: input.codexProbe.command,
98
+ capabilityProfile: input.codexProbe.capabilityProfile,
99
+ autonomyResolution: input.codexProbe.autonomyResolution
100
+ }
101
+ : {})
102
+ });
103
+ }
104
+ if (input.engine === "gemini") {
105
+ return createGeminiCliAdapter({
106
+ workingDirectory: input.workingDirectory,
107
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
108
+ ...(input.providerExecutionTimeoutMs !== undefined
109
+ ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
110
+ : {}),
111
+ ...(input.model ? { model: input.model } : {})
112
+ });
113
+ }
114
+ if (input.engine === "openai") {
115
+ const config = resolveOpenAiCompatibleRuntimeConfig();
116
+ const resolvedModel = input.model ?? config.model;
117
+ return createOpenAiCompatibleAdapter({
118
+ baseUrl: config.baseUrl,
119
+ apiKey: config.apiKey,
120
+ ...(resolvedModel ? { model: resolvedModel } : {}),
121
+ workingDirectory: input.workingDirectory,
122
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {})
123
+ });
124
+ }
125
+ return createClaudeCliAdapter({
126
+ workingDirectory: input.workingDirectory,
127
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
128
+ ...(input.providerExecutionTimeoutMs !== undefined
129
+ ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
130
+ : {}),
131
+ ...(input.model ? { model: input.model } : {})
132
+ });
133
+ }
15
134
  export async function runLoopTool(input) {
16
135
  const workingDirectory = resolveSafeRepoRoot(input.workingDirectory);
17
136
  const engine = input.engine ?? "claude";
18
137
  const model = input.model;
138
+ const fallbackEngines = input.fallbackEngines ?? [];
139
+ assertAuthorizedFallbackEngines(engine, fallbackEngines);
19
140
  const allowedPaths = normalizeSafePathPatterns(input.allowedPaths, "allowedPaths");
20
141
  const deniedPaths = normalizeSafePathPatterns(input.deniedPaths, "deniedPaths");
21
142
  const executionMode = resolveExecutionMode();
@@ -31,41 +152,11 @@ export async function runLoopTool(input) {
31
152
  runsRoot
32
153
  };
33
154
  let codexProbeOverride;
155
+ const fallbackProbeOverrides = new Map();
34
156
  if (executionMode.liveMode) {
35
- if (engine === "codex") {
36
- const engineAvailability = resolveCliCommandAvailability("codex");
37
- if (!engineAvailability.available) {
38
- throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
39
- category: "environment",
40
- suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
41
- retryable: false
42
- });
43
- }
44
- const codexProbe = probeCodexLaunch({
45
- workingDirectory,
46
- availability: engineAvailability,
47
- ...(input.providerExecutionTimeoutMs !== undefined
48
- ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
49
- : {})
50
- });
51
- if (!codexProbe.ok) {
52
- throw new MartinToolError("engine_unavailable", codexProbe.summary, {
53
- category: "environment",
54
- suggestion: "Run martin_doctor or martin_preflight with engine='codex' before retrying live governed work.",
55
- retryable: false
56
- });
57
- }
58
- codexProbeOverride = codexProbe;
59
- }
60
- else {
61
- const engineAvailability = getEngineAvailability(engine);
62
- if (!engineAvailability.available) {
63
- throw new MartinToolError("engine_unavailable", `Engine '${engine}' is not available on PATH.`, {
64
- category: "environment",
65
- suggestion: "Install the requested CLI. MARTIN_LIVE=false is verification-only and cannot emit governed VERIFIED.",
66
- retryable: false
67
- });
68
- }
157
+ codexProbeOverride = ensureLiveEngineReady(engine, workingDirectory, input.providerExecutionTimeoutMs);
158
+ for (const fallbackEngine of fallbackEngines) {
159
+ fallbackProbeOverrides.set(fallbackEngine, ensureLiveEngineReady(fallbackEngine, workingDirectory, input.providerExecutionTimeoutMs));
69
160
  }
70
161
  }
71
162
  const adapter = !executionMode.liveMode
@@ -75,39 +166,35 @@ export async function runLoopTool(input) {
75
166
  ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
76
167
  ...(proofModeVerifierSpawnImpl ? { spawnImpl: proofModeVerifierSpawnImpl } : {})
77
168
  })
78
- : engine === "codex"
79
- ? createCodexCliAdapter({
80
- workingDirectory,
81
- ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
82
- ...(input.providerExecutionTimeoutMs !== undefined
83
- ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
84
- : {}),
85
- ...(model ? { model } : {}),
86
- ...(codexProbeOverride
87
- ? {
88
- command: codexProbeOverride.command,
89
- capabilityProfile: codexProbeOverride.capabilityProfile,
90
- autonomyResolution: codexProbeOverride.autonomyResolution
91
- }
92
- : {})
93
- })
94
- : engine === "gemini"
95
- ? createGeminiCliAdapter({
96
- workingDirectory,
97
- ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
98
- ...(input.providerExecutionTimeoutMs !== undefined
99
- ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
100
- : {}),
101
- ...(model ? { model } : {})
102
- })
103
- : createClaudeCliAdapter({
104
- workingDirectory,
105
- ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
106
- ...(input.providerExecutionTimeoutMs !== undefined
107
- ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
108
- : {}),
109
- ...(model ? { model } : {})
110
- });
169
+ : createLiveEngineAdapter({
170
+ engine,
171
+ workingDirectory,
172
+ ...(model ? { model } : {}),
173
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
174
+ ...(input.providerExecutionTimeoutMs !== undefined
175
+ ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
176
+ : {}),
177
+ ...(codexProbeOverride ? { codexProbe: codexProbeOverride } : {})
178
+ });
179
+ if (!executionMode.liveMode && fallbackEngines.length > 0) {
180
+ throw new MartinToolError("invalid_arguments", "fallbackEngines are available only in live governed mode.", {
181
+ category: "invalid_input",
182
+ retryable: false
183
+ });
184
+ }
185
+ const fallbackAdapters = executionMode.liveMode
186
+ ? fallbackEngines.map((fallbackEngine) => createLiveEngineAdapter({
187
+ engine: fallbackEngine,
188
+ workingDirectory,
189
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
190
+ ...(input.providerExecutionTimeoutMs !== undefined
191
+ ? { providerExecutionTimeoutMs: input.providerExecutionTimeoutMs }
192
+ : {}),
193
+ ...(fallbackProbeOverrides.get(fallbackEngine)
194
+ ? { codexProbe: fallbackProbeOverrides.get(fallbackEngine) }
195
+ : {})
196
+ }))
197
+ : [];
111
198
  const partialBudget = {};
112
199
  if (input.maxUsd !== undefined) {
113
200
  partialBudget.maxUsd = input.maxUsd;
@@ -141,9 +228,14 @@ export async function runLoopTool(input) {
141
228
  budget,
142
229
  metadata: {
143
230
  executionMode: evidenceExecutionMode,
144
- governanceClaimEligible: evidenceExecutionMode === "governed" ? "true" : "false"
231
+ governanceClaimEligible: evidenceExecutionMode === "governed" ? "true" : "false",
232
+ ...(executionMode.liveMode ? { authorizedPrimaryEngine: engine } : {}),
233
+ ...(executionMode.liveMode && fallbackEngines.length > 0
234
+ ? { authorizedFallbackEngines: fallbackEngines.join(",") }
235
+ : {})
145
236
  },
146
- adapter
237
+ adapter,
238
+ ...(fallbackAdapters.length > 0 ? { fallbackAdapters } : {})
147
239
  });
148
240
  const lastAttempt = result.loop.attempts.at(-1);
149
241
  // D5: verificationPassed must reflect authoritative verifier execution/binding,
@@ -153,6 +245,12 @@ export async function runLoopTool(input) {
153
245
  const verificationPassed = lastAttempt !== undefined &&
154
246
  result.decision.lifecycleState === "completed" &&
155
247
  (lastAttempt.verifierScore ?? 0) >= 1;
248
+ const fallbackAdapterIndex = lastAttempt
249
+ ? fallbackAdapters.findIndex((candidate) => candidate.adapterId === lastAttempt.adapterId)
250
+ : -1;
251
+ const effectiveEngine = fallbackAdapterIndex >= 0
252
+ ? (fallbackEngines[fallbackAdapterIndex] ?? engine)
253
+ : engine;
156
254
  const costState = evaluateCostGovernor({
157
255
  budget: result.loop.budget,
158
256
  cost: {
@@ -183,7 +281,7 @@ export async function runLoopTool(input) {
183
281
  ...(costState.remainingTokens !== undefined
184
282
  ? { remainingTokens: costState.remainingTokens }
185
283
  : {}),
186
- engine,
284
+ engine: effectiveEngine,
187
285
  workingDirectory,
188
286
  budget,
189
287
  inspection: {
@@ -1,4 +1,4 @@
1
- import { type ApprovalPolicy, type AgentExecutionIntent, type ContextHandoffReceipt, type CostProvenance, type ExecutionProfile, type ExternalOutcomeEvidenceReference, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type ProviderUsageSettlement, type MutationMode, type LoopRecord, type LoopTask, type ReceiptScope } from "../contracts/index.js";
1
+ import { type ApprovalPolicy, type AgentExecutionIntent, type ContextHandoffReceipt, type CostProvenance, type ExecutionProfile, type ExternalOutcomeEvidenceReference, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type ProviderUsageSettlement, type MutationMode, type LoopRecord, type LoopTask, type ReceiptScope, type RollbackBoundaryArtifact } from "../contracts/index.js";
2
2
  import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, type ExitDecision } from "./policy.js";
3
3
  import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
4
4
  import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
@@ -7,6 +7,7 @@ import { type RunStore } from "./persistence/index.js";
7
7
  import { type ExitPolicyOverrides } from "./exits.js";
8
8
  import { type ExitSignalSource } from "./exit-signal.js";
9
9
  import { type VerifierExecutionBinding } from "./verified-handoff.js";
10
+ import { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
10
11
  export type { AgentExecutionIntent, ApprovalPolicy, BudgetPreflightEstimate, BudgetSettlement, CostProvenance, EvidenceVector, ExecutionProfile, FailureClass, InterventionType, PatchDecision, PatchDecisionArtifact, PatchDecisionReasonCode, PatchScore, MutationMode, RollbackBoundaryArtifact, RollbackBoundaryStrategy, RollbackFileSnapshot, RollbackOutcomeArtifact, RollbackOutcomeStatus, PolicyPhase, CallStage, AgentRole, FirstDelta, RoutingEconomics } from "../contracts/index.js";
11
12
  export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
12
13
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary };
@@ -22,6 +23,7 @@ export type { LocalIdentityAuthority } from "./identity.js";
22
23
  export { compileExecutionPolicy } from "./policy-compiler.js";
23
24
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
24
25
  export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
26
+ export { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
25
27
  export type { AvoidedUsdInput } from "./savings.js";
26
28
  export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
27
29
  export type { RouteDecision, RouteClassificationInput } from "./routing.js";
@@ -257,6 +259,15 @@ export interface RunMartinInput {
257
259
  idFactory?: (prefix: string) => string;
258
260
  maxRecentAttempts?: number;
259
261
  fallbackAdapters?: MartinAdapter[];
262
+ /**
263
+ * Continue an unfinished persisted run under the same loop ID and authority.
264
+ * runMartin acquires the durable execution lease before any store persistence.
265
+ */
266
+ resume?: {
267
+ loop: LoopRecord;
268
+ preCandidateBoundary?: RollbackBoundaryArtifact;
269
+ lastVerifierFailure?: MartinAdapterRequest["context"]["lastVerifierFailure"];
270
+ };
260
271
  receiptScope?: ReceiptScope;
261
272
  /** Optional persistence store. When provided, runMartin writes artifacts on each lifecycle event. */
262
273
  store?: RunStore;