@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.
- package/README.md +112 -107
- package/dist/discovery-support.js +2 -2
- package/dist/prompts.d.ts +2 -1
- package/dist/resources.d.ts +2 -1
- package/dist/resources.js +147 -147
- package/dist/server-validation.js +5 -2
- package/dist/server.js +5 -4
- package/dist/tools/eval.js +29 -5
- package/dist/tools/execution-policy.d.ts +11 -0
- package/dist/tools/execution-policy.js +47 -0
- package/dist/tools/run-dossier.js +2 -2
- package/dist/tools/run-loop.d.ts +2 -1
- package/dist/tools/run-loop.js +15 -2
- package/dist/tools/run-store.js +166 -6
- package/dist/tools/tool-support.d.ts +3 -2
- package/dist/tools/tool-support.js +112 -18
- package/dist/tools/triage-runs.js +31 -6
- package/dist/vendor/adapters/claude-cli.d.ts +1 -1
- package/dist/vendor/adapters/claude-cli.js +71 -20
- package/dist/vendor/adapters/cli-bridge.d.ts +6 -0
- package/dist/vendor/adapters/cli-bridge.js +91 -7
- package/dist/vendor/adapters/codex-launcher.js +65 -8
- package/dist/vendor/adapters/openai-compatible.d.ts +1 -1
- package/dist/vendor/adapters/openai-compatible.js +7 -7
- package/dist/vendor/adapters/verifier-only.js +1 -1
- package/dist/vendor/contracts/adapter-capabilities.d.ts +17 -0
- package/dist/vendor/contracts/adapter-capabilities.js +35 -0
- package/dist/vendor/contracts/context-graph.d.ts +48 -0
- package/dist/vendor/contracts/context-graph.js +26 -0
- package/dist/vendor/contracts/execution-policy.d.ts +62 -0
- package/dist/vendor/contracts/execution-policy.js +34 -0
- package/dist/vendor/contracts/governance.d.ts +6 -3
- package/dist/vendor/contracts/identity.d.ts +29 -0
- package/dist/vendor/contracts/identity.js +30 -0
- package/dist/vendor/contracts/index.d.ts +13 -2
- package/dist/vendor/contracts/index.js +4 -0
- package/dist/vendor/contracts/observation.d.ts +21 -0
- package/dist/vendor/contracts/observation.js +27 -0
- package/dist/vendor/contracts/trajectory.d.ts +26 -0
- package/dist/vendor/contracts/trajectory.js +27 -0
- package/dist/vendor/contracts/verification.d.ts +28 -0
- package/dist/vendor/contracts/verification.js +7 -0
- package/dist/vendor/core/context-graph.d.ts +3 -0
- package/dist/vendor/core/context-graph.js +371 -0
- package/dist/vendor/core/context-integrity.js +1 -1
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/identity.d.ts +20 -0
- package/dist/vendor/core/identity.js +86 -0
- package/dist/vendor/core/index.d.ts +7 -0
- package/dist/vendor/core/index.js +63 -9
- package/dist/vendor/core/leash.js +1 -1
- package/dist/vendor/core/persistence/integrity.d.ts +1 -0
- package/dist/vendor/core/persistence/integrity.js +7 -3
- package/dist/vendor/core/persistence/runs-reader.d.ts +14 -0
- package/dist/vendor/core/persistence/runs-reader.js +61 -4
- package/dist/vendor/core/policy-compiler.d.ts +2 -0
- package/dist/vendor/core/policy-compiler.js +195 -0
- package/dist/vendor/core/policy.d.ts +2 -0
- package/dist/vendor/core/policy.js +71 -4
- package/dist/vendor/core/rollback.js +32 -12
- package/dist/vendor/core/trajectory.d.ts +18 -0
- package/dist/vendor/core/trajectory.js +295 -0
- package/package.json +76 -75
- 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",
|
|
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",
|
|
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", "
|
|
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: [
|
|
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: [
|
|
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: [
|
|
938
|
+
enum: [...MARTIN_ENGINE_VALUES],
|
|
938
939
|
description: "Which agent CLI would be used. Defaults to claude."
|
|
939
940
|
},
|
|
940
941
|
model: {
|
package/dist/tools/eval.js
CHANGED
|
@@ -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:
|
|
26
|
-
|
|
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 === "
|
|
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 = [
|
|
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
|
};
|
package/dist/tools/run-loop.d.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, type
|
|
|
5
5
|
export interface RunLoopInput {
|
|
6
6
|
objective: string;
|
|
7
7
|
workingDirectory?: string;
|
|
8
|
-
engine?:
|
|
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[];
|
package/dist/tools/run-loop.js
CHANGED
|
@@ -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({
|
|
79
|
-
|
|
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 } : {})
|
package/dist/tools/run-store.js
CHANGED
|
@@ -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: "
|
|
19
|
+
state: "material_missing",
|
|
18
20
|
reason: "Receipt integrity verification could not be completed."
|
|
19
21
|
}))
|
|
20
|
-
:
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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" | "
|
|
64
|
+
status: "passed" | "failed" | "contradicted" | "not_run";
|
|
64
65
|
eventCount: number;
|
|
65
66
|
ledgerEventCount: number;
|
|
66
67
|
latestAttemptIndex?: number;
|