@martinloop/mcp 0.3.2 → 0.3.4

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 (64) hide show
  1. package/README.md +112 -107
  2. package/dist/discovery-support.js +2 -2
  3. package/dist/prompts.d.ts +2 -1
  4. package/dist/resources.d.ts +2 -1
  5. package/dist/resources.js +147 -147
  6. package/dist/server-validation.js +5 -2
  7. package/dist/server.js +5 -4
  8. package/dist/tools/eval.js +29 -5
  9. package/dist/tools/execution-policy.d.ts +11 -0
  10. package/dist/tools/execution-policy.js +47 -0
  11. package/dist/tools/run-dossier.js +2 -2
  12. package/dist/tools/run-loop.d.ts +2 -1
  13. package/dist/tools/run-loop.js +15 -2
  14. package/dist/tools/run-store.js +166 -6
  15. package/dist/tools/tool-support.d.ts +3 -2
  16. package/dist/tools/tool-support.js +112 -18
  17. package/dist/tools/triage-runs.js +31 -6
  18. package/dist/vendor/adapters/claude-cli.d.ts +1 -1
  19. package/dist/vendor/adapters/claude-cli.js +71 -20
  20. package/dist/vendor/adapters/cli-bridge.d.ts +6 -0
  21. package/dist/vendor/adapters/cli-bridge.js +91 -7
  22. package/dist/vendor/adapters/codex-launcher.js +65 -8
  23. package/dist/vendor/adapters/openai-compatible.d.ts +1 -1
  24. package/dist/vendor/adapters/openai-compatible.js +7 -7
  25. package/dist/vendor/adapters/verifier-only.js +1 -1
  26. package/dist/vendor/contracts/adapter-capabilities.d.ts +17 -0
  27. package/dist/vendor/contracts/adapter-capabilities.js +35 -0
  28. package/dist/vendor/contracts/context-graph.d.ts +48 -0
  29. package/dist/vendor/contracts/context-graph.js +26 -0
  30. package/dist/vendor/contracts/execution-policy.d.ts +62 -0
  31. package/dist/vendor/contracts/execution-policy.js +34 -0
  32. package/dist/vendor/contracts/governance.d.ts +6 -3
  33. package/dist/vendor/contracts/identity.d.ts +29 -0
  34. package/dist/vendor/contracts/identity.js +30 -0
  35. package/dist/vendor/contracts/index.d.ts +13 -2
  36. package/dist/vendor/contracts/index.js +4 -0
  37. package/dist/vendor/contracts/observation.d.ts +21 -0
  38. package/dist/vendor/contracts/observation.js +27 -0
  39. package/dist/vendor/contracts/trajectory.d.ts +26 -0
  40. package/dist/vendor/contracts/trajectory.js +27 -0
  41. package/dist/vendor/contracts/verification.d.ts +28 -0
  42. package/dist/vendor/contracts/verification.js +7 -0
  43. package/dist/vendor/core/context-graph.d.ts +3 -0
  44. package/dist/vendor/core/context-graph.js +371 -0
  45. package/dist/vendor/core/context-integrity.js +1 -1
  46. package/dist/vendor/core/grounding.js +1 -1
  47. package/dist/vendor/core/identity.d.ts +20 -0
  48. package/dist/vendor/core/identity.js +86 -0
  49. package/dist/vendor/core/index.d.ts +7 -0
  50. package/dist/vendor/core/index.js +63 -9
  51. package/dist/vendor/core/leash.js +1 -1
  52. package/dist/vendor/core/persistence/integrity.d.ts +1 -0
  53. package/dist/vendor/core/persistence/integrity.js +7 -3
  54. package/dist/vendor/core/persistence/runs-reader.d.ts +14 -0
  55. package/dist/vendor/core/persistence/runs-reader.js +61 -4
  56. package/dist/vendor/core/policy-compiler.d.ts +2 -0
  57. package/dist/vendor/core/policy-compiler.js +195 -0
  58. package/dist/vendor/core/policy.d.ts +2 -0
  59. package/dist/vendor/core/policy.js +71 -4
  60. package/dist/vendor/core/rollback.js +32 -12
  61. package/dist/vendor/core/trajectory.d.ts +18 -0
  62. package/dist/vendor/core/trajectory.js +295 -0
  63. package/package.json +76 -75
  64. package/server.json +21 -21
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, realpathSync } from "node:fs";
2
2
  import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { resolveRunsRoot } from "./vendor/core/index.js";
4
4
  import { invalidArgumentsError, invalidPathError, invalidSelectorError } from "./tools/tool-errors.js";
5
+ import { MARTIN_ENGINE_VALUES } from "./tools/tool-support.js";
5
6
  export { sanitizeToolErrorMessage } from "./tools/tool-errors.js";
6
7
  export function validateToolInput(name, args) {
7
8
  switch (name) {
@@ -147,6 +148,7 @@ function validateRunInput(args) {
147
148
  "maxUsd",
148
149
  "maxIterations",
149
150
  "maxTokens",
151
+ "verifyTimeoutMs",
150
152
  "maxMinutes",
151
153
  "maxFilesChanged",
152
154
  "maxCommands",
@@ -156,7 +158,7 @@ function validateRunInput(args) {
156
158
  "workspaceId",
157
159
  "projectId"
158
160
  ]);
159
- const engine = optionalEnum(record.engine, "engine", ["claude", "codex", "gemini"]);
161
+ const engine = optionalEnum(record.engine, "engine", MARTIN_ENGINE_VALUES);
160
162
  return {
161
163
  objective: requireString(record.objective, "objective"),
162
164
  ...(record.workingDirectory !== undefined
@@ -175,6 +177,7 @@ function validateRunInput(args) {
175
177
  ...optionalPositiveNumber(record.maxUsd, "maxUsd"),
176
178
  ...optionalPositiveInteger(record.maxIterations, "maxIterations"),
177
179
  ...optionalPositiveInteger(record.maxTokens, "maxTokens"),
180
+ ...optionalPositiveInteger(record.verifyTimeoutMs, "verifyTimeoutMs"),
178
181
  ...optionalPositiveInteger(record.maxMinutes, "maxMinutes"),
179
182
  ...optionalPositiveInteger(record.maxFilesChanged, "maxFilesChanged"),
180
183
  ...optionalPositiveInteger(record.maxCommands, "maxCommands"),
@@ -246,7 +249,7 @@ function validateDoctorInput(args) {
246
249
  ...(record.runsDir !== undefined
247
250
  ? { runsDir: resolveSafeRunsRootPath(requireString(record.runsDir, "runsDir")) }
248
251
  : {}),
249
- ...optionalEnumAsObject(record.engine, "engine", ["claude", "codex", "gemini"])
252
+ ...optionalEnumAsObject(record.engine, "engine", MARTIN_ENGINE_VALUES)
250
253
  };
251
254
  }
252
255
  function validatePreflightInput(args) {
package/dist/server.js CHANGED
@@ -42,6 +42,7 @@ import { martinRunDossierTool } from "./tools/run-dossier.js";
42
42
  import { createRunControlReceipt } from "./tools/run-controls.js";
43
43
  import { martinTriageRunsTool } from "./tools/triage-runs.js";
44
44
  import { runLoopTool } from "./tools/run-loop.js";
45
+ import { MARTIN_ENGINE_VALUES } from "./tools/tool-support.js";
45
46
  import { createToolErrorResult, createToolSuccessResult } from "./tools/tool-response.js";
46
47
  import { MartinToolError, toToolFailure } from "./tools/tool-errors.js";
47
48
  import { normalizeLoopBudget } from "./tools/workflow-governance.js";
@@ -117,7 +118,7 @@ const verificationSchema = {
117
118
  type: "object",
118
119
  additionalProperties: true,
119
120
  properties: {
120
- status: { type: "string", enum: ["passed", "failed", "unavailable"] },
121
+ status: { type: "string", enum: ["passed", "failed", "contradicted", "not_run"] },
121
122
  eventCount: { type: "integer" },
122
123
  ledgerEventCount: { type: "integer" },
123
124
  latestAttemptIndex: { type: "integer" },
@@ -739,7 +740,7 @@ export function createMartinMcpServer(serverInfo) {
739
740
  },
740
741
  engine: {
741
742
  type: "string",
742
- enum: ["claude", "codex", "gemini"],
743
+ enum: [...MARTIN_ENGINE_VALUES],
743
744
  description: "Which agent CLI to use. Defaults to claude."
744
745
  },
745
746
  model: {
@@ -871,7 +872,7 @@ export function createMartinMcpServer(serverInfo) {
871
872
  },
872
873
  engine: {
873
874
  type: "string",
874
- enum: ["claude", "codex", "gemini"],
875
+ enum: [...MARTIN_ENGINE_VALUES],
875
876
  description: "Optional engine to highlight in diagnostics."
876
877
  }
877
878
  }
@@ -934,7 +935,7 @@ export function createMartinMcpServer(serverInfo) {
934
935
  },
935
936
  engine: {
936
937
  type: "string",
937
- enum: ["claude", "codex", "gemini"],
938
+ enum: [...MARTIN_ENGINE_VALUES],
938
939
  description: "Which agent CLI would be used. Defaults to claude."
939
940
  },
940
941
  model: {
@@ -15,15 +15,20 @@ export async function martinEvalTool(input) {
15
15
  verifiers: detail.loop.task?.verificationPlan ?? [],
16
16
  signals
17
17
  });
18
+ const observationMismatch = hasObservationMismatch(detail.loop.events ?? []);
18
19
  const checks = {
19
20
  taskCompletion: detail.loop.status === "completed" ? "passed" : detail.loop.status === "exited" ? "warning" : "failed",
20
21
  verifier: verification.status === "passed"
21
22
  ? "passed"
22
- : verification.status === "failed"
23
+ : verification.status === "failed" || verification.status === "contradicted"
23
24
  ? "failed"
24
25
  : "warning",
25
- diffDiscipline: (detail.loop.task?.allowedPaths?.length ?? 0) > 0 ? "passed" : "warning",
26
- regressionRisk: verification.status === "passed" ? "passed" : "warning",
26
+ diffDiscipline: observationMismatch
27
+ ? "failed"
28
+ : (detail.loop.task?.allowedPaths?.length ?? 0) > 0
29
+ ? "passed"
30
+ : "warning",
31
+ regressionRisk: verification.status === "passed" && !observationMismatch ? "passed" : "warning",
27
32
  securityRisk: risk.level === "high" ? "failed" : risk.level === "medium" ? "warning" : "passed",
28
33
  reviewability: detail.loop.attempts.length > 0 && (detail.loop.events?.length ?? 0) > 0 ? "passed" : "warning"
29
34
  };
@@ -35,7 +40,7 @@ export async function martinEvalTool(input) {
35
40
  score -= checks.securityRisk === "failed" ? 20 : checks.securityRisk === "warning" ? 10 : 0;
36
41
  score -= checks.reviewability === "warning" ? 8 : 0;
37
42
  score = Math.max(0, score);
38
- const grade = verification.status === "unavailable"
43
+ const grade = verification.status === "not_run" || observationMismatch
39
44
  ? "insufficient_evidence"
40
45
  : score >= 90
41
46
  ? "mergeable"
@@ -44,7 +49,14 @@ export async function martinEvalTool(input) {
44
49
  : score >= 55
45
50
  ? "needs_review"
46
51
  : "blocked";
47
- const warnings = [...detail.warnings, ...verification.warnings, ...risk.reasons];
52
+ const warnings = [
53
+ ...detail.warnings,
54
+ ...verification.warnings,
55
+ ...risk.reasons,
56
+ ...(observationMismatch
57
+ ? ["Change observation mismatch: adapter-reported files differ from repo-observed files."]
58
+ : [])
59
+ ];
48
60
  return {
49
61
  source: detail.source,
50
62
  sourceKind: detail.sourceKind,
@@ -64,3 +76,15 @@ export async function martinEvalTool(input) {
64
76
  : `Run ${detail.loop.loopId} is blocked from promotion by verification or risk gaps.`
65
77
  };
66
78
  }
79
+ function hasObservationMismatch(events) {
80
+ return events.some((event) => {
81
+ if (event.type !== "observation.reconciled") {
82
+ return false;
83
+ }
84
+ const observation = isRecord(event.payload?.["observation"]) ? event.payload?.["observation"] : undefined;
85
+ return observation?.["status"] === "mismatch";
86
+ });
87
+ }
88
+ function isRecord(value) {
89
+ return typeof value === "object" && value !== null;
90
+ }
@@ -0,0 +1,11 @@
1
+ import { type ExecutionPolicy } from "../vendor/contracts/index.js";
2
+ export interface CompileMcpExecutionPolicyInput {
3
+ workingDirectory: string;
4
+ maxUsd?: number;
5
+ maxIterations?: number;
6
+ maxTokens?: number;
7
+ verificationPlan?: string[];
8
+ allowedPaths?: string[];
9
+ deniedPaths?: string[];
10
+ }
11
+ export declare function compileMcpExecutionPolicy(input: CompileMcpExecutionPolicyInput): ExecutionPolicy;
@@ -0,0 +1,47 @@
1
+ import { DEFAULT_BUDGET } from "../vendor/contracts/index.js";
2
+ import { compileExecutionPolicy } from "../vendor/core/index.js";
3
+ export function compileMcpExecutionPolicy(input) {
4
+ const normalizedBudget = {
5
+ maxUsd: input.maxUsd ?? DEFAULT_BUDGET.maxUsd,
6
+ softLimitUsd: Math.min(DEFAULT_BUDGET.softLimitUsd, input.maxUsd ?? DEFAULT_BUDGET.maxUsd),
7
+ maxIterations: input.maxIterations ?? DEFAULT_BUDGET.maxIterations,
8
+ maxTokens: input.maxTokens ?? DEFAULT_BUDGET.maxTokens
9
+ };
10
+ const policy = compileExecutionPolicy({
11
+ configPath: "mcp://inline",
12
+ defaults: {
13
+ budget: DEFAULT_BUDGET,
14
+ policyProfile: "balanced",
15
+ telemetryDestination: "local-only",
16
+ destructiveActionPolicy: "approval",
17
+ verifierRules: []
18
+ },
19
+ request: {
20
+ budget: normalizedBudget,
21
+ budgetOverrides: {
22
+ ...(input.maxUsd !== undefined ? { maxUsd: true } : {}),
23
+ ...(input.maxIterations !== undefined ? { maxIterations: true } : {}),
24
+ ...(input.maxTokens !== undefined ? { maxTokens: true } : {})
25
+ },
26
+ verificationPlan: input.verificationPlan,
27
+ repoRoot: input.workingDirectory,
28
+ allowedPaths: input.allowedPaths,
29
+ deniedPaths: input.deniedPaths
30
+ }
31
+ });
32
+ const softLimitSource = input.maxUsd !== undefined && normalizedBudget.softLimitUsd !== DEFAULT_BUDGET.softLimitUsd
33
+ ? "request"
34
+ : "default";
35
+ return {
36
+ ...policy,
37
+ budget: normalizedBudget,
38
+ provenance: [
39
+ ...policy.provenance.filter((entry) => entry.field !== "budget.softLimitUsd"),
40
+ {
41
+ field: "budget.softLimitUsd",
42
+ source: softLimitSource,
43
+ value: String(normalizedBudget.softLimitUsd)
44
+ }
45
+ ]
46
+ };
47
+ }
@@ -37,12 +37,12 @@ export async function martinRunDossierTool(input) {
37
37
  risk,
38
38
  outcome: verification.status === "passed"
39
39
  ? "passed"
40
- : verification.status === "failed"
40
+ : verification.status === "failed" || verification.status === "contradicted"
41
41
  ? "failed"
42
42
  : "needs_review",
43
43
  nextAction: verification.status === "passed"
44
44
  ? "Review the dossier and evaluation, then decide whether to merge or promote."
45
- : verification.status === "failed"
45
+ : verification.status === "failed" || verification.status === "contradicted"
46
46
  ? "Investigate the latest verifier failure before retrying or promoting."
47
47
  : "Collect more evidence before claiming completion."
48
48
  };
@@ -5,11 +5,12 @@ import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, type
5
5
  export interface RunLoopInput {
6
6
  objective: string;
7
7
  workingDirectory?: string;
8
- engine?: "claude" | "codex" | "gemini";
8
+ engine?: MartinEngine;
9
9
  model?: string;
10
10
  maxUsd?: number;
11
11
  maxIterations?: number;
12
12
  maxTokens?: number;
13
+ verifyTimeoutMs?: number;
13
14
  verificationPlan?: string[];
14
15
  allowedPaths?: string[];
15
16
  deniedPaths?: string[];
@@ -66,17 +66,27 @@ export async function runLoopTool(input) {
66
66
  ? createVerifierOnlyAdapter({
67
67
  workingDirectory,
68
68
  label: "Proof mode adapter (MARTIN_LIVE=false)",
69
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
69
70
  ...(proofModeVerifierSpawnImpl ? { spawnImpl: proofModeVerifierSpawnImpl } : {})
70
71
  })
71
72
  : engine === "codex"
72
73
  ? createCodexCliAdapter({
73
74
  workingDirectory,
75
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
74
76
  ...(model ? { model } : {}),
75
77
  ...(codexCommandOverride ? { command: codexCommandOverride } : {})
76
78
  })
77
79
  : engine === "gemini"
78
- ? createGeminiCliAdapter({ workingDirectory, ...(model ? { model } : {}) })
79
- : createClaudeCliAdapter({ workingDirectory, ...(model ? { model } : {}) });
80
+ ? createGeminiCliAdapter({
81
+ workingDirectory,
82
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
83
+ ...(model ? { model } : {})
84
+ })
85
+ : createClaudeCliAdapter({
86
+ workingDirectory,
87
+ ...(input.verifyTimeoutMs !== undefined ? { verifyTimeoutMs: input.verifyTimeoutMs } : {}),
88
+ ...(model ? { model } : {})
89
+ });
80
90
  const partialBudget = {};
81
91
  if (input.maxUsd !== undefined) {
82
92
  partialBudget.maxUsd = input.maxUsd;
@@ -97,6 +107,9 @@ export async function runLoopTool(input) {
97
107
  title: input.objective.slice(0, 100),
98
108
  objective: input.objective,
99
109
  verificationPlan: input.verificationPlan ?? [],
110
+ ...(input.verifyTimeoutMs !== undefined
111
+ ? { verificationTimeoutMs: input.verifyTimeoutMs }
112
+ : {}),
100
113
  repoRoot: workingDirectory,
101
114
  ...(allowedPaths ? { allowedPaths } : {}),
102
115
  ...(deniedPaths ? { deniedPaths } : {})
@@ -1,8 +1,10 @@
1
- import { readFile, readdir, stat } from "node:fs/promises";
1
+ import { open, readFile, readdir, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, verifyReceiptIntegrityFromFiles } from "../vendor/core/index.js";
4
4
  import { resolveSafeLoopRecordPath, resolveSafeRunsJsonPath, resolveSafeRunsPath, resolveSafeRunsRootPath } from "../server-validation.js";
5
5
  import { attemptNotFoundError, invalidSelectorError, noLoopRecordsError, storeUnreadableError } from "./tool-errors.js";
6
+ const RUN_INDEX_FILENAME = "run-index.ndjson";
7
+ const RUN_INDEX_READ_MAX_BYTES = 2 * 1024 * 1024;
6
8
  async function attachReceiptIntegrity(detail) {
7
9
  const ledgerPath = detail.canonicalRunDirectory
8
10
  ? await resolveReceiptEvidencePath(detail.canonicalRunDirectory)
@@ -14,13 +16,18 @@ async function attachReceiptIntegrity(detail) {
14
16
  loopRecordPath: detail.canonicalLoopRecordPath,
15
17
  ledgerPath
16
18
  }).catch(() => ({
17
- state: "unsigned",
19
+ state: "material_missing",
18
20
  reason: "Receipt integrity verification could not be completed."
19
21
  }))
20
- : ({
21
- state: "unsigned",
22
- reason: "Receipt integrity is only available for canonical run directories."
23
- });
22
+ : detail.canonicalLoopRecordPath && detail.canonicalRunDirectory
23
+ ? ({
24
+ state: "material_missing",
25
+ reason: "Receipt integrity material is incomplete for this canonical run."
26
+ })
27
+ : ({
28
+ state: "selector_noncanonical",
29
+ reason: "Receipt integrity is only available for canonical run selectors."
30
+ });
24
31
  const receiptScope = resolveReceiptScope(detail.loop, detail.runsRoot);
25
32
  return {
26
33
  ...detail,
@@ -107,6 +114,20 @@ export async function loadLoopRecordForStatus(input) {
107
114
  }
108
115
  export async function listLoopRecords(input) {
109
116
  const runsRoot = resolveSafeRunsRootPath(input.runsDir, resolveRunsRoot(process.env));
117
+ const indexed = await listLoopsFromRunIndex(runsRoot, input);
118
+ if (indexed.loops.length > 0) {
119
+ const loops = indexed.loops;
120
+ const warnings = [...indexed.warnings];
121
+ if (loops.length === 0) {
122
+ warnings.push("No loop records matched the current filters.");
123
+ }
124
+ return {
125
+ source: runsRoot,
126
+ runsRoot,
127
+ loops,
128
+ warnings
129
+ };
130
+ }
110
131
  const inspected = await readAllLoopRecordsSafely(runsRoot);
111
132
  const warnings = [...inspected.warnings];
112
133
  const updatedAfterTimestamp = input.updatedAfter !== undefined ? new Date(input.updatedAfter).getTime() : undefined;
@@ -248,6 +269,15 @@ export async function loadDetailedLoopRecord(input) {
248
269
  warnings: [...detail.warnings, ...inspected.warnings]
249
270
  });
250
271
  }
272
+ const indexedLatest = await loadLatestLoopFromRunIndex(runsRoot);
273
+ if (indexedLatest) {
274
+ return await attachReceiptIntegrity(await buildDetailedLoopSourceFromDiscoveredLoop({
275
+ source: runsRoot,
276
+ sourceKind: "latest",
277
+ runsRoot,
278
+ loop: indexedLatest.loop
279
+ }));
280
+ }
251
281
  const inspected = await readAllLoopRecordsSafely(runsRoot);
252
282
  const loop = inspected.loops[0];
253
283
  if (!loop) {
@@ -264,6 +294,129 @@ export async function loadDetailedLoopRecord(input) {
264
294
  warnings: [...detail.warnings, ...inspected.warnings]
265
295
  });
266
296
  }
297
+ async function listLoopsFromRunIndex(runsRoot, input) {
298
+ const indexed = await readRunIndexEntries(runsRoot);
299
+ if (indexed.entries.length === 0) {
300
+ return { loops: [], warnings: [] };
301
+ }
302
+ const warnings = [];
303
+ const updatedAfterTimestamp = input.updatedAfter !== undefined ? new Date(input.updatedAfter).getTime() : undefined;
304
+ if (input.updatedAfter !== undefined &&
305
+ (!Number.isFinite(updatedAfterTimestamp) || Number.isNaN(updatedAfterTimestamp))) {
306
+ throw invalidSelectorError("Invalid updatedAfter.", "Provide updatedAfter as an ISO-8601 timestamp.");
307
+ }
308
+ const deduped = dedupeRunIndexEntries(indexed.entries);
309
+ const loops = [];
310
+ const limit = input.limit ?? 20;
311
+ const maxLookups = Math.max(limit * 5, 100);
312
+ for (const entry of deduped) {
313
+ if (loops.length >= limit || loops.length >= maxLookups) {
314
+ break;
315
+ }
316
+ if (input.status && entry.status !== input.status) {
317
+ continue;
318
+ }
319
+ if (input.lifecycleState && entry.lifecycleState !== input.lifecycleState) {
320
+ continue;
321
+ }
322
+ if (updatedAfterTimestamp !== undefined) {
323
+ const timestamp = Date.parse(entry.updatedAt);
324
+ if (!Number.isFinite(timestamp) || timestamp <= updatedAfterTimestamp) {
325
+ continue;
326
+ }
327
+ }
328
+ const loop = await loadLoopFromIndexEntry(runsRoot, entry.loopId).catch(() => null);
329
+ if (!loop) {
330
+ continue;
331
+ }
332
+ if (input.adapterId && !loop.attempts.some((attempt) => attempt.adapterId === input.adapterId)) {
333
+ continue;
334
+ }
335
+ if (input.model && !loop.attempts.some((attempt) => attempt.model === input.model)) {
336
+ continue;
337
+ }
338
+ loops.push(loop);
339
+ }
340
+ return {
341
+ loops: loops
342
+ .sort((left, right) => timestampForLoop(right) - timestampForLoop(left))
343
+ .slice(0, limit),
344
+ warnings
345
+ };
346
+ }
347
+ async function loadLatestLoopFromRunIndex(runsRoot) {
348
+ const indexed = await readRunIndexEntries(runsRoot);
349
+ if (indexed.entries.length === 0) {
350
+ return undefined;
351
+ }
352
+ for (const entry of dedupeRunIndexEntries(indexed.entries)) {
353
+ const loop = await loadLoopFromIndexEntry(runsRoot, entry.loopId).catch(() => null);
354
+ if (loop) {
355
+ return { loop };
356
+ }
357
+ }
358
+ return undefined;
359
+ }
360
+ async function loadLoopFromIndexEntry(runsRoot, loopId) {
361
+ const canonicalLoopRecordPath = resolvePotentialLoopRecordPath(loopId, runsRoot);
362
+ const canonicalStats = await safeStat(canonicalLoopRecordPath);
363
+ if (!canonicalStats?.isFile()) {
364
+ throw noLoopRecordsError();
365
+ }
366
+ return await readCanonicalLoopRecord(canonicalLoopRecordPath);
367
+ }
368
+ async function readRunIndexEntries(runsRoot) {
369
+ const indexPath = path.join(runsRoot, RUN_INDEX_FILENAME);
370
+ const stats = await safeStat(indexPath);
371
+ if (!stats?.isFile()) {
372
+ return { entries: [], truncated: false };
373
+ }
374
+ const size = Number(stats.size);
375
+ const readBytes = Math.min(size, RUN_INDEX_READ_MAX_BYTES);
376
+ const start = Math.max(0, size - readBytes);
377
+ const handle = await open(indexPath, "r");
378
+ try {
379
+ const buffer = Buffer.alloc(readBytes);
380
+ if (readBytes > 0) {
381
+ await handle.read(buffer, 0, readBytes, start);
382
+ }
383
+ const lines = buffer
384
+ .toString("utf8")
385
+ .split(/\r?\n/u)
386
+ .map((line) => line.trim())
387
+ .filter(Boolean);
388
+ const entries = [];
389
+ for (const line of lines) {
390
+ try {
391
+ const parsed = JSON.parse(line);
392
+ if (isRunIndexEntry(parsed)) {
393
+ entries.push(parsed);
394
+ }
395
+ }
396
+ catch {
397
+ // Ignore partial tail lines.
398
+ }
399
+ }
400
+ entries.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
401
+ return {
402
+ entries,
403
+ truncated: size > RUN_INDEX_READ_MAX_BYTES
404
+ };
405
+ }
406
+ finally {
407
+ await handle.close();
408
+ }
409
+ }
410
+ function dedupeRunIndexEntries(entries) {
411
+ const byLoopId = new Map();
412
+ for (const entry of entries) {
413
+ const existing = byLoopId.get(entry.loopId);
414
+ if (!existing || Date.parse(entry.updatedAt) >= Date.parse(existing.updatedAt)) {
415
+ byLoopId.set(entry.loopId, entry);
416
+ }
417
+ }
418
+ return [...byLoopId.values()].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
419
+ }
267
420
  export async function loadAttemptFromLoop(input) {
268
421
  const detail = await loadDetailedLoopRecord(input);
269
422
  const attempt = input.attemptIndex !== undefined
@@ -510,3 +663,10 @@ function isRecord(value) {
510
663
  function isFiniteNumber(value) {
511
664
  return typeof value === "number" && Number.isFinite(value);
512
665
  }
666
+ function isRunIndexEntry(value) {
667
+ return (isRecord(value) &&
668
+ typeof value["loopId"] === "string" &&
669
+ value["loopId"].trim().length > 0 &&
670
+ typeof value["updatedAt"] === "string" &&
671
+ value["updatedAt"].trim().length > 0);
672
+ }
@@ -1,6 +1,7 @@
1
1
  import type { LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope } from "../vendor/contracts/index.js";
2
2
  import { type LedgerEvent, type LoopAttemptRecord, type LoopRunRecord } from "../vendor/core/index.js";
3
- export type MartinEngine = "claude" | "codex" | "gemini";
3
+ export declare const MARTIN_ENGINE_VALUES: readonly ["claude", "codex", "gemini"];
4
+ export type MartinEngine = (typeof MARTIN_ENGINE_VALUES)[number];
4
5
  export interface InspectableLoopAttempt extends LoopAttemptRecord {
5
6
  attemptId?: string;
6
7
  summary?: string;
@@ -60,7 +61,7 @@ export interface ArtifactSummary {
60
61
  }>;
61
62
  }
62
63
  export interface VerificationSummary {
63
- status: "passed" | "failed" | "unavailable";
64
+ status: "passed" | "failed" | "contradicted" | "not_run";
64
65
  eventCount: number;
65
66
  ledgerEventCount: number;
66
67
  latestAttemptIndex?: number;