@martinloop/mcp 0.5.3 → 0.5.6

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 (65) hide show
  1. package/README.md +175 -175
  2. package/dist/arcade/capabilities.d.ts +36 -0
  3. package/dist/arcade/capabilities.js +37 -0
  4. package/dist/arcade/resource.d.ts +31 -0
  5. package/dist/arcade/resource.js +52 -0
  6. package/dist/arcade/status.d.ts +23 -0
  7. package/dist/arcade/status.js +25 -0
  8. package/dist/package-version.d.ts +1 -1
  9. package/dist/package-version.js +3 -3
  10. package/dist/resources.js +147 -147
  11. package/dist/server-validation.js +2 -0
  12. package/dist/server.js +79 -35
  13. package/dist/tools/run-loop.d.ts +2 -2
  14. package/dist/tools/run-loop.js +26 -6
  15. package/dist/tools/tool-response.d.ts +9 -1
  16. package/dist/tools/tool-response.js +7 -5
  17. package/dist/tools/tool-support.js +62 -271
  18. package/dist/vendor/adapters/claude-cli.d.ts +9 -0
  19. package/dist/vendor/adapters/claude-cli.js +21 -3
  20. package/dist/vendor/adapters/cli-bridge.js +7 -1
  21. package/dist/vendor/adapters/codex-capabilities.d.ts +12 -3
  22. package/dist/vendor/adapters/codex-capabilities.js +46 -18
  23. package/dist/vendor/adapters/codex-cli.d.ts +2 -1
  24. package/dist/vendor/adapters/codex-cli.js +12 -2
  25. package/dist/vendor/adapters/codex-host.d.ts +4 -1
  26. package/dist/vendor/adapters/codex-host.js +34 -16
  27. package/dist/vendor/adapters/codex-launcher.d.ts +1 -1
  28. package/dist/vendor/adapters/codex-launcher.js +1 -1
  29. package/dist/vendor/adapters/index.d.ts +3 -1
  30. package/dist/vendor/adapters/index.js +2 -0
  31. package/dist/vendor/adapters/openai-compatible.js +12 -9
  32. package/dist/vendor/adapters/stub-agent-cli.d.ts +9 -0
  33. package/dist/vendor/adapters/stub-agent-cli.js +43 -0
  34. package/dist/vendor/adapters/stub-direct-provider.d.ts +8 -0
  35. package/dist/vendor/adapters/stub-direct-provider.js +16 -0
  36. package/dist/vendor/contracts/agent-execution.d.ts +9 -0
  37. package/dist/vendor/contracts/agent-execution.js +14 -0
  38. package/dist/vendor/contracts/index.d.ts +9 -0
  39. package/dist/vendor/contracts/index.js +1 -0
  40. package/dist/vendor/core/grounding.js +42 -7
  41. package/dist/vendor/core/index.d.ts +5 -2
  42. package/dist/vendor/core/index.js +20 -66
  43. package/dist/vendor/core/policy.d.ts +1 -0
  44. package/dist/vendor/core/policy.js +8 -0
  45. package/dist/vendor/core/rollback.d.ts +3 -0
  46. package/dist/vendor/core/rollback.js +33 -20
  47. package/dist/vendor/core/verified-handoff.js +25 -6
  48. package/dist/vendor/presentation/governed-plan.d.ts +33 -0
  49. package/dist/vendor/presentation/governed-plan.js +146 -0
  50. package/dist/vendor/presentation/index.d.ts +7 -0
  51. package/dist/vendor/presentation/index.js +7 -0
  52. package/dist/vendor/presentation/motion.d.ts +12 -0
  53. package/dist/vendor/presentation/motion.js +123 -0
  54. package/dist/vendor/presentation/status.d.ts +8 -0
  55. package/dist/vendor/presentation/status.js +11 -0
  56. package/dist/vendor/presentation/table.d.ts +11 -0
  57. package/dist/vendor/presentation/table.js +47 -0
  58. package/dist/vendor/presentation/text.d.ts +6 -0
  59. package/dist/vendor/presentation/text.js +30 -0
  60. package/dist/vendor/presentation/theme.d.ts +31 -0
  61. package/dist/vendor/presentation/theme.js +82 -0
  62. package/dist/vendor/presentation/verified-handoff.d.ts +15 -0
  63. package/dist/vendor/presentation/verified-handoff.js +148 -0
  64. package/package.json +81 -80
  65. package/server.json +21 -21
@@ -0,0 +1,43 @@
1
+ import { createAdapterCapabilities } from "./runtime-support.js";
2
+ export function createStubAgentCliAdapter(options) {
3
+ const metadata = {
4
+ command: options.command.join(" "),
5
+ providerId: "agent-cli",
6
+ model: options.profile ?? options.command[0] ?? "default",
7
+ transport: "cli",
8
+ capabilities: createAdapterCapabilities(),
9
+ ...(options.profile ? { profile: options.profile } : {})
10
+ };
11
+ return {
12
+ adapterId: `agent-cli:stub:${options.command.join(":")}`,
13
+ kind: "agent-cli",
14
+ label: options.label ?? `Stub agent CLI (${options.command.join(" ")})`,
15
+ metadata,
16
+ async execute(request) {
17
+ if (options.responder) {
18
+ return await options.responder(request);
19
+ }
20
+ return {
21
+ status: "failed",
22
+ summary: `Stub agent CLI ${options.command.join(" ")} did not execute a live session.`,
23
+ usage: {
24
+ actualUsd: 0,
25
+ tokensIn: 0,
26
+ tokensOut: 0,
27
+ provenance: "unavailable"
28
+ },
29
+ verification: {
30
+ passed: false,
31
+ summary: "No CLI execution was attempted."
32
+ },
33
+ failure: {
34
+ message: `Agent CLI ${options.command.join(" ")} is not configured for live execution.`,
35
+ classHint: "environment_mismatch"
36
+ }
37
+ };
38
+ }
39
+ };
40
+ }
41
+ // Internal test-compatibility re-export. Public builders remove the entire
42
+ // stub compatibility surface before constructing distributable packages.
43
+ export { createStubDirectProviderAdapter } from "./stub-direct-provider.js";
@@ -0,0 +1,8 @@
1
+ import type { MartinAdapter, MartinAdapterRequest, MartinAdapterResult } from "../core/index.js";
2
+ export interface StubDirectProviderAdapterOptions {
3
+ providerId: string;
4
+ model: string;
5
+ label?: string;
6
+ responder?: (request: MartinAdapterRequest) => Promise<MartinAdapterResult> | MartinAdapterResult;
7
+ }
8
+ export declare function createStubDirectProviderAdapter(options: StubDirectProviderAdapterOptions): MartinAdapter;
@@ -0,0 +1,16 @@
1
+ import { createDirectProviderAdapter } from "./direct-provider.js";
2
+ export function createStubDirectProviderAdapter(options) {
3
+ const adapter = createDirectProviderAdapter({
4
+ providerId: options.providerId,
5
+ model: options.model,
6
+ label: options.label ?? `Stub direct provider (${options.providerId}/${options.model})`,
7
+ capabilities: {
8
+ workspaceMutations: false
9
+ },
10
+ responder: options.responder
11
+ });
12
+ return {
13
+ ...adapter,
14
+ adapterId: `direct:stub:${options.providerId}:${options.model}`,
15
+ };
16
+ }
@@ -0,0 +1,9 @@
1
+ export declare const DEFAULT_AGENT_EXECUTION_INTENT: "governed-autonomous";
2
+ export type AgentExecutionIntent = typeof DEFAULT_AGENT_EXECUTION_INTENT;
3
+ /** Provider-neutral safety boundary every governed-autonomous adapter must honor. */
4
+ export declare const GOVERNED_AUTONOMOUS_BOUNDARY: {
5
+ readonly interaction: "non-interactive";
6
+ readonly writeScope: "workspace-only";
7
+ };
8
+ export declare const DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS = 300000;
9
+ export declare function normalizeProviderExecutionTimeoutMs(value: number | undefined): number;
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_AGENT_EXECUTION_INTENT = "governed-autonomous";
2
+ /** Provider-neutral safety boundary every governed-autonomous adapter must honor. */
3
+ export const GOVERNED_AUTONOMOUS_BOUNDARY = {
4
+ interaction: "non-interactive",
5
+ writeScope: "workspace-only"
6
+ };
7
+ export const DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS = 300_000;
8
+ export function normalizeProviderExecutionTimeoutMs(value) {
9
+ const resolved = value ?? DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS;
10
+ if (!Number.isFinite(resolved) || resolved <= 0) {
11
+ throw new Error("providerExecutionTimeoutMs must be a positive finite number.");
12
+ }
13
+ return resolved;
14
+ }
@@ -8,6 +8,9 @@
8
8
  * portfolio snapshots and routing economics.
9
9
  */
10
10
  import type { TerminationEnvelopeV1 } from "./exits.js";
11
+ import type { AgentExecutionIntent } from "./agent-execution.js";
12
+ export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "./agent-execution.js";
13
+ export type { AgentExecutionIntent } from "./agent-execution.js";
11
14
  export type LoopStatus = "queued" | "running" | "verifying" | "completed" | "failed" | "exited";
12
15
  export type LoopLifecycleState = "created" | "running" | "verifying" | "completed" | "budget_exit" | "diminishing_returns" | "stuck_exit" | "human_escalation" | "wall_clock" | "error_threshold" | "external_event";
13
16
  export declare const FAILURE_CLASSES: readonly ["logic_error", "hallucination", "syntax_error", "type_error", "test_regression", "scope_creep", "no_progress", "repo_grounding_failure", "verification_failure", "environment_mismatch", "budget_pressure", "safety_leash_blocked", "sandbox_write_blocked"];
@@ -27,6 +30,10 @@ export interface LoopTask {
27
30
  executionProfile?: ExecutionProfile;
28
31
  allowedNetworkDomains?: string[];
29
32
  approvalPolicy?: ApprovalPolicy;
33
+ /** Provider-neutral execution posture. Defaults to governed autonomous execution. */
34
+ agentExecutionIntent?: AgentExecutionIntent;
35
+ /** Hard wall-clock limit for one provider coding process. */
36
+ providerExecutionTimeoutMs?: number;
30
37
  /** Glob patterns for files the agent is allowed to modify. Empty = no restriction. */
31
38
  allowedPaths?: string[];
32
39
  /** Glob patterns for files the agent must never modify. */
@@ -103,6 +110,8 @@ export interface ReceiptScope {
103
110
  writableRoot?: string;
104
111
  /** How the effective sandbox was determined. */
105
112
  capabilitySource?: "probe" | "configured" | "unknown";
113
+ agentExecutionIntent?: AgentExecutionIntent;
114
+ providerExecutionTimeoutMs?: number;
106
115
  /** Enforced demo changes (DEMO.md-only enforcement output). */
107
116
  demoChangedFiles?: string[];
108
117
  }
@@ -7,6 +7,7 @@
7
7
  * creating and managing loop records, handling events, validating batches, and tracking
8
8
  * portfolio snapshots and routing economics.
9
9
  */
10
+ export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "./agent-execution.js";
10
11
  export const FAILURE_CLASSES = [
11
12
  "logic_error",
12
13
  "hallucination",
@@ -196,7 +196,14 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
196
196
  const indexedPaths = new Set(index.files.map((file) => file.path));
197
197
  const indexedSymbols = new Set(index.files.flatMap((file) => file.symbols.map((symbol) => symbol.toLowerCase())));
198
198
  const referencedFiles = new Set();
199
+ const addedFiles = new Set();
199
200
  const diffFilePattern = /^(?:---|\+\+\+)\s+[ab]\/(.+)$/gm;
201
+ const addedFilePattern = /^---\s+\/dev\/null\r?\n\+\+\+\s+b\/(.+)$/gm;
202
+ for (const match of diff.matchAll(addedFilePattern)) {
203
+ const filePath = match[1]?.trim();
204
+ if (filePath)
205
+ addedFiles.add(filePath);
206
+ }
200
207
  for (const match of diff.matchAll(diffFilePattern)) {
201
208
  const filePath = match[1]?.trim();
202
209
  if (filePath && filePath !== "/dev/null") {
@@ -204,7 +211,7 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
204
211
  }
205
212
  }
206
213
  for (const filePath of referencedFiles) {
207
- if (indexedPaths.has(filePath)) {
214
+ if (indexedPaths.has(filePath) || addedFiles.has(filePath)) {
208
215
  resolvedFiles.push(filePath);
209
216
  }
210
217
  else {
@@ -227,7 +234,7 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
227
234
  }
228
235
  const addedLinePattern = /^\+(?!\+\+)(.+)$/gm;
229
236
  const importPattern = /(?:import|require)\s*(?:.*\s+from\s+)?['"]([^'"]+)['"]/g;
230
- const identifierPattern = /\b([A-Za-z_][A-Za-z0-9_]{2,})\b/g;
237
+ const callableIdentifierPattern = /(?<!\.)\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
231
238
  const symbolKeywords = new Set([
232
239
  "const",
233
240
  "let",
@@ -244,10 +251,33 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
244
251
  "true",
245
252
  "false",
246
253
  "null",
247
- "undefined"
254
+ "undefined",
255
+ "if",
256
+ "for",
257
+ "while",
258
+ "switch",
259
+ "catch",
260
+ "typeof"
248
261
  ]);
249
- for (const lineMatch of diff.matchAll(addedLinePattern)) {
250
- const line = lineMatch[1] ?? "";
262
+ const addedLines = [...diff.matchAll(addedLinePattern)].map((match) => match[1] ?? "");
263
+ const declaredSymbols = new Set();
264
+ const declarationPattern = /\b(?:function|class|interface|type|enum|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
265
+ for (const line of addedLines) {
266
+ for (const declarationMatch of line.matchAll(declarationPattern)) {
267
+ const symbol = declarationMatch[1];
268
+ if (symbol)
269
+ declaredSymbols.add(symbol.toLowerCase());
270
+ }
271
+ if (/^\s*import\b/u.test(line)) {
272
+ const importClause = line.match(/^\s*import\s+(.+?)\s+from\s+['"]/u)?.[1] ?? "";
273
+ for (const importSymbol of importClause.matchAll(/\b(?:as\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/g)) {
274
+ const symbol = importSymbol[1];
275
+ if (symbol && symbol !== "as")
276
+ declaredSymbols.add(symbol.toLowerCase());
277
+ }
278
+ }
279
+ }
280
+ for (const line of addedLines) {
251
281
  for (const importMatch of line.matchAll(importPattern)) {
252
282
  const reference = importMatch[1] ?? "";
253
283
  if (!reference.startsWith(".") && !reference.startsWith("/")) {
@@ -259,6 +289,9 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
259
289
  const hasMatch = index.files.some((file) => {
260
290
  const withoutExt = file.path.replace(/\.(js|ts|tsx|jsx)$/, "");
261
291
  return withoutExt.endsWith(normalized) || withoutExt.includes(normalized);
292
+ }) || [...addedFiles].some((filePath) => {
293
+ const withoutExt = filePath.replace(/\.(js|ts|tsx|jsx)$/, "");
294
+ return withoutExt.endsWith(normalized) || withoutExt.includes(normalized);
262
295
  });
263
296
  if (!hasMatch) {
264
297
  violations.push({
@@ -268,13 +301,15 @@ export function scanPatchForGroundingViolations(diff, index, options = {}) {
268
301
  });
269
302
  }
270
303
  }
271
- for (const symbolMatch of line.matchAll(identifierPattern)) {
304
+ for (const symbolMatch of line.matchAll(callableIdentifierPattern)) {
272
305
  const symbol = symbolMatch[1];
273
306
  if (!symbol)
274
307
  continue;
275
308
  if (symbolKeywords.has(symbol))
276
309
  continue;
277
- if (symbol.toUpperCase() === symbol)
310
+ if (/^[A-Z]/u.test(symbol))
311
+ continue;
312
+ if (declaredSymbols.has(symbol.toLowerCase()))
278
313
  continue;
279
314
  if (indexedSymbols.has(symbol.toLowerCase()))
280
315
  continue;
@@ -1,4 +1,4 @@
1
- import { type ApprovalPolicy, type ContextHandoffReceipt, type CostProvenance, type ExecutionProfile, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type ProviderUsageSettlement, type MutationMode, type LoopRecord, type LoopTask, type ReceiptScope } from "../contracts/index.js";
1
+ import { type ApprovalPolicy, type AgentExecutionIntent, type ContextHandoffReceipt, type CostProvenance, type ExecutionProfile, type FailureClass, type InterventionType, type LoopArtifact, type LoopAttempt, type LoopBudget, type ProviderUsageSettlement, type MutationMode, type LoopRecord, type LoopTask, type ReceiptScope } from "../contracts/index.js";
2
2
  import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, type ExitDecision } from "./policy.js";
3
3
  import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
4
4
  import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
@@ -7,7 +7,8 @@ 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
- export type { 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";
10
+ 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
+ export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
11
12
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
12
13
  export type { BudgetPreflightDecision, BudgetPreflightInput, CostGovernorState, EvidenceVectorInput, EvaluatedPatchDecision, ExitDecision, FailureAssessment, PatchDecisionInput, RecoveryDecision, RecoveryRecipe } from "./policy.js";
13
14
  export type { ResolvedExecutionProfile, SafetyLeashDecision, SafetyViolation } from "./leash.js";
@@ -69,6 +70,8 @@ export interface MartinAdapterRequest {
69
70
  executionProfile?: ExecutionProfile;
70
71
  allowedNetworkDomains?: string[];
71
72
  approvalPolicy?: ApprovalPolicy;
73
+ agentExecutionIntent?: AgentExecutionIntent;
74
+ providerExecutionTimeoutMs?: number;
72
75
  focus: string;
73
76
  remainingBudgetUsd: number;
74
77
  remainingIterations: number;
@@ -1,6 +1,4 @@
1
- import { spawnSync } from "node:child_process";
2
- import { isAbsolute, relative, resolve } from "node:path";
3
- import { appendLoopEvent, createLoopRecord } from "../contracts/index.js";
1
+ import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs, appendLoopEvent, createLoopRecord } from "../contracts/index.js";
4
2
  import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
5
3
  import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
6
4
  import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
@@ -15,6 +13,7 @@ import { runContextIntegrityPrecheck } from "./context-integrity.js";
15
13
  import { decideCircuitBreak } from "./trajectory.js";
16
14
  import { verifyContextHandoff, decideContextCircuitBreak } from "./context-handoff.js";
17
15
  import { verifierActuallyPassed, } from "./verified-handoff.js";
16
+ export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
18
17
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
19
18
  export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
20
19
  export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
@@ -160,12 +159,16 @@ export function distillContext(loop, options = {}) {
160
159
  export async function runMartin(input) {
161
160
  const now = input.now ?? (() => new Date().toISOString());
162
161
  const idFactory = input.idFactory;
162
+ const providerExecutionTimeoutMs = normalizeProviderExecutionTimeoutMs(input.task.providerExecutionTimeoutMs);
163
+ const agentExecutionIntent = input.task.agentExecutionIntent ?? DEFAULT_AGENT_EXECUTION_INTENT;
163
164
  let loop = createLoopRecord({
164
165
  workspaceId: input.workspaceId,
165
166
  projectId: input.projectId,
166
- task: input.task,
167
+ task: { ...input.task, agentExecutionIntent, providerExecutionTimeoutMs },
167
168
  budget: input.budget,
168
- ...(input.receiptScope ? { receiptScope: input.receiptScope } : {}),
169
+ ...(input.receiptScope
170
+ ? { receiptScope: { ...input.receiptScope, agentExecutionIntent, providerExecutionTimeoutMs } }
171
+ : {}),
169
172
  ...(input.teamId ? { teamId: input.teamId } : {}),
170
173
  ...(input.metadata ? { metadata: input.metadata } : {})
171
174
  }, { now: now(), idFactory });
@@ -655,6 +658,8 @@ export async function runMartin(input) {
655
658
  ? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
656
659
  : {}),
657
660
  ...(loop.task.approvalPolicy ? { approvalPolicy: loop.task.approvalPolicy } : {}),
661
+ agentExecutionIntent: loop.task.agentExecutionIntent ?? DEFAULT_AGENT_EXECUTION_INTENT,
662
+ providerExecutionTimeoutMs: normalizeProviderExecutionTimeoutMs(loop.task.providerExecutionTimeoutMs),
658
663
  focus: distilled.focus,
659
664
  remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
660
665
  remainingIterations: distilled.constraints.remainingIterations,
@@ -745,6 +750,8 @@ export async function runMartin(input) {
745
750
  ? { allowedNetworkDomains: loop.task.allowedNetworkDomains }
746
751
  : {}),
747
752
  ...(loop.task.approvalPolicy ? { approvalPolicy: loop.task.approvalPolicy } : {}),
753
+ agentExecutionIntent: loop.task.agentExecutionIntent ?? DEFAULT_AGENT_EXECUTION_INTENT,
754
+ providerExecutionTimeoutMs: normalizeProviderExecutionTimeoutMs(loop.task.providerExecutionTimeoutMs),
748
755
  focus: distilled.focus,
749
756
  remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
750
757
  remainingIterations: distilled.constraints.remainingIterations,
@@ -787,6 +794,7 @@ export async function runMartin(input) {
787
794
  const attemptCompletedAt = now();
788
795
  const compiledContext = compilePromptPacket(request);
789
796
  const expectedVerifierBinding = resolveExpectedVerifierBinding(request);
797
+ const verifierConfigured = expectedVerifierBinding.commands.length > 0;
790
798
  const verification = normalizeVerificationOutcome(result, expectedVerifierBinding);
791
799
  // PATCH → VERIFY
792
800
  currentPhase = "VERIFY";
@@ -1126,17 +1134,12 @@ export async function runMartin(input) {
1126
1134
  // Uses the task's repoRoot to build/load the grounding index, then scans any diff
1127
1135
  let groundingScanResult;
1128
1136
  const patchDiff = buildPatchDiff(result, changedFiles);
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) {
1137
+ if (patchDiff && input.task.repoRoot) {
1134
1138
  try {
1135
1139
  const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
1136
- groundingScanResult = reconcileExistingGroundingFiles(scanPatchForGroundingViolations(patchDiff, groundingIndex, {
1140
+ groundingScanResult = scanPatchForGroundingViolations(patchDiff, groundingIndex, {
1137
1141
  allowedPaths: input.task.allowedPaths
1138
- }), input.task.repoRoot, rollbackBoundary);
1139
- groundingEvidenceAvailable = true;
1142
+ });
1140
1143
  if (input.store && groundingScanResult.violations.length > 0) {
1141
1144
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1142
1145
  kind: "grounding.violations_found",
@@ -1152,22 +1155,10 @@ export async function runMartin(input) {
1152
1155
  }
1153
1156
  }
1154
1157
  catch {
1155
- // Availability is persisted below so scanner failures and missing
1156
- // grounding inputs follow the same recover-forward trust contract.
1158
+ // Grounding scan is best-effort — never fail the loop because of a scan error
1157
1159
  }
1158
1160
  }
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;
1161
+ const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations && verifierConfigured;
1171
1162
  let patchDecision;
1172
1163
  if (shouldEvaluatePatchTruth) {
1173
1164
  patchDecision = evaluatePatchDecision({
@@ -1228,7 +1219,7 @@ export async function runMartin(input) {
1228
1219
  }
1229
1220
  else {
1230
1221
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1231
- kind: verification.passed ? "attempt.kept" : "attempt.discarded",
1222
+ kind: verification.passed || !verifierConfigured ? "attempt.kept" : "attempt.discarded",
1232
1223
  runId: loop.loopId,
1233
1224
  attemptIndex: currentAttemptIndex,
1234
1225
  payload: { reason: verification.summary }
@@ -1305,6 +1296,7 @@ export async function runMartin(input) {
1305
1296
  lastResult: effectiveResult,
1306
1297
  lastFailure: failure,
1307
1298
  costState,
1299
+ verificationRequired: verifierConfigured,
1308
1300
  canSwitchAdapter: failure?.recommendedIntervention === "switch_adapter" &&
1309
1301
  adapterChain[currentAdapterIndex] !== undefined &&
1310
1302
  currentAdapter.adapterId !== executingAdapter.adapterId
@@ -1569,9 +1561,6 @@ function mergeCostProvenance(previous, current) {
1569
1561
  return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
1570
1562
  }
1571
1563
  function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
1572
- if (repoRoot && rollbackBoundary?.headRef) {
1573
- return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
1574
- }
1575
1564
  if (result.execution?.changedFiles !== undefined) {
1576
1565
  return result.execution.changedFiles;
1577
1566
  }
@@ -1592,41 +1581,6 @@ function buildPatchDiff(result, changedFiles) {
1592
1581
  }
1593
1582
  return undefined;
1594
1583
  }
1595
- function reconcileExistingGroundingFiles(scan, repoRoot, boundary) {
1596
- const resolvedFiles = new Set(scan.resolvedFiles);
1597
- const violations = scan.violations.filter((violation) => {
1598
- if (violation.kind !== "file_not_found") {
1599
- return true;
1600
- }
1601
- const absolutePath = resolve(repoRoot, violation.reference);
1602
- const relativePath = relative(repoRoot, absolutePath);
1603
- const isInsideRepo = relativePath.length > 0 && !relativePath.startsWith("..") && !isAbsolute(relativePath);
1604
- if (!isInsideRepo || !isTrackedRepoFileAtBoundary(repoRoot, violation.reference, boundary)) {
1605
- return true;
1606
- }
1607
- resolvedFiles.add(violation.reference);
1608
- return false;
1609
- });
1610
- return {
1611
- ...scan,
1612
- violations,
1613
- resolvedFiles: [...resolvedFiles]
1614
- };
1615
- }
1616
- function isTrackedRepoFileAtBoundary(repoRoot, filePath, boundary) {
1617
- if (!boundary) {
1618
- return false;
1619
- }
1620
- const normalizedPath = filePath.replace(/\\/gu, "/");
1621
- if (boundary.snapshots.some((snapshot) => snapshot.existed && snapshot.path === normalizedPath)) {
1622
- return true;
1623
- }
1624
- if (!boundary.headRef) {
1625
- return false;
1626
- }
1627
- const result = spawnSync("git", ["-C", repoRoot, "cat-file", "-e", `${boundary.headRef}:${normalizedPath}`], { encoding: "utf8", stdio: "ignore", windowsHide: true });
1628
- return result.status === 0;
1629
- }
1630
1584
  function createBudgetSettlement(input) {
1631
1585
  const totalActualUsd = getUsageUsd(input.usage);
1632
1586
  return {
@@ -97,6 +97,7 @@ export declare function inferExit(input: {
97
97
  lastFailure?: FailureAssessment;
98
98
  costState: CostGovernorState;
99
99
  canSwitchAdapter?: boolean;
100
+ verificationRequired?: boolean;
100
101
  }): ExitDecision;
101
102
  export interface BudgetPreflightInput {
102
103
  promptCharCount: number;
@@ -210,6 +210,14 @@ export function calculateCostPerOutcome(input) {
210
210
  };
211
211
  }
212
212
  export function inferExit(input) {
213
+ if (input.lastResult.status === "completed" && input.verificationRequired === false) {
214
+ return {
215
+ shouldExit: true,
216
+ lifecycleState: "completed",
217
+ status: "completed",
218
+ reason: "Execution-only work completed without verifier evidence; outcome is not VERIFIED."
219
+ };
220
+ }
213
221
  if (input.lastResult.status === "completed" && input.lastResult.verification.passed) {
214
222
  return {
215
223
  shouldExit: true,
@@ -1,4 +1,7 @@
1
1
  import type { PatchDecision, RollbackBoundaryArtifact, RollbackOutcomeArtifact } from "../contracts/index.js";
2
+ export declare class RollbackStateUnavailableError extends Error {
3
+ constructor(message: string);
4
+ }
2
5
  export declare function listAttemptChangedFilesSinceBoundary(input: {
3
6
  repoRoot?: string;
4
7
  boundary?: RollbackBoundaryArtifact;
@@ -1,7 +1,12 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { readFileSync } from "node:fs";
3
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
3
  import { dirname, relative, resolve } from "node:path";
4
+ export class RollbackStateUnavailableError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "RollbackStateUnavailableError";
8
+ }
9
+ }
5
10
  export function listAttemptChangedFilesSinceBoundary(input) {
6
11
  if (!input.repoRoot) {
7
12
  return [];
@@ -12,24 +17,11 @@ export function listAttemptChangedFilesSinceBoundary(input) {
12
17
  }
13
18
  const baselineTracked = new Set(input.boundary.trackedDirtyFiles);
14
19
  const baselineUntracked = new Set(input.boundary.untrackedFiles);
15
- const baselineContentChanges = input.boundary.snapshots
16
- .filter((snapshot) => snapshot.existed && repoFileDiffersFromSnapshot(input.repoRoot, snapshot))
17
- .map((snapshot) => snapshot.path);
18
20
  return uniqueSorted([
19
21
  ...repoState.trackedDirtyFiles.filter((filePath) => !baselineTracked.has(filePath)),
20
- ...repoState.untrackedFiles.filter((filePath) => !baselineUntracked.has(filePath)),
21
- ...baselineContentChanges
22
+ ...repoState.untrackedFiles.filter((filePath) => !baselineUntracked.has(filePath))
22
23
  ]);
23
24
  }
24
- function repoFileDiffersFromSnapshot(repoRoot, snapshot) {
25
- try {
26
- const current = readFileSync(resolveRepoPath(repoRoot, snapshot.path));
27
- return current.toString("base64") !== snapshot.contentBase64;
28
- }
29
- catch {
30
- return true;
31
- }
32
- }
33
25
  export async function captureRollbackBoundary(input) {
34
26
  if (!input.repoRoot) {
35
27
  return undefined;
@@ -147,6 +139,7 @@ function readRepoState(repoRoot) {
147
139
  };
148
140
  }
149
141
  function readGitLines(repoRoot, args) {
142
+ let lastFailure = "unknown Git failure";
150
143
  for (let attempt = 0; attempt < 2; attempt++) {
151
144
  const result = spawnSync("git", args, {
152
145
  cwd: repoRoot,
@@ -158,13 +151,14 @@ function readGitLines(repoRoot, args) {
158
151
  .map((line) => normalizeRepoPath(line))
159
152
  .filter(Boolean));
160
153
  }
154
+ lastFailure = gitFailureMessage(result);
161
155
  // Retry once after a short delay — handles git lock-file contention
162
156
  // that occurs when another process holds .git/index.lock.
163
157
  if (attempt === 0 && result.status !== 0) {
164
- spawnSync("sleep", ["0.5"], { encoding: "utf8" });
158
+ delaySynchronously(500);
165
159
  }
166
160
  }
167
- return [];
161
+ throw new RollbackStateUnavailableError(`Git rollback state unavailable for ${args.join(" ")}: ${lastFailure}`);
168
162
  }
169
163
  function readGitScalar(repoRoot, args) {
170
164
  const result = spawnSync("git", args, {
@@ -172,7 +166,7 @@ function readGitScalar(repoRoot, args) {
172
166
  encoding: "utf8"
173
167
  });
174
168
  if (result.status !== 0 || typeof result.stdout !== "string") {
175
- return undefined;
169
+ throw new RollbackStateUnavailableError(`Git rollback state unavailable for ${args.join(" ")}: ${gitFailureMessage(result)}`);
176
170
  }
177
171
  const value = result.stdout.trim();
178
172
  return value.length > 0 ? value : undefined;
@@ -188,7 +182,10 @@ async function readRollbackSnapshot(repoRoot, filePath) {
188
182
  contentBase64: contents.toString("base64")
189
183
  };
190
184
  }
191
- catch {
185
+ catch (error) {
186
+ if (!isFileNotFoundError(error)) {
187
+ throw new RollbackStateUnavailableError(`Rollback snapshot unavailable for ${normalizeRepoPath(filePath)}: ${toErrorMessage(error)}`);
188
+ }
192
189
  return {
193
190
  path: normalizeRepoPath(filePath),
194
191
  existed: false,
@@ -214,7 +211,7 @@ function restoreTrackedFileFromHead(repoRoot, filePath) {
214
211
  return;
215
212
  }
216
213
  // Retry once after a short delay (git lock-file contention)
217
- spawnSync("sleep", ["0.5"], { encoding: "utf8" });
214
+ delaySynchronously(500);
218
215
  const retry = spawnSync("git", ["restore", "--staged", "--worktree", "--source=HEAD", "--", filePath], { cwd: repoRoot, encoding: "utf8" });
219
216
  if (retry.status === 0) {
220
217
  return;
@@ -264,3 +261,19 @@ function emptyRepoState() {
264
261
  function toErrorMessage(error) {
265
262
  return error instanceof Error ? error.message : String(error);
266
263
  }
264
+ function gitFailureMessage(result) {
265
+ if (result.error) {
266
+ return result.error.message;
267
+ }
268
+ if (typeof result.stderr === "string" && result.stderr.trim().length > 0) {
269
+ return result.stderr.trim();
270
+ }
271
+ return `git exited with status ${String(result.status)}`;
272
+ }
273
+ function isFileNotFoundError(error) {
274
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
275
+ }
276
+ function delaySynchronously(milliseconds) {
277
+ const signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
278
+ Atomics.wait(signal, 0, 0, milliseconds);
279
+ }