@martinloop/mcp 0.5.5 → 0.5.7

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 (57) hide show
  1. package/README.md +14 -52
  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 +1 -13
  11. package/dist/server.js +241 -75
  12. package/dist/tools/execution-policy.js +1 -1
  13. package/dist/tools/get-status.d.ts +2 -4
  14. package/dist/tools/get-status.js +6 -3
  15. package/dist/tools/preflight.d.ts +1 -1
  16. package/dist/tools/run-loop.d.ts +2 -3
  17. package/dist/tools/run-loop.js +4 -3
  18. package/dist/tools/run-store.js +1 -1
  19. package/dist/tools/tool-support.d.ts +3 -2
  20. package/dist/tools/tool-support.js +9 -2
  21. package/dist/tools/workflow-governance.d.ts +1 -1
  22. package/dist/tools/workflow-governance.js +1 -1
  23. package/dist/vendor/adapters/claude-cli.js +2 -1
  24. package/dist/vendor/adapters/cli-bridge.js +7 -1
  25. package/dist/vendor/adapters/index.d.ts +1 -0
  26. package/dist/vendor/adapters/index.js +1 -0
  27. package/dist/vendor/adapters/openai-compatible.js +6 -3
  28. package/dist/vendor/adapters/stub-agent-cli.d.ts +9 -0
  29. package/dist/vendor/adapters/stub-agent-cli.js +43 -0
  30. package/dist/vendor/adapters/stub-direct-provider.d.ts +8 -0
  31. package/dist/vendor/adapters/stub-direct-provider.js +16 -0
  32. package/dist/vendor/adapters/verifier-only.js +1 -1
  33. package/dist/vendor/contracts/execution-policy.js +1 -1
  34. package/dist/vendor/contracts/exits.d.ts +1 -1
  35. package/dist/vendor/contracts/governance.d.ts +1 -1
  36. package/dist/vendor/contracts/index.d.ts +1 -1
  37. package/dist/vendor/contracts/index.js +1 -2
  38. package/dist/vendor/contracts/operator.d.ts +1 -1
  39. package/dist/vendor/contracts/operator.js +1 -2
  40. package/dist/vendor/core/compiler.d.ts +2 -2
  41. package/dist/vendor/core/compiler.js +3 -1
  42. package/dist/vendor/core/exits.js +9 -4
  43. package/dist/vendor/core/grounding.d.ts +1 -0
  44. package/dist/vendor/core/grounding.js +28 -1
  45. package/dist/vendor/core/index.d.ts +2 -2
  46. package/dist/vendor/core/index.js +11 -64
  47. package/dist/vendor/core/policy-compiler.js +17 -1
  48. package/dist/vendor/core/policy.d.ts +2 -1
  49. package/dist/vendor/core/policy.js +15 -5
  50. package/dist/vendor/core/rollback.d.ts +3 -0
  51. package/dist/vendor/core/rollback.js +33 -20
  52. package/dist/vendor/core/verified-handoff.js +25 -6
  53. package/dist/vendor/presentation/motion.js +0 -4
  54. package/dist/vendor/presentation/verified-handoff.js +10 -13
  55. package/dist/workflow-state.js +1 -1
  56. package/package.json +20 -11
  57. package/server.json +3 -3
@@ -641,7 +641,7 @@ function validateInspectableLoopRecord(value) {
641
641
  !isFiniteNumber(budget["maxUsd"]) ||
642
642
  !isFiniteNumber(budget["softLimitUsd"]) ||
643
643
  !Number.isInteger(budget["maxIterations"]) ||
644
- !Number.isInteger(budget["maxTokens"]) ||
644
+ (budget["maxTokens"] !== undefined && !Number.isInteger(budget["maxTokens"])) ||
645
645
  !isRecord(cost) ||
646
646
  !isFiniteNumber(cost["actualUsd"]) ||
647
647
  !isFiniteNumber(cost["tokensIn"]) ||
@@ -6,7 +6,8 @@ export interface InspectableLoopAttempt extends LoopAttemptRecord {
6
6
  attemptId?: string;
7
7
  summary?: string;
8
8
  }
9
- export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" | "task"> {
9
+ export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" | "task" | "budget"> {
10
+ budget: LoopBudget;
10
11
  attempts: InspectableLoopAttempt[];
11
12
  task?: LoopTask;
12
13
  artifacts?: LoopArtifact[];
@@ -33,7 +34,7 @@ export interface LoopPreview {
33
34
  shouldStop: boolean;
34
35
  remainingBudgetUsd: number;
35
36
  remainingIterations: number;
36
- remainingTokens: number;
37
+ remainingTokens?: number;
37
38
  lastAttempt?: AttemptSummary;
38
39
  routingEconomics?: Record<string, unknown>;
39
40
  }
@@ -157,7 +157,9 @@ export function buildLoopPreview(loop) {
157
157
  shouldStop: costState.shouldStop,
158
158
  remainingBudgetUsd: costState.remainingBudgetUsd,
159
159
  remainingIterations: costState.remainingIterations,
160
- remainingTokens: costState.remainingTokens,
160
+ ...(costState.remainingTokens !== undefined
161
+ ? { remainingTokens: costState.remainingTokens }
162
+ : {}),
161
163
  ...(lastAttempt ? { lastAttempt: buildAttemptSummary(lastAttempt) } : {}),
162
164
  ...(loop.routingEconomics ? { routingEconomics: buildRoutingEconomicsSummary(loop.routingEconomics) } : {})
163
165
  };
@@ -291,7 +293,12 @@ export function buildCostSnapshot(cost) {
291
293
  };
292
294
  }
293
295
  export function buildBudgetSnapshot(budget) {
294
- return { maxUsd: budget.maxUsd, softLimitUsd: budget.softLimitUsd, maxIterations: budget.maxIterations, maxTokens: budget.maxTokens };
296
+ return {
297
+ maxUsd: budget.maxUsd,
298
+ softLimitUsd: budget.softLimitUsd,
299
+ maxIterations: budget.maxIterations,
300
+ ...(budget.maxTokens !== undefined ? { maxTokens: budget.maxTokens } : {})
301
+ };
295
302
  }
296
303
  export function buildSuggestedResourceUris(loopId) {
297
304
  return ["martin://server/health", "martin://runs/recent", "martin://runs/triage", "martin://runs/latest", "martin://runs/latest/summary", "martin://runs/latest/proof-card", "martin://runs/latest/budget-status", "martin://runs/latest/verifier-evidence", "martin://runs/latest/rollback-evidence", "martin://policies/current", "martin://repo/risk-map", "martin://verifiers/results", "martin://agent/next-step", `martin://runs/${loopId}`, `martin://runs/${loopId}/dossier`, `martin://runs/${loopId}/verification`, "martin://guides/mcp-usage", "martin://guides/agent-start", "martin://guides/publish-readiness"];
@@ -50,7 +50,7 @@ export interface MartinPlanBudget {
50
50
  maxUsd: number;
51
51
  softLimitUsd: number;
52
52
  maxIterations: number;
53
- maxTokens: number;
53
+ maxTokens?: number;
54
54
  maxMinutes: number;
55
55
  maxFilesChanged: number;
56
56
  maxCommands: number;
@@ -271,7 +271,7 @@ export function normalizeLoopBudget(overrides = {}) {
271
271
  maxUsd,
272
272
  softLimitUsd,
273
273
  maxIterations: overrides.maxIterations ?? DEFAULT_BUDGET.maxIterations,
274
- maxTokens: overrides.maxTokens ?? DEFAULT_BUDGET.maxTokens
274
+ ...(overrides.maxTokens !== undefined ? { maxTokens: overrides.maxTokens } : {})
275
275
  };
276
276
  }
277
277
  export function assessRunRisk(input) {
@@ -749,6 +749,7 @@ export function createAgentCliAdapter(options) {
749
749
  workspaceId: request.workspaceId,
750
750
  cwd: workingDirectory,
751
751
  });
752
+ const verificationConfigured = verification.binding.commands.length > 0;
752
753
  // Check for zero-diff (agent ran but made no file changes)
753
754
  const postRunChangedFiles = gitRepoRoot
754
755
  ? await readGitChangedFiles(gitRepoRoot, 5_000, options.spawnImpl)
@@ -791,7 +792,7 @@ export function createAgentCliAdapter(options) {
791
792
  }
792
793
  // Derive structural classHint from evidence, not keyword scanning
793
794
  const structuralHint = inferStructuralClassHint(agentText, verification.summary, agentResult.exitCode, request.context.objective);
794
- if (verification.passed) {
795
+ if (verification.passed || !verificationConfigured) {
795
796
  return {
796
797
  status: "completed",
797
798
  summary,
@@ -154,7 +154,13 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
154
154
  commands: steps.map((step) => step.command),
155
155
  };
156
156
  if (steps.length === 0) {
157
- return { passed: true, summary: "No verification commands specified.", steps: [], binding: executionBinding };
157
+ return {
158
+ passed: false,
159
+ summary: "No verification commands specified; execution is not VERIFIED.",
160
+ steps: [],
161
+ warnings: ["Execution completed without verifier evidence."],
162
+ binding: executionBinding,
163
+ };
158
164
  }
159
165
  const failedSteps = [];
160
166
  const stepOutcomes = [];
@@ -4,5 +4,6 @@ export { createCodexCliAdapter, type CodexCliAdapterOptions } from "./codex-cli.
4
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js";
5
5
  export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js";
6
6
  /** @internal Test-only compatibility export. Public package builders strip this surface. */
7
+ export { createStubAgentCliAdapter, type StubAgentCliAdapterOptions, createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-agent-cli.js";
7
8
  export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexApprovalCapability, type CodexAutonomyResolution, 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";
8
9
  export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js";
@@ -4,5 +4,6 @@ export { createCodexCliAdapter } from "./codex-cli.js";
4
4
  export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js";
5
5
  export { createVerifierOnlyAdapter } from "./verifier-only.js";
6
6
  /** @internal Test-only compatibility export. Public package builders strip this surface. */
7
+ export { createStubAgentCliAdapter, createStubDirectProviderAdapter } from "./stub-agent-cli.js";
7
8
  export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js";
8
9
  export { createSpawnPlan } from "./cli-bridge.js";
@@ -347,11 +347,14 @@ export function createOpenAiCompatibleAdapter(options) {
347
347
  ? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
348
348
  : []
349
349
  };
350
+ const verificationConfigured = verification.binding.commands.length > 0;
350
351
  return {
351
- status: verification.passed ? "completed" : "failed",
352
+ status: verification.passed || !verificationConfigured ? "completed" : "failed",
352
353
  summary: verification.passed
353
354
  ? `${model} completed the task. Verifier passed.`
354
- : `${model} completed but verifier failed: ${verification.summary}`,
355
+ : !verificationConfigured
356
+ ? `${model} completed the task without verifier evidence; outcome is not VERIFIED.`
357
+ : `${model} completed but verifier failed: ${verification.summary}`,
355
358
  usage: normalizeOpenAiCompatibleUsage({
356
359
  model,
357
360
  tokensIn,
@@ -362,7 +365,7 @@ export function createOpenAiCompatibleAdapter(options) {
362
365
  }),
363
366
  verification,
364
367
  execution,
365
- ...(verification.passed ? {} : {
368
+ ...(verification.passed || !verificationConfigured ? {} : {
366
369
  failure: { message: verification.summary }
367
370
  })
368
371
  };
@@ -0,0 +1,9 @@
1
+ import type { MartinAdapter, MartinAdapterRequest, MartinAdapterResult } from "../core/index.js";
2
+ export interface StubAgentCliAdapterOptions {
3
+ command: string[];
4
+ profile?: string;
5
+ label?: string;
6
+ responder?: (request: MartinAdapterRequest) => Promise<MartinAdapterResult> | MartinAdapterResult;
7
+ }
8
+ export declare function createStubAgentCliAdapter(options: StubAgentCliAdapterOptions): MartinAdapter;
9
+ export { createStubDirectProviderAdapter, type StubDirectProviderAdapterOptions } from "./stub-direct-provider.js";
@@ -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
+ }
@@ -26,7 +26,7 @@ export function createVerifierOnlyAdapter(options = {}) {
26
26
  const verification = await runVerification(request.context.verificationPlan, workingDirectory, verifyTimeoutMs, request.context.verificationStack, options.spawnImpl, {
27
27
  runId: request.loopId,
28
28
  workspaceId: request.workspaceId,
29
- cwd: workingDirectory
29
+ cwd: workingDirectory,
30
30
  });
31
31
  const changedFiles = shouldTrackVerifierWrites
32
32
  ? (await readGitChangedFiles(workingDirectory, 5_000)).filter((file) => !baselineChangedFiles.has(file))
@@ -14,7 +14,7 @@ export function cloneExecutionPolicy(policy) {
14
14
  maxUsd: policy.budget.maxUsd,
15
15
  softLimitUsd: policy.budget.softLimitUsd,
16
16
  maxIterations: policy.budget.maxIterations,
17
- maxTokens: policy.budget.maxTokens
17
+ ...(policy.budget.maxTokens !== undefined ? { maxTokens: policy.budget.maxTokens } : {})
18
18
  },
19
19
  governance: {
20
20
  policyProfile: policy.governance.policyProfile,
@@ -44,7 +44,7 @@ export interface ExitPolicyV1 {
44
44
  };
45
45
  budget: {
46
46
  maxUsd: number;
47
- maxTokens: number;
47
+ maxTokens?: number;
48
48
  };
49
49
  wallClock: {
50
50
  maxElapsedMs: number;
@@ -5,7 +5,7 @@ export type GovernanceSnapshot = {
5
5
  policyProfile: PolicyProfile;
6
6
  maxUsd: number;
7
7
  softLimitUsd: number;
8
- maxTokens: number;
8
+ maxTokens?: number;
9
9
  maxIterations: number;
10
10
  allowedAdapters: string[];
11
11
  allowedModels: string[];
@@ -63,7 +63,7 @@ export interface LoopBudget {
63
63
  maxUsd: number;
64
64
  softLimitUsd: number;
65
65
  maxIterations: number;
66
- maxTokens: number;
66
+ maxTokens?: number;
67
67
  }
68
68
  export interface LoopCost {
69
69
  actualUsd: number;
@@ -27,8 +27,7 @@ export { MARTIN_ERROR_CATEGORIES } from "./operator.js";
27
27
  export const DEFAULT_BUDGET = {
28
28
  maxUsd: 25,
29
29
  softLimitUsd: 15,
30
- maxIterations: 8,
31
- maxTokens: 80_000
30
+ maxIterations: 8
32
31
  };
33
32
  export const EMPTY_COST = {
34
33
  actualUsd: 0,
@@ -1,4 +1,4 @@
1
- export declare const MARTIN_ERROR_CATEGORIES: readonly ["invalid_input", "environment", "auth", "not_found", "store_unreadable", "verification_failed", "policy_blocked", "budget_exit", "transient", "install_failed"];
1
+ export declare const MARTIN_ERROR_CATEGORIES: readonly ["invalid_input", "environment", "auth", "not_found", "store_unreadable", "verification_failed", "policy_blocked", "budget_exit", "transient"];
2
2
  export type MartinErrorCategory = (typeof MARTIN_ERROR_CATEGORIES)[number];
3
3
  export type MartinOutputMode = "human" | "json" | "quiet";
4
4
  export interface MartinRunSelector {
@@ -7,6 +7,5 @@ export const MARTIN_ERROR_CATEGORIES = [
7
7
  "verification_failed",
8
8
  "policy_blocked",
9
9
  "budget_exit",
10
- "transient",
11
- "install_failed"
10
+ "transient"
12
11
  ];
@@ -20,7 +20,7 @@ export interface CompilerAdapterRequest {
20
20
  focus: string;
21
21
  remainingBudgetUsd: number;
22
22
  remainingIterations: number;
23
- remainingTokens: number;
23
+ remainingTokens?: number;
24
24
  };
25
25
  previousAttempts: LoopAttempt[];
26
26
  }
@@ -41,7 +41,7 @@ export interface PromptPacket {
41
41
  budgetEnvelope: {
42
42
  remainingBudgetUsd: number;
43
43
  remainingIterations: number;
44
- remainingTokens: number;
44
+ remainingTokens?: number;
45
45
  };
46
46
  }
47
47
  /**
@@ -41,7 +41,9 @@ export function compilePromptPacket(request) {
41
41
  budgetEnvelope: {
42
42
  remainingBudgetUsd: request.context.remainingBudgetUsd,
43
43
  remainingIterations: request.context.remainingIterations,
44
- remainingTokens: request.context.remainingTokens
44
+ ...(request.context.remainingTokens !== undefined
45
+ ? { remainingTokens: request.context.remainingTokens }
46
+ : {})
45
47
  }
46
48
  };
47
49
  }
@@ -26,7 +26,10 @@ export function createDefaultExitPolicy(budget, overrides = {}) {
26
26
  schemaVersion: "exit-policy/1",
27
27
  goal: { verifierRequired: true, minimumScore: 1 },
28
28
  turns: { max: budget.maxIterations },
29
- budget: { maxUsd: budget.maxUsd, maxTokens: budget.maxTokens },
29
+ budget: {
30
+ maxUsd: budget.maxUsd,
31
+ ...(budget.maxTokens !== undefined ? { maxTokens: budget.maxTokens } : {})
32
+ },
30
33
  wallClock: { maxElapsedMs: 30 * 60 * 1000 },
31
34
  progress: { windowSize: 3, unchangedStateLimit: 3 },
32
35
  errors: { maxConsecutive: 3 },
@@ -49,7 +52,9 @@ export function createDefaultExitPolicy(budget, overrides = {}) {
49
52
  export function validateExitPolicy(policy) {
50
53
  assertFinitePositive("turns.max", policy.turns.max);
51
54
  assertFinitePositive("budget.maxUsd", policy.budget.maxUsd);
52
- assertFinitePositive("budget.maxTokens", policy.budget.maxTokens);
55
+ if (policy.budget.maxTokens !== undefined) {
56
+ assertFinitePositive("budget.maxTokens", policy.budget.maxTokens);
57
+ }
53
58
  assertFinitePositive("wallClock.maxElapsedMs", policy.wallClock.maxElapsedMs);
54
59
  assertFinitePositive("progress.windowSize", policy.progress.windowSize);
55
60
  assertFinitePositive("progress.unchangedStateLimit", policy.progress.unchangedStateLimit);
@@ -117,13 +122,13 @@ export function evaluateExitPolicy(policy, snapshot) {
117
122
  }
118
123
  // 5. Budget cap
119
124
  const usdExceeded = snapshot.actualUsd >= policy.budget.maxUsd;
120
- const tokensExceeded = snapshot.tokensUsed >= policy.budget.maxTokens;
125
+ const tokensExceeded = policy.budget.maxTokens !== undefined && snapshot.tokensUsed >= policy.budget.maxTokens;
121
126
  if (usdExceeded || tokensExceeded) {
122
127
  push("budget_cap", "Run token or dollar budget reached.", {
123
128
  actualUsd: snapshot.actualUsd,
124
129
  maxUsd: policy.budget.maxUsd,
125
130
  tokensUsed: snapshot.tokensUsed,
126
- maxTokens: policy.budget.maxTokens,
131
+ ...(policy.budget.maxTokens !== undefined ? { maxTokens: policy.budget.maxTokens } : {}),
127
132
  usdExceeded,
128
133
  tokensExceeded
129
134
  });
@@ -9,6 +9,7 @@ export interface RepoGroundingIndex {
9
9
  createdAt: string;
10
10
  fileCount: number;
11
11
  files: RepoGroundingFile[];
12
+ trackedPaths?: string[];
12
13
  repositoryFingerprint?: string;
13
14
  }
14
15
  export interface RepoGroundingHit {
@@ -1,7 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
2
3
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
5
  import { extname, join, relative } from "node:path";
6
+ import { promisify } from "node:util";
7
+ const execFileAsync = promisify(execFile);
5
8
  const TEXT_EXTENSIONS = new Set([
6
9
  ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
7
10
  ".json", ".md", ".yaml", ".yml", ".py", ".go",
@@ -42,21 +45,42 @@ export async function loadOrBuildRepoGroundingIndex(repoRoot) {
42
45
  export async function buildRepoGroundingIndex(repoRoot, repositoryFingerprint) {
43
46
  const files = [];
44
47
  const discovered = await walk(repoRoot, repoRoot, files, { count: 0 });
48
+ const trackedPaths = await loadTrackedPaths(repoRoot);
45
49
  return {
46
50
  schemaVersion: "martin.grounding.v1",
47
51
  repoRoot,
48
52
  createdAt: new Date().toISOString(),
49
53
  fileCount: discovered.count,
50
54
  files,
55
+ ...(trackedPaths.length > 0 ? { trackedPaths } : {}),
51
56
  repositoryFingerprint: repositoryFingerprint ?? await computeRepositoryFingerprint(repoRoot)
52
57
  };
53
58
  }
54
59
  async function computeRepositoryFingerprint(repoRoot) {
55
60
  const entries = [];
56
61
  await collectFingerprintEntries(repoRoot, repoRoot, entries, { count: 0 });
62
+ for (const trackedPath of await loadTrackedPaths(repoRoot)) {
63
+ entries.push(`tracked:${trackedPath}`);
64
+ }
57
65
  entries.sort();
58
66
  return createHash("sha256").update(entries.join("\n")).digest("hex");
59
67
  }
68
+ async function loadTrackedPaths(repoRoot) {
69
+ try {
70
+ const { stdout } = await execFileAsync("git", ["-C", repoRoot, "ls-files", "-z"], {
71
+ encoding: "utf8",
72
+ windowsHide: true
73
+ });
74
+ return stdout
75
+ .split("\0")
76
+ .map((filePath) => filePath.trim().replace(/\\/g, "/"))
77
+ .filter(Boolean)
78
+ .sort();
79
+ }
80
+ catch {
81
+ return [];
82
+ }
83
+ }
60
84
  async function collectFingerprintEntries(repoRoot, currentDir, entries, state) {
61
85
  if (state.count >= MAX_FILES)
62
86
  return;
@@ -193,7 +217,10 @@ function tokenize(input) {
193
217
  export function scanPatchForGroundingViolations(diff, index, options = {}) {
194
218
  const violations = [];
195
219
  const resolvedFiles = [];
196
- const indexedPaths = new Set(index.files.map((file) => file.path));
220
+ const indexedPaths = new Set([
221
+ ...index.files.map((file) => file.path),
222
+ ...(index.trackedPaths ?? [])
223
+ ]);
197
224
  const indexedSymbols = new Set(index.files.flatMap((file) => file.symbols.map((symbol) => symbol.toLowerCase())));
198
225
  const referencedFiles = new Set();
199
226
  const addedFiles = new Set();
@@ -75,7 +75,7 @@ export interface MartinAdapterRequest {
75
75
  focus: string;
76
76
  remainingBudgetUsd: number;
77
77
  remainingIterations: number;
78
- remainingTokens: number;
78
+ remainingTokens?: number;
79
79
  };
80
80
  previousAttempts: LoopAttempt[];
81
81
  /** Abort signal propagated from the harness — adapters should honour it. */
@@ -165,7 +165,7 @@ export interface DistilledContext {
165
165
  constraints: {
166
166
  remainingBudgetUsd: number;
167
167
  remainingIterations: number;
168
- remainingTokens: number;
168
+ remainingTokens?: number;
169
169
  };
170
170
  }
171
171
  export interface AttemptPolicyDecision {
@@ -1,5 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
- import { isAbsolute, relative, resolve } from "node:path";
3
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";
@@ -154,7 +152,9 @@ export function distillContext(loop, options = {}) {
154
152
  constraints: {
155
153
  remainingBudgetUsd: roundUsd(loop.budget.maxUsd - loop.cost.actualUsd),
156
154
  remainingIterations: Math.max(loop.budget.maxIterations - loop.attempts.length, 0),
157
- remainingTokens: Math.max(loop.budget.maxTokens - loop.cost.tokensIn - loop.cost.tokensOut, 0)
155
+ ...(loop.budget.maxTokens !== undefined
156
+ ? { remainingTokens: Math.max(loop.budget.maxTokens - loop.cost.tokensIn - loop.cost.tokensOut, 0) }
157
+ : {})
158
158
  }
159
159
  };
160
160
  }
@@ -796,6 +796,7 @@ export async function runMartin(input) {
796
796
  const attemptCompletedAt = now();
797
797
  const compiledContext = compilePromptPacket(request);
798
798
  const expectedVerifierBinding = resolveExpectedVerifierBinding(request);
799
+ const verifierConfigured = expectedVerifierBinding.commands.length > 0;
799
800
  const verification = normalizeVerificationOutcome(result, expectedVerifierBinding);
800
801
  // PATCH → VERIFY
801
802
  currentPhase = "VERIFY";
@@ -1135,17 +1136,12 @@ export async function runMartin(input) {
1135
1136
  // Uses the task's repoRoot to build/load the grounding index, then scans any diff
1136
1137
  let groundingScanResult;
1137
1138
  const patchDiff = buildPatchDiff(result, changedFiles);
1138
- const workspaceMutationCapable = executingAdapter.metadata.capabilities?.workspaceMutations !== false;
1139
- const hasReportedOrObservedChanges = changedFiles.length > 0 || (result.execution?.changedFiles?.length ?? 0) > 0;
1140
- const groundingEvidenceRequired = result.status === "completed" && workspaceMutationCapable && hasReportedOrObservedChanges;
1141
- let groundingEvidenceAvailable = !groundingEvidenceRequired;
1142
- if (groundingEvidenceRequired && patchDiff && input.task.repoRoot) {
1139
+ if (patchDiff && input.task.repoRoot) {
1143
1140
  try {
1144
1141
  const groundingIndex = await loadOrBuildRepoGroundingIndex(input.task.repoRoot);
1145
- groundingScanResult = reconcileExistingGroundingFiles(scanPatchForGroundingViolations(patchDiff, groundingIndex, {
1142
+ groundingScanResult = scanPatchForGroundingViolations(patchDiff, groundingIndex, {
1146
1143
  allowedPaths: input.task.allowedPaths
1147
- }), input.task.repoRoot, rollbackBoundary);
1148
- groundingEvidenceAvailable = true;
1144
+ });
1149
1145
  if (input.store && groundingScanResult.violations.length > 0) {
1150
1146
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1151
1147
  kind: "grounding.violations_found",
@@ -1161,22 +1157,10 @@ export async function runMartin(input) {
1161
1157
  }
1162
1158
  }
1163
1159
  catch {
1164
- // Availability is persisted below so scanner failures and missing
1165
- // grounding inputs follow the same recover-forward trust contract.
1160
+ // Grounding scan is best-effort — never fail the loop because of a scan error
1166
1161
  }
1167
1162
  }
1168
- if (groundingEvidenceRequired && !groundingEvidenceAvailable) {
1169
- // The completed work remains useful, but unavailable grounding evidence
1170
- // cannot be converted into a zero-violation governance claim.
1171
- loop = {
1172
- ...loop,
1173
- metadata: {
1174
- ...loop.metadata,
1175
- groundingEvidenceStatus: "unavailable"
1176
- }
1177
- };
1178
- }
1179
- const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations && groundingEvidenceAvailable;
1163
+ const shouldEvaluatePatchTruth = result.status === "completed" && tracksWorkspaceMutations && verifierConfigured;
1180
1164
  let patchDecision;
1181
1165
  if (shouldEvaluatePatchTruth) {
1182
1166
  patchDecision = evaluatePatchDecision({
@@ -1237,7 +1221,7 @@ export async function runMartin(input) {
1237
1221
  }
1238
1222
  else {
1239
1223
  await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1240
- kind: verification.passed ? "attempt.kept" : "attempt.discarded",
1224
+ kind: verification.passed || !verifierConfigured ? "attempt.kept" : "attempt.discarded",
1241
1225
  runId: loop.loopId,
1242
1226
  attemptIndex: currentAttemptIndex,
1243
1227
  payload: { reason: verification.summary }
@@ -1314,6 +1298,7 @@ export async function runMartin(input) {
1314
1298
  lastResult: effectiveResult,
1315
1299
  lastFailure: failure,
1316
1300
  costState,
1301
+ verificationRequired: verifierConfigured,
1317
1302
  canSwitchAdapter: failure?.recommendedIntervention === "switch_adapter" &&
1318
1303
  adapterChain[currentAdapterIndex] !== undefined &&
1319
1304
  currentAdapter.adapterId !== executingAdapter.adapterId
@@ -1578,9 +1563,6 @@ function mergeCostProvenance(previous, current) {
1578
1563
  return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
1579
1564
  }
1580
1565
  function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
1581
- if (repoRoot && rollbackBoundary?.headRef) {
1582
- return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
1583
- }
1584
1566
  if (result.execution?.changedFiles !== undefined) {
1585
1567
  return result.execution.changedFiles;
1586
1568
  }
@@ -1601,41 +1583,6 @@ function buildPatchDiff(result, changedFiles) {
1601
1583
  }
1602
1584
  return undefined;
1603
1585
  }
1604
- function reconcileExistingGroundingFiles(scan, repoRoot, boundary) {
1605
- const resolvedFiles = new Set(scan.resolvedFiles);
1606
- const violations = scan.violations.filter((violation) => {
1607
- if (violation.kind !== "file_not_found") {
1608
- return true;
1609
- }
1610
- const absolutePath = resolve(repoRoot, violation.reference);
1611
- const relativePath = relative(repoRoot, absolutePath);
1612
- const isInsideRepo = relativePath.length > 0 && !relativePath.startsWith("..") && !isAbsolute(relativePath);
1613
- if (!isInsideRepo || !isTrackedRepoFileAtBoundary(repoRoot, violation.reference, boundary)) {
1614
- return true;
1615
- }
1616
- resolvedFiles.add(violation.reference);
1617
- return false;
1618
- });
1619
- return {
1620
- ...scan,
1621
- violations,
1622
- resolvedFiles: [...resolvedFiles]
1623
- };
1624
- }
1625
- function isTrackedRepoFileAtBoundary(repoRoot, filePath, boundary) {
1626
- if (!boundary) {
1627
- return false;
1628
- }
1629
- const normalizedPath = filePath.replace(/\\/gu, "/");
1630
- if (boundary.snapshots.some((snapshot) => snapshot.existed && snapshot.path === normalizedPath)) {
1631
- return true;
1632
- }
1633
- if (!boundary.headRef) {
1634
- return false;
1635
- }
1636
- const result = spawnSync("git", ["-C", repoRoot, "cat-file", "-e", `${boundary.headRef}:${normalizedPath}`], { encoding: "utf8", stdio: "ignore", windowsHide: true });
1637
- return result.status === 0;
1638
- }
1639
1586
  function createBudgetSettlement(input) {
1640
1587
  const totalActualUsd = getUsageUsd(input.usage);
1641
1588
  return {