@martinloop/mcp 0.8.2 → 0.8.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/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/tools/run-dossier.js +60 -48
- package/dist/tools/tool-support.d.ts +16 -2
- package/dist/tools/tool-support.js +144 -6
- package/dist/vendor/adapters/claude-cli.d.ts +2 -0
- package/dist/vendor/adapters/claude-cli.js +16 -7
- package/dist/vendor/adapters/codex-cli.js +1 -0
- package/dist/vendor/contracts/index.d.ts +1 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +21 -0
- package/dist/vendor/core/external-outcome.js +6 -1
- package/dist/vendor/core/index.d.ts +1 -1
- package/dist/vendor/core/index.js +158 -50
- package/dist/vendor/core/verified-handoff.d.ts +6 -1
- package/dist/vendor/core/verified-handoff.js +113 -7
- package/dist/vendor/presentation/verified-handoff.js +64 -12
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.
|
|
1
|
+
export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.4";
|
package/dist/package-version.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { buildArtifactSummary, buildBudgetSnapshot, buildCostSnapshot, buildEventSummaries, buildLoopPreview, resolveReceiptIntegrity, buildSuggestedPromptNames, buildSuggestedResourceUris, buildVerificationSummary } from "./tool-support.js";
|
|
1
|
+
import { buildArtifactSummary, buildBudgetSnapshot, buildCostSnapshot, buildEventSummaries, buildFileEffectsSummary, buildLoopPreview, resolveReceiptIntegrity, buildSuggestedPromptNames, buildSuggestedResourceUris, buildVerificationSummary } from "./tool-support.js";
|
|
2
2
|
import { resolveTrustedLoopRepoRoot } from "../server-validation.js";
|
|
3
3
|
import { loadDetailedLoopRecord, readAttemptArtifactFiles, readLedgerEvents } from "./run-store.js";
|
|
4
4
|
import { readRunControlState } from "./run-controls.js";
|
|
5
5
|
import { martinEvalTool } from "./eval.js";
|
|
6
6
|
import { assessRunRisk, inspectRepoSignals } from "./workflow-governance.js";
|
|
7
7
|
import { buildVerifiedHandoff } from "../vendor/core/index.js";
|
|
8
|
+
import { renderVerifiedHandoffMarkdown } from "../vendor/presentation/index.js";
|
|
8
9
|
export async function martinRunDossierTool(input) {
|
|
9
10
|
const detail = await loadDetailedLoopRecord(input);
|
|
10
11
|
const ledgerEvents = await readLedgerEvents(detail);
|
|
@@ -31,21 +32,64 @@ export async function martinRunDossierTool(input) {
|
|
|
31
32
|
...(attempt.summary ? { summary: attempt.summary } : {}),
|
|
32
33
|
artifactFiles: await readAttemptArtifactFiles(detail, attempt.index)
|
|
33
34
|
})));
|
|
35
|
+
const fileEffects = buildFileEffectsSummary(detail.loop);
|
|
36
|
+
const verifiedHandoff = buildVerifiedHandoff({
|
|
37
|
+
loop: detail.loop,
|
|
38
|
+
receiptIntegrity: resolveReceiptIntegrity(detail.loop),
|
|
39
|
+
verification: {
|
|
40
|
+
status: verification.status,
|
|
41
|
+
summary: verification.summary ?? "No verification summary recorded.",
|
|
42
|
+
steps: verification.steps,
|
|
43
|
+
warnings: verification.warnings,
|
|
44
|
+
...(verification.binding ? { binding: verification.binding } : {})
|
|
45
|
+
},
|
|
46
|
+
fileEffects,
|
|
47
|
+
scope: {
|
|
48
|
+
status: detail.loop.task?.allowedPaths?.length ||
|
|
49
|
+
detail.loop.task?.deniedPaths?.length
|
|
50
|
+
? "WITHIN_SCOPE"
|
|
51
|
+
: "NOT_EVALUATED",
|
|
52
|
+
allowedPaths: detail.loop.task?.allowedPaths ?? [],
|
|
53
|
+
deniedPaths: detail.loop.task?.deniedPaths ?? [],
|
|
54
|
+
changedFiles: verification.changedFiles,
|
|
55
|
+
violations: []
|
|
56
|
+
},
|
|
57
|
+
testIntegrity: {
|
|
58
|
+
status: "NOT_EVALUATED",
|
|
59
|
+
verdict: "NOT_EVALUATED",
|
|
60
|
+
protectedPaths: [],
|
|
61
|
+
changedProtectedPaths: [],
|
|
62
|
+
findings: [],
|
|
63
|
+
summary: "Automatic test-integrity evidence was not recorded for this run."
|
|
64
|
+
},
|
|
65
|
+
unresolvedWork: verification.status === "passed"
|
|
66
|
+
? []
|
|
67
|
+
: [verification.summary ?? "Verification did not pass."],
|
|
68
|
+
nextAction: "Review the canonical dossier evidence."
|
|
69
|
+
});
|
|
70
|
+
const receiptIntegrity = resolveReceiptIntegrity(detail.loop);
|
|
71
|
+
const reviewOutcome = verifiedHandoff.outcome === "VERIFIED"
|
|
72
|
+
? "passed"
|
|
73
|
+
: verifiedHandoff.outcome === "STOPPED"
|
|
74
|
+
? "failed"
|
|
75
|
+
: "needs_review";
|
|
76
|
+
const nextAction = receiptIntegrity.state !== "verified"
|
|
77
|
+
? `Resolve receipt integrity (${receiptIntegrity.state}) before trusting or promoting this run.`
|
|
78
|
+
: verifiedHandoff.outcome === "VERIFIED"
|
|
79
|
+
? "Review the dossier and evaluation, then decide whether to merge or promote."
|
|
80
|
+
: verifiedHandoff.outcome === "STOPPED"
|
|
81
|
+
? `The run stopped${verifiedHandoff.lifecycleTruth.reasonCode ? ` (${verifiedHandoff.lifecycleTruth.reasonCode})` : ""}; review lifecycle evidence before retrying.`
|
|
82
|
+
: verification.status === "failed" || verification.status === "contradicted"
|
|
83
|
+
? "Investigate the latest verifier failure before retrying or promoting."
|
|
84
|
+
: "Collect more evidence before claiming completion.";
|
|
85
|
+
verifiedHandoff.nextAction = nextAction;
|
|
34
86
|
const review = {
|
|
35
87
|
diffSummary: attempts.length > 0
|
|
36
88
|
? `Run touched ${attempts.length} attempt(s); latest summary: ${attempts.at(-1)?.summary ?? "No attempt summary recorded."}`
|
|
37
89
|
: "No attempts were recorded for this run.",
|
|
38
90
|
risk,
|
|
39
|
-
outcome:
|
|
40
|
-
|
|
41
|
-
: verification.status === "failed" || verification.status === "contradicted"
|
|
42
|
-
? "failed"
|
|
43
|
-
: "needs_review",
|
|
44
|
-
nextAction: verification.status === "passed"
|
|
45
|
-
? "Review the dossier and evaluation, then decide whether to merge or promote."
|
|
46
|
-
: verification.status === "failed" || verification.status === "contradicted"
|
|
47
|
-
? "Investigate the latest verifier failure before retrying or promoting."
|
|
48
|
-
: "Collect more evidence before claiming completion."
|
|
91
|
+
outcome: reviewOutcome,
|
|
92
|
+
nextAction
|
|
49
93
|
};
|
|
50
94
|
const output = {
|
|
51
95
|
source: detail.source,
|
|
@@ -53,7 +97,7 @@ export async function martinRunDossierTool(input) {
|
|
|
53
97
|
loop: buildLoopPreview(detail.loop),
|
|
54
98
|
budget: buildBudgetSnapshot(detail.loop.budget),
|
|
55
99
|
cost: buildCostSnapshot(detail.loop.cost),
|
|
56
|
-
receiptIntegrity
|
|
100
|
+
receiptIntegrity,
|
|
57
101
|
...(detail.loop.receiptScope ? { receiptScope: detail.loop.receiptScope } : {}),
|
|
58
102
|
...(detail.loop.terminationEnvelope ? { terminationEnvelope: detail.loop.terminationEnvelope } : {}),
|
|
59
103
|
attempts,
|
|
@@ -75,36 +119,7 @@ export async function martinRunDossierTool(input) {
|
|
|
75
119
|
...(detail.ledgerPath ? { ledgerPath: detail.ledgerPath } : {})
|
|
76
120
|
},
|
|
77
121
|
warnings: [...detail.warnings, ...verification.warnings],
|
|
78
|
-
verifiedHandoff
|
|
79
|
-
loop: detail.loop,
|
|
80
|
-
receiptIntegrity: resolveReceiptIntegrity(detail.loop),
|
|
81
|
-
verification: {
|
|
82
|
-
status: verification.status,
|
|
83
|
-
summary: verification.summary ?? "No verification summary recorded.",
|
|
84
|
-
steps: [],
|
|
85
|
-
warnings: verification.warnings,
|
|
86
|
-
},
|
|
87
|
-
scope: {
|
|
88
|
-
status: detail.loop.task?.allowedPaths?.length ||
|
|
89
|
-
detail.loop.task?.deniedPaths?.length
|
|
90
|
-
? "WITHIN_SCOPE"
|
|
91
|
-
: "NOT_EVALUATED",
|
|
92
|
-
allowedPaths: detail.loop.task?.allowedPaths ?? [],
|
|
93
|
-
deniedPaths: detail.loop.task?.deniedPaths ?? [],
|
|
94
|
-
changedFiles: [],
|
|
95
|
-
violations: [],
|
|
96
|
-
},
|
|
97
|
-
testIntegrity: {
|
|
98
|
-
status: "NOT_EVALUATED",
|
|
99
|
-
verdict: "NOT_EVALUATED",
|
|
100
|
-
protectedPaths: [],
|
|
101
|
-
changedProtectedPaths: [],
|
|
102
|
-
findings: [],
|
|
103
|
-
summary: "Automatic test-integrity evidence was not recorded for this run.",
|
|
104
|
-
},
|
|
105
|
-
unresolvedWork: verification.status === "passed" ? [] : [verification.summary ?? "Verification did not pass."],
|
|
106
|
-
nextAction: review.nextAction,
|
|
107
|
-
}),
|
|
122
|
+
verifiedHandoff,
|
|
108
123
|
};
|
|
109
124
|
if (output.format !== "json") {
|
|
110
125
|
output.rendered = renderDossier(output);
|
|
@@ -113,14 +128,11 @@ export async function martinRunDossierTool(input) {
|
|
|
113
128
|
}
|
|
114
129
|
function renderDossier(output) {
|
|
115
130
|
const lines = [
|
|
116
|
-
output.
|
|
131
|
+
renderVerifiedHandoffMarkdown(output.verifiedHandoff),
|
|
117
132
|
"",
|
|
118
|
-
|
|
119
|
-
`Definition of Done: ${output.verifiedHandoff.definitionOfDone.acceptanceCriteria.length} acceptance criterion/criteria`,
|
|
120
|
-
`Verification: ${output.verifiedHandoff.verification.status}`,
|
|
121
|
-
`Scope: ${output.verifiedHandoff.scope.status}`,
|
|
122
|
-
`Test Integrity: ${output.verifiedHandoff.testIntegrity.verdict}`,
|
|
133
|
+
output.format === "github-pr" ? "### MCP Dossier Context" : "## MCP Dossier Context",
|
|
123
134
|
"",
|
|
135
|
+
`Definition of Done: ${output.verifiedHandoff.definitionOfDone.acceptanceCriteria.length} acceptance criterion/criteria`,
|
|
124
136
|
`Objective: ${output.loop.objective}`,
|
|
125
137
|
`Run: ${output.loop.loopId}`,
|
|
126
138
|
`Status: ${output.loop.status} / ${output.loop.lifecycleState}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CostProvenance, LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope, TerminationEnvelopeV1 } from "../vendor/contracts/index.js";
|
|
2
|
-
import { type LedgerEvent, type LoopAttemptRecord, type LoopRunRecord } from "../vendor/core/index.js";
|
|
1
|
+
import type { CostProvenance, LoopArtifact, LoopBudget, LoopCost, LoopEvent, LoopTask, ReceiptIntegritySummary, ReceiptScope, TerminationEnvelopeV1, VerifiedHandoffFileEffectsV1 } from "../vendor/contracts/index.js";
|
|
2
|
+
import { type LedgerEvent, type LoopAttemptRecord, type LoopRunRecord, type VerifierExecutionBinding } from "../vendor/core/index.js";
|
|
3
3
|
export declare const MARTIN_ENGINE_VALUES: readonly ["claude", "codex", "gemini", "openai"];
|
|
4
4
|
export type MartinEngine = (typeof MARTIN_ENGINE_VALUES)[number];
|
|
5
5
|
export interface InspectableLoopAttempt extends LoopAttemptRecord {
|
|
@@ -7,6 +7,7 @@ export interface InspectableLoopAttempt extends LoopAttemptRecord {
|
|
|
7
7
|
summary?: string;
|
|
8
8
|
}
|
|
9
9
|
export interface InspectableLoopRecord extends Omit<LoopRunRecord, "attempts" | "task" | "budget"> {
|
|
10
|
+
workspaceId?: string;
|
|
10
11
|
budget: LoopBudget;
|
|
11
12
|
attempts: InspectableLoopAttempt[];
|
|
12
13
|
task?: LoopTask;
|
|
@@ -75,8 +76,20 @@ export interface VerificationSummary {
|
|
|
75
76
|
latestAttemptIndex?: number;
|
|
76
77
|
completedAt?: string;
|
|
77
78
|
summary?: string;
|
|
79
|
+
steps: VerificationStepSummary[];
|
|
80
|
+
binding?: VerifierExecutionBinding;
|
|
81
|
+
changedFiles: string[];
|
|
78
82
|
warnings: string[];
|
|
79
83
|
}
|
|
84
|
+
export interface VerificationStepSummary {
|
|
85
|
+
command: string;
|
|
86
|
+
launched: boolean;
|
|
87
|
+
completed?: boolean;
|
|
88
|
+
crashed?: boolean;
|
|
89
|
+
exitCode?: number;
|
|
90
|
+
timedOut?: boolean;
|
|
91
|
+
detail?: string;
|
|
92
|
+
}
|
|
80
93
|
export interface RunWarningEnvelope {
|
|
81
94
|
warnings: string[];
|
|
82
95
|
}
|
|
@@ -126,6 +139,7 @@ export declare function buildLoopPreview(loop: InspectableLoopRecord): LoopPrevi
|
|
|
126
139
|
export declare function buildAttemptSummary(attempt: InspectableLoopAttempt, artifacts?: AttemptArtifactFiles): AttemptSummary;
|
|
127
140
|
export declare function buildArtifactSummary(loop: InspectableLoopRecord): ArtifactSummary;
|
|
128
141
|
export declare function buildVerificationSummary(loop: InspectableLoopRecord, ledgerEvents?: LedgerEvent[]): VerificationSummary;
|
|
142
|
+
export declare function buildFileEffectsSummary(loop: InspectableLoopRecord): VerifiedHandoffFileEffectsV1;
|
|
129
143
|
export declare function resolveReceiptIntegrity(loop: InspectableLoopRecord): ReceiptIntegritySummary;
|
|
130
144
|
export declare function buildEventSummaries(loop: InspectableLoopRecord, limit?: number): EventSummary[];
|
|
131
145
|
export declare function buildLoopCollectionSummary(loops: Array<LoopRunRecord | InspectableLoopRecord>): LoopCollectionSummary;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants } from "node:fs";
|
|
2
2
|
import { readdir, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { evaluateCostGovernor, resolveRunsRoot } from "../vendor/core/index.js";
|
|
4
|
+
import { evaluateCostGovernor, resolveRunsRoot, verifierActuallyPassed } from "../vendor/core/index.js";
|
|
5
5
|
import { readAllLoopRecordsSafely } from "./run-store.js";
|
|
6
6
|
export const MARTIN_ENGINE_VALUES = ["claude", "codex", "gemini", "openai"];
|
|
7
7
|
const CLI_CACHE_TTL_MS = 60_000;
|
|
@@ -241,18 +241,94 @@ export function buildVerificationSummary(loop, ledgerEvents = []) {
|
|
|
241
241
|
warnings.push(...selectedEvidence.warnings);
|
|
242
242
|
const latestEvidence = selectedEvidence.evidence;
|
|
243
243
|
if (!latestEvidence)
|
|
244
|
-
return {
|
|
245
|
-
|
|
244
|
+
return {
|
|
245
|
+
status: "not_run",
|
|
246
|
+
eventCount: verificationEvents.length,
|
|
247
|
+
ledgerEventCount: verificationLedgerEvents.length,
|
|
248
|
+
steps: [],
|
|
249
|
+
changedFiles: [],
|
|
250
|
+
warnings
|
|
251
|
+
};
|
|
252
|
+
const expectedAttemptId = latestEvidence.attemptIndex !== undefined
|
|
253
|
+
? loop.attempts.find((attempt) => attempt.index === latestEvidence.attemptIndex)?.attemptId
|
|
254
|
+
: loop.attempts.at(-1)?.attemptId;
|
|
255
|
+
const expectedBinding = latestEvidence.binding && loop.workspaceId
|
|
256
|
+
? {
|
|
257
|
+
runId: loop.loopId,
|
|
258
|
+
workspaceId: loop.workspaceId,
|
|
259
|
+
...(expectedAttemptId ? { attemptId: expectedAttemptId } : {}),
|
|
260
|
+
cwd: loop.receiptScope?.workingDirectory ?? loop.task?.repoRoot ?? latestEvidence.binding.cwd,
|
|
261
|
+
...(loop.receiptScope?.runsRoot ? { runsRoot: loop.receiptScope.runsRoot } : {}),
|
|
262
|
+
...(loop.task?.executionProfile ? { executionProfile: loop.task.executionProfile } : {}),
|
|
263
|
+
...(loop.task?.allowedNetworkDomains?.length
|
|
264
|
+
? { allowedNetworkDomains: [...loop.task.allowedNetworkDomains] }
|
|
265
|
+
: {}),
|
|
266
|
+
commands: [...(loop.task?.verificationPlan ?? [])]
|
|
267
|
+
}
|
|
268
|
+
: undefined;
|
|
269
|
+
const executionPassed = expectedBinding
|
|
270
|
+
? verifierActuallyPassed({
|
|
271
|
+
passed: latestEvidence.passed === true,
|
|
272
|
+
binding: latestEvidence.binding,
|
|
273
|
+
steps: latestEvidence.steps
|
|
274
|
+
}, expectedBinding)
|
|
275
|
+
: false;
|
|
276
|
+
const contradicted = latestEvidence.passed === true && !executionPassed;
|
|
277
|
+
if (contradicted) {
|
|
278
|
+
warnings.push("Persisted verifier claimed success without complete bound execution evidence.");
|
|
279
|
+
}
|
|
246
280
|
return {
|
|
247
|
-
status: contradicted ? "contradicted" :
|
|
281
|
+
status: contradicted ? "contradicted" : executionPassed ? "passed" : latestEvidence.passed === false ? "failed" : "not_run",
|
|
248
282
|
eventCount: verificationEvents.length,
|
|
249
283
|
ledgerEventCount: verificationLedgerEvents.length,
|
|
250
284
|
...(latestEvidence.attemptIndex !== undefined ? { latestAttemptIndex: latestEvidence.attemptIndex } : {}),
|
|
251
285
|
...(latestEvidence.timestamp ? { completedAt: latestEvidence.timestamp } : {}),
|
|
252
286
|
...(typeof latestEvidence.summary === "string" && latestEvidence.summary.trim().length > 0 ? { summary: latestEvidence.summary.trim() } : {}),
|
|
287
|
+
steps: latestEvidence.steps,
|
|
288
|
+
...(latestEvidence.binding ? { binding: latestEvidence.binding } : {}),
|
|
289
|
+
changedFiles: latestEvidence.changedFiles,
|
|
253
290
|
warnings
|
|
254
291
|
};
|
|
255
292
|
}
|
|
293
|
+
export function buildFileEffectsSummary(loop) {
|
|
294
|
+
const completedEvent = (loop.events ?? []).filter((event) => event.type === "run.completed").at(-1);
|
|
295
|
+
const payload = completedEvent && isRecord(completedEvent.payload) ? completedEvent.payload : undefined;
|
|
296
|
+
const candidate = payload && isRecord(payload["fileEffects"]) ? payload["fileEffects"] : undefined;
|
|
297
|
+
if (!candidate) {
|
|
298
|
+
return {
|
|
299
|
+
observationStatus: "unavailable",
|
|
300
|
+
touchedDuringExecution: [],
|
|
301
|
+
netChangedAtCompletion: [],
|
|
302
|
+
revertedBeforeCompletion: [],
|
|
303
|
+
observationReason: "File-effect evidence was not recorded for this run."
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const observationStatus = candidate["observationStatus"];
|
|
307
|
+
const touchedDuringExecution = readStringArray(candidate["touchedDuringExecution"]);
|
|
308
|
+
const netChangedAtCompletion = readStringArray(candidate["netChangedAtCompletion"]);
|
|
309
|
+
const revertedBeforeCompletion = readStringArray(candidate["revertedBeforeCompletion"]);
|
|
310
|
+
if ((observationStatus !== "observed" && observationStatus !== "unavailable")
|
|
311
|
+
|| touchedDuringExecution === undefined
|
|
312
|
+
|| netChangedAtCompletion === undefined
|
|
313
|
+
|| revertedBeforeCompletion === undefined) {
|
|
314
|
+
return {
|
|
315
|
+
observationStatus: "unavailable",
|
|
316
|
+
touchedDuringExecution: [],
|
|
317
|
+
netChangedAtCompletion: [],
|
|
318
|
+
revertedBeforeCompletion: [],
|
|
319
|
+
observationReason: "Persisted file-effect evidence was malformed."
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
observationStatus,
|
|
324
|
+
touchedDuringExecution,
|
|
325
|
+
netChangedAtCompletion,
|
|
326
|
+
revertedBeforeCompletion,
|
|
327
|
+
...(typeof candidate["observationReason"] === "string"
|
|
328
|
+
? { observationReason: candidate["observationReason"] }
|
|
329
|
+
: {})
|
|
330
|
+
};
|
|
331
|
+
}
|
|
256
332
|
export function resolveReceiptIntegrity(loop) {
|
|
257
333
|
return loop.receiptIntegrity ?? { state: "selector_noncanonical", reason: "Receipt integrity metadata was not available on the loop record." };
|
|
258
334
|
}
|
|
@@ -370,7 +446,10 @@ function normalizeLoopVerificationEvidence(loop, event) {
|
|
|
370
446
|
...(matchedAttempt.attemptId ? { attemptId: matchedAttempt.attemptId } : {}),
|
|
371
447
|
attemptIndex: matchedAttempt.index,
|
|
372
448
|
...(typeof payload?.["passed"] === "boolean" ? { passed: payload["passed"] } : {}),
|
|
373
|
-
...(typeof payload?.["summary"] === "string" ? { summary: payload["summary"] } : {})
|
|
449
|
+
...(typeof payload?.["summary"] === "string" ? { summary: payload["summary"] } : {}),
|
|
450
|
+
steps: readVerificationSteps(payload?.["steps"]),
|
|
451
|
+
...(readVerifierBinding(payload?.["binding"]) ? { binding: readVerifierBinding(payload?.["binding"]) } : {}),
|
|
452
|
+
changedFiles: readStringArray(payload?.["changedFiles"]) ?? []
|
|
374
453
|
};
|
|
375
454
|
}
|
|
376
455
|
function normalizeLedgerVerificationEvidence(loop, event) {
|
|
@@ -387,9 +466,68 @@ function normalizeLedgerVerificationEvidence(loop, event) {
|
|
|
387
466
|
...(matchedAttempt.attemptId ? { attemptId: matchedAttempt.attemptId } : {}),
|
|
388
467
|
attemptIndex: event.attemptIndex,
|
|
389
468
|
...(typeof payload?.["passed"] === "boolean" ? { passed: payload["passed"] } : {}),
|
|
390
|
-
...(typeof payload?.["summary"] === "string" ? { summary: payload["summary"] } : {})
|
|
469
|
+
...(typeof payload?.["summary"] === "string" ? { summary: payload["summary"] } : {}),
|
|
470
|
+
steps: readVerificationSteps(payload?.["steps"]),
|
|
471
|
+
...(readVerifierBinding(payload?.["binding"]) ? { binding: readVerifierBinding(payload?.["binding"]) } : {}),
|
|
472
|
+
changedFiles: readStringArray(payload?.["changedFiles"]) ?? []
|
|
391
473
|
};
|
|
392
474
|
}
|
|
475
|
+
function readVerificationSteps(value) {
|
|
476
|
+
if (!Array.isArray(value))
|
|
477
|
+
return [];
|
|
478
|
+
return value
|
|
479
|
+
.map((candidate) => {
|
|
480
|
+
if (!isRecord(candidate) || typeof candidate["command"] !== "string" || typeof candidate["launched"] !== "boolean") {
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
command: candidate["command"],
|
|
485
|
+
launched: candidate["launched"],
|
|
486
|
+
...(typeof candidate["completed"] === "boolean" ? { completed: candidate["completed"] } : {}),
|
|
487
|
+
...(typeof candidate["crashed"] === "boolean" ? { crashed: candidate["crashed"] } : {}),
|
|
488
|
+
...(typeof candidate["exitCode"] === "number" ? { exitCode: candidate["exitCode"] } : {}),
|
|
489
|
+
...(typeof candidate["timedOut"] === "boolean" ? { timedOut: candidate["timedOut"] } : {}),
|
|
490
|
+
...(typeof candidate["detail"] === "string" ? { detail: candidate["detail"] } : {})
|
|
491
|
+
};
|
|
492
|
+
})
|
|
493
|
+
.filter((candidate) => candidate !== undefined);
|
|
494
|
+
}
|
|
495
|
+
function readVerifierBinding(value) {
|
|
496
|
+
if (!isRecord(value)
|
|
497
|
+
|| typeof value["runId"] !== "string"
|
|
498
|
+
|| typeof value["workspaceId"] !== "string"
|
|
499
|
+
|| typeof value["cwd"] !== "string"
|
|
500
|
+
|| !Array.isArray(value["commands"])
|
|
501
|
+
|| !value["commands"].every((command) => typeof command === "string")) {
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
const executionProfile = value["executionProfile"];
|
|
505
|
+
if (executionProfile !== undefined
|
|
506
|
+
&& executionProfile !== "strict_local"
|
|
507
|
+
&& executionProfile !== "ci_safe"
|
|
508
|
+
&& executionProfile !== "staging_controlled"
|
|
509
|
+
&& executionProfile !== "research_untrusted") {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
const allowedNetworkDomains = readStringArray(value["allowedNetworkDomains"]);
|
|
513
|
+
if (value["allowedNetworkDomains"] !== undefined && allowedNetworkDomains === undefined)
|
|
514
|
+
return undefined;
|
|
515
|
+
return {
|
|
516
|
+
runId: value["runId"],
|
|
517
|
+
workspaceId: value["workspaceId"],
|
|
518
|
+
...(typeof value["attemptId"] === "string" ? { attemptId: value["attemptId"] } : {}),
|
|
519
|
+
cwd: value["cwd"],
|
|
520
|
+
...(typeof value["runsRoot"] === "string" ? { runsRoot: value["runsRoot"] } : {}),
|
|
521
|
+
...(executionProfile !== undefined ? { executionProfile } : {}),
|
|
522
|
+
...(allowedNetworkDomains !== undefined ? { allowedNetworkDomains } : {}),
|
|
523
|
+
commands: [...value["commands"]]
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function readStringArray(value) {
|
|
527
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
528
|
+
? [...value]
|
|
529
|
+
: undefined;
|
|
530
|
+
}
|
|
393
531
|
function isTrustedVerificationTimestamp(value) {
|
|
394
532
|
const timestamp = new Date(value).getTime();
|
|
395
533
|
return Number.isFinite(timestamp) && timestamp <= Date.now() + 5 * 60_000;
|
|
@@ -63,6 +63,8 @@ export interface AgentCliAdapterOptions {
|
|
|
63
63
|
streamingTokenCap?: boolean;
|
|
64
64
|
/** Provider detail-token fields are already included in input/output token totals. */
|
|
65
65
|
streamingUsageDetailsIncludedInTotals?: boolean;
|
|
66
|
+
/** Whether provider usage arrives incrementally or only on a terminal event. */
|
|
67
|
+
streamingUsageReportingMode?: "incremental" | "terminal";
|
|
66
68
|
/** Estimated floor used by the host to reject impossible token caps before spawning. */
|
|
67
69
|
budgetPreflight?: {
|
|
68
70
|
minimumViableTokens: number;
|
|
@@ -383,7 +383,7 @@ function extractGeminiJsonResult(stdout, modelLabel) {
|
|
|
383
383
|
})
|
|
384
384
|
};
|
|
385
385
|
}
|
|
386
|
-
function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate, capTokens, detailTokensIncludedInTotals = false, observedUsageGovernor) {
|
|
386
|
+
function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate, capTokens, detailTokensIncludedInTotals = false, observedUsageGovernor, usageReportingMode = "incremental") {
|
|
387
387
|
let pricingResolution = resolveModelPricing(modelLabel);
|
|
388
388
|
// Safety margin: terminate at 80% of cap to bound one-turn overshoot.
|
|
389
389
|
// Without this, a single expensive turn can blow past the cap before the
|
|
@@ -424,15 +424,22 @@ function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate,
|
|
|
424
424
|
finalObservationPublished = true;
|
|
425
425
|
try {
|
|
426
426
|
const decision = observedUsageGovernor({ cumulativeUsd, cumulativeTokens, turns, final });
|
|
427
|
-
if (decision.action === "terminate") {
|
|
427
|
+
if (decision.action === "terminate" && !(usageReportingMode === "terminal" && final)) {
|
|
428
428
|
terminate(`Parent observed-usage governor terminated the subprocess: ${decision.reason}.`);
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
catch (error) {
|
|
432
|
-
|
|
432
|
+
if (!(usageReportingMode === "terminal" && final)) {
|
|
433
|
+
terminate(`Parent observed-usage governor failed closed: ${error instanceof Error ? error.message : String(error)}.`);
|
|
434
|
+
}
|
|
433
435
|
}
|
|
434
436
|
return;
|
|
435
437
|
}
|
|
438
|
+
// Codex reports authoritative usage on turn.completed, after the provider
|
|
439
|
+
// turn and its workspace edits have already finished. Record that terminal
|
|
440
|
+
// settlement, but do not kill and discard completed work at this point.
|
|
441
|
+
if (usageReportingMode === "terminal" && final)
|
|
442
|
+
return;
|
|
436
443
|
if (capTokens !== undefined && capTokens > 0 && cumulativeTokens > capTokens) {
|
|
437
444
|
terminate(`Streaming token lease exceeded after ${String(turns)} turn(s): observed ${String(cumulativeTokens)} tokens ` +
|
|
438
445
|
`surpassed the per-attempt token cap ${String(capTokens)}. Subprocess terminated to prevent additional token usage.`);
|
|
@@ -557,7 +564,7 @@ function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate,
|
|
|
557
564
|
}
|
|
558
565
|
return;
|
|
559
566
|
}
|
|
560
|
-
extractUsageFromEvent(event, terminate);
|
|
567
|
+
extractUsageFromEvent(event, terminate, usageReportingMode === "terminal" && event.type === "turn.completed");
|
|
561
568
|
};
|
|
562
569
|
return {
|
|
563
570
|
onChunk: (chunk, terminate) => {
|
|
@@ -572,7 +579,8 @@ function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate,
|
|
|
572
579
|
const bytesCeiling = blendedCostPerChar && blendedCostPerChar > 0
|
|
573
580
|
? Math.ceil((capUsd / blendedCostPerChar) * 2)
|
|
574
581
|
: undefined;
|
|
575
|
-
if (
|
|
582
|
+
if (usageReportingMode === "incremental" &&
|
|
583
|
+
!usageEventSeen && capUsd > 0 && bytesCeiling !== undefined && totalBytes > bytesCeiling) {
|
|
576
584
|
terminate(`Streaming byte ceiling exceeded (${String(totalBytes)} bytes > ${String(bytesCeiling)} ceiling) ` +
|
|
577
585
|
`without any usage events parsed. The Claude stream-json event format may have changed. ` +
|
|
578
586
|
`Subprocess terminated as a fallback budget guard for cap $${capUsd.toFixed(4)}.`);
|
|
@@ -582,7 +590,8 @@ function createStreamingUsageInspector(capUsd, modelLabel, promptTokenEstimate,
|
|
|
582
590
|
// without a single usage event, estimate spend from byte volume and
|
|
583
591
|
// enforce the cap. This catches cases where Claude's event format
|
|
584
592
|
// changed but bytes are still flowing.
|
|
585
|
-
if (
|
|
593
|
+
if (usageReportingMode === "incremental" &&
|
|
594
|
+
!usageEventSeen &&
|
|
586
595
|
capUsd > 0 &&
|
|
587
596
|
firstChunkAt !== undefined &&
|
|
588
597
|
Date.now() - firstChunkAt > USAGE_BLIND_TIMEOUT_MS &&
|
|
@@ -726,7 +735,7 @@ export function createAgentCliAdapter(options) {
|
|
|
726
735
|
const streamingUsage = options.streamingUsageCap &&
|
|
727
736
|
(request.context.remainingBudgetUsd > 0 ||
|
|
728
737
|
(options.streamingTokenCap && (request.context.remainingTokens ?? 0) > 0))
|
|
729
|
-
? createStreamingUsageInspector(request.context.remainingBudgetUsd, options.model ?? options.command, estimatedUsage.tokensIn, options.streamingTokenCap ? request.context.remainingTokens : undefined, options.streamingUsageDetailsIncludedInTotals, request.observedUsageGovernor)
|
|
738
|
+
? createStreamingUsageInspector(request.context.remainingBudgetUsd, options.model ?? options.command, estimatedUsage.tokensIn, options.streamingTokenCap ? request.context.remainingTokens : undefined, options.streamingUsageDetailsIncludedInTotals, request.observedUsageGovernor, options.streamingUsageReportingMode)
|
|
730
739
|
: undefined;
|
|
731
740
|
const agentResult = await runSubprocess(options.executionCommand ?? options.command, args, {
|
|
732
741
|
cwd: workingDirectory,
|
|
@@ -51,6 +51,7 @@ export function createCodexCliAdapter(options = {}) {
|
|
|
51
51
|
streamingUsageCap: true,
|
|
52
52
|
streamingTokenCap: true,
|
|
53
53
|
streamingUsageDetailsIncludedInTotals: true,
|
|
54
|
+
streamingUsageReportingMode: "terminal",
|
|
54
55
|
budgetPreflight: {
|
|
55
56
|
minimumViableTokens: CODEX_MINIMUM_VIABLE_TOKEN_BUDGET,
|
|
56
57
|
basis: CODEX_TOKEN_BUDGET_PREFLIGHT_BASIS
|
|
@@ -472,7 +472,7 @@ export type { ContextC5EnvelopeV1, ContextEvidence, ContextShadowDecisionV1, Con
|
|
|
472
472
|
export { CONTEXT_MANIFEST_VERSION, CONTEXT_LEDGER_VERSION } from "./context-manifest.js";
|
|
473
473
|
export type { ContinuationCheckpoint, ContextBudget, ContextCandidateDecision, ContextFaultRequest, ContextFaultResult, ContextKind, ContextLedgerEntry, ContextManifest, ContextObject, ContextPolicy, ContextPriority, ContextSensitivity, ContextTrust, TaskItem, UsageEvidence } from "./context-manifest.js";
|
|
474
474
|
export { EVIDENCE_STATUSES, EXECUTION_MODES, TEST_INTEGRITY_STATUSES, TEST_INTEGRITY_VERDICTS, VERIFIED_HANDOFF_OUTCOMES, } from "./verified-handoff.js";
|
|
475
|
-
export type { EvidenceStatus, ExecutionMode, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffCheckV1, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1, } from "./verified-handoff.js";
|
|
475
|
+
export type { EvidenceStatus, ExecutionMode, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffCheckV1, VerifiedHandoffExhaustedLimitV1, VerifiedHandoffFileEffectsV1, VerifiedHandoffLifecycleTruthV1, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1, } from "./verified-handoff.js";
|
|
476
476
|
export { HANDOFF_SCHEMA_VERSION } from "./context-handoff.js";
|
|
477
477
|
export type { ChainIntegrityState, ContextCircuitBreakResult, ContextExclusionDecision, ContextHandoffArtifact, ContextHandoffClaim, ContextHandoffReceipt, ContextHandoffVerification, HandoffClaimState } from "./context-handoff.js";
|
|
478
478
|
export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CostProvenance, LoopLifecycleState, LoopStatus, ReceiptIntegritySummary } from "./index.js";
|
|
2
|
+
import type { ExitKind } from "./exits.js";
|
|
2
3
|
export declare const VERIFIED_HANDOFF_OUTCOMES: readonly ["VERIFIED", "STOPPED", "NEEDS_REVIEW"];
|
|
3
4
|
export type VerifiedHandoffOutcome = (typeof VERIFIED_HANDOFF_OUTCOMES)[number];
|
|
4
5
|
export declare const EXECUTION_MODES: readonly ["governed", "verification_only", "simulated"];
|
|
@@ -59,6 +60,24 @@ export interface VerifiedHandoffRecoveryV1 {
|
|
|
59
60
|
nextCommand?: string;
|
|
60
61
|
summary: string;
|
|
61
62
|
}
|
|
63
|
+
export type VerifiedHandoffExhaustedLimitV1 = "usd" | "tokens" | "iterations" | "wall_clock";
|
|
64
|
+
export interface VerifiedHandoffLifecycleTruthV1 {
|
|
65
|
+
executionStatus: LoopStatus;
|
|
66
|
+
lifecycleState: LoopLifecycleState;
|
|
67
|
+
terminationClass: "operational_exit" | "guard_stop" | "none";
|
|
68
|
+
reasonCode?: ExitKind | string;
|
|
69
|
+
reason?: string;
|
|
70
|
+
matchedReasons: ExitKind[];
|
|
71
|
+
exhaustedLimits: VerifiedHandoffExhaustedLimitV1[];
|
|
72
|
+
evidenceAvailable: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface VerifiedHandoffFileEffectsV1 {
|
|
75
|
+
observationStatus: "observed" | "unavailable";
|
|
76
|
+
touchedDuringExecution: string[];
|
|
77
|
+
netChangedAtCompletion: string[];
|
|
78
|
+
revertedBeforeCompletion: string[];
|
|
79
|
+
observationReason?: string;
|
|
80
|
+
}
|
|
62
81
|
export interface VerifiedHandoffV1 {
|
|
63
82
|
schemaVersion: "1.0.0";
|
|
64
83
|
handoffId: string;
|
|
@@ -81,6 +100,8 @@ export interface VerifiedHandoffV1 {
|
|
|
81
100
|
status: LoopStatus;
|
|
82
101
|
lifecycleState: LoopLifecycleState;
|
|
83
102
|
};
|
|
103
|
+
lifecycleTruth: VerifiedHandoffLifecycleTruthV1;
|
|
104
|
+
fileEffects: VerifiedHandoffFileEffectsV1;
|
|
84
105
|
verification: {
|
|
85
106
|
status: EvidenceStatus;
|
|
86
107
|
summary: string;
|
|
@@ -71,7 +71,12 @@ export async function verifyExternalOutcomes(contract, options = {}) {
|
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
if (observation.kind === "transient") {
|
|
74
|
-
|
|
74
|
+
// A final request may inherit only the milliseconds left in the
|
|
75
|
+
// contract-wide deadline. Do not let that verifier-side timeout erase
|
|
76
|
+
// a concrete source failure observed by an earlier poll.
|
|
77
|
+
if (observation.reasonCode !== "deadline_exceeded" || state.observationCount === 1) {
|
|
78
|
+
state.lastTransient = observation.reasonCode;
|
|
79
|
+
}
|
|
75
80
|
state.nextPollAt = performance.now() + contract.pollIntervalMs;
|
|
76
81
|
return;
|
|
77
82
|
}
|
|
@@ -32,7 +32,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
|
32
32
|
export type { ContextIntegrityPrecheck, ContextIntegrityVerdict } from "./context-integrity.js";
|
|
33
33
|
export { compilePromptPacket } from "./compiler.js";
|
|
34
34
|
export type { PromptPacket, CompilerAdapterRequest } from "./compiler.js";
|
|
35
|
-
export { buildVerifiedHandoff, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
|
|
35
|
+
export { buildVerifiedHandoff, deriveVerifiedHandoffLifecycleTruth, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
|
|
36
36
|
export type { BoundVerifierEvidence, BuildVerifiedHandoffInput, VerifierExecutionBinding, } from "./verified-handoff.js";
|
|
37
37
|
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
38
38
|
export type { AttemptArtifacts, LedgerEvent, LedgerEventKind, LoopAttemptRecord, LoopRunRecord, ReceiptIntegrityChainEntry, RunContract, RunStore, StoredReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
@@ -32,7 +32,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
|
|
|
32
32
|
// ─── Prompt packet compiler ──────────────────────────────────────────────────
|
|
33
33
|
export { compilePromptPacket } from "./compiler.js";
|
|
34
34
|
// ─── Verified Handoff builder ────────────────────────────────────────────────
|
|
35
|
-
export { buildVerifiedHandoff, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
|
|
35
|
+
export { buildVerifiedHandoff, deriveVerifiedHandoffLifecycleTruth, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
|
|
36
36
|
// ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
|
|
37
37
|
export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
|
|
38
38
|
export { compileAndPersistContext } from "./persistence/index.js";
|
|
@@ -186,6 +186,67 @@ export async function runMartin(input) {
|
|
|
186
186
|
transport: getAdapterTransport(input.adapter)
|
|
187
187
|
}
|
|
188
188
|
}, { now: now(), idFactory });
|
|
189
|
+
const adapterChain = [input.adapter, ...(input.fallbackAdapters ?? [])];
|
|
190
|
+
const runsRoot = input.store?.runsRoot ?? resolveRunsRoot();
|
|
191
|
+
const observesWorkspaceMutations = input.task.repoRoot !== undefined &&
|
|
192
|
+
adapterChain.some((adapter) => adapter.metadata.capabilities?.workspaceMutations !== false);
|
|
193
|
+
const touchedDuringExecution = new Set();
|
|
194
|
+
let completionBoundary;
|
|
195
|
+
let completionBoundaryReason;
|
|
196
|
+
if (observesWorkspaceMutations) {
|
|
197
|
+
try {
|
|
198
|
+
completionBoundary = await captureRollbackBoundary({
|
|
199
|
+
repoRoot: input.task.repoRoot,
|
|
200
|
+
capturedAt: loop.createdAt
|
|
201
|
+
});
|
|
202
|
+
if (!completionBoundary) {
|
|
203
|
+
completionBoundaryReason = "A run-wide file-effect observation boundary was not available.";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
completionBoundaryReason = `Run-wide file-effect observation was unavailable: ${fileEffectErrorMessage(error)}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
completionBoundaryReason =
|
|
212
|
+
"A mutation-capable run-wide file-effect observation boundary was not available.";
|
|
213
|
+
}
|
|
214
|
+
async function finalizeCurrentLoop(decision, timestamp, routingEconomicsInput) {
|
|
215
|
+
const fileEffects = observeRunFileEffects({
|
|
216
|
+
repoRoot: input.task.repoRoot,
|
|
217
|
+
boundary: completionBoundary,
|
|
218
|
+
touchedDuringExecution,
|
|
219
|
+
unavailableReason: completionBoundaryReason
|
|
220
|
+
});
|
|
221
|
+
return finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInput, fileEffects);
|
|
222
|
+
}
|
|
223
|
+
async function persistGuardStop(decision) {
|
|
224
|
+
const envelope = {
|
|
225
|
+
schemaVersion: "termination/1",
|
|
226
|
+
class: "guard_stop",
|
|
227
|
+
guard: {
|
|
228
|
+
reasonCode: decision.reasonCode ?? "guard_stop",
|
|
229
|
+
reason: decision.reason,
|
|
230
|
+
...(decision.failureClass ? { failureClass: decision.failureClass } : {}),
|
|
231
|
+
...(decision.safetySurface ? { safetySurface: decision.safetySurface } : {})
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
if (input.store) {
|
|
235
|
+
await persistTerminationEnvelope(runDir(runsRoot, loop.loopId), envelope);
|
|
236
|
+
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
237
|
+
kind: "run.terminated",
|
|
238
|
+
runId: loop.loopId,
|
|
239
|
+
payload: {
|
|
240
|
+
terminationClass: envelope.class,
|
|
241
|
+
reasonCode: envelope.guard.reasonCode,
|
|
242
|
+
reason: envelope.guard.reason,
|
|
243
|
+
...(envelope.guard.failureClass ? { failureClass: envelope.guard.failureClass } : {}),
|
|
244
|
+
...(envelope.guard.safetySurface ? { safetySurface: envelope.guard.safetySurface } : {})
|
|
245
|
+
}
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
return envelope;
|
|
249
|
+
}
|
|
189
250
|
if (input.store) {
|
|
190
251
|
await input.store.initRun({
|
|
191
252
|
runId: loop.loopId,
|
|
@@ -255,7 +316,7 @@ export async function runMartin(input) {
|
|
|
255
316
|
reason: handoffGate.message ?? "Context handoff verification failed.",
|
|
256
317
|
reasonCode: handoffGate.reasonCode ?? "handoff_verification_failed"
|
|
257
318
|
};
|
|
258
|
-
const finalizedLoop =
|
|
319
|
+
const finalizedLoop = await finalizeCurrentLoop(handoffExitDecision, now());
|
|
259
320
|
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
260
321
|
return { loop: finalizedLoop, decision: handoffExitDecision };
|
|
261
322
|
}
|
|
@@ -272,7 +333,6 @@ export async function runMartin(input) {
|
|
|
272
333
|
}));
|
|
273
334
|
}
|
|
274
335
|
}
|
|
275
|
-
const adapterChain = [input.adapter, ...(input.fallbackAdapters ?? [])];
|
|
276
336
|
let currentAdapterIndex = 0;
|
|
277
337
|
let currentAdapter = adapterChain[currentAdapterIndex] ?? input.adapter;
|
|
278
338
|
let useCompressedContext = false;
|
|
@@ -313,10 +373,12 @@ export async function runMartin(input) {
|
|
|
313
373
|
payload: createRunExitPayload(leashExitDecision)
|
|
314
374
|
}));
|
|
315
375
|
}
|
|
316
|
-
const
|
|
317
|
-
await
|
|
376
|
+
const terminationEnvelope = await persistGuardStop(leashExitDecision);
|
|
377
|
+
const finalizedLoop = await finalizeCurrentLoop(leashExitDecision, now());
|
|
378
|
+
const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope };
|
|
379
|
+
await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
|
|
318
380
|
return {
|
|
319
|
-
loop:
|
|
381
|
+
loop: finalizedWithEnvelope,
|
|
320
382
|
decision: leashExitDecision
|
|
321
383
|
};
|
|
322
384
|
}
|
|
@@ -351,10 +413,12 @@ export async function runMartin(input) {
|
|
|
351
413
|
payload: createRunExitPayload(secretExitDecision)
|
|
352
414
|
}));
|
|
353
415
|
}
|
|
354
|
-
const
|
|
355
|
-
await
|
|
416
|
+
const terminationEnvelope = await persistGuardStop(secretExitDecision);
|
|
417
|
+
const finalizedLoop = await finalizeCurrentLoop(secretExitDecision, now());
|
|
418
|
+
const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope };
|
|
419
|
+
await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
|
|
356
420
|
return {
|
|
357
|
-
loop:
|
|
421
|
+
loop: finalizedWithEnvelope,
|
|
358
422
|
decision: secretExitDecision
|
|
359
423
|
};
|
|
360
424
|
}
|
|
@@ -380,7 +444,6 @@ export async function runMartin(input) {
|
|
|
380
444
|
// governedTurnsUsed starts from the already-completed attempt count so
|
|
381
445
|
// resumed runs count correctly; incremented once per governed attempt.
|
|
382
446
|
let governedTurnsUsed = loop.attempts.length;
|
|
383
|
-
const runsRoot = input.store?.runsRoot ?? resolveRunsRoot();
|
|
384
447
|
const exitSignalSource = input.exitSignalSource ?? createFileExitSignalSource(runsRoot);
|
|
385
448
|
// Diagnostic writes fire from sync callbacks — track them so the finally
|
|
386
449
|
// block can await completion before the run tears down.
|
|
@@ -571,7 +634,7 @@ export async function runMartin(input) {
|
|
|
571
634
|
// persistLoopRecordIfSupported reads ledger.jsonl to compute the integrity hash;
|
|
572
635
|
// any write that lands after that read produces a false ledger_hash_mismatch on verify.
|
|
573
636
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
574
|
-
const finalizedLoop =
|
|
637
|
+
const finalizedLoop = await finalizeCurrentLoop(exitDecision, now(), routingInput);
|
|
575
638
|
const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope: envelope };
|
|
576
639
|
await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
|
|
577
640
|
return { loop: finalizedWithEnvelope, decision: exitDecision };
|
|
@@ -625,7 +688,7 @@ export async function runMartin(input) {
|
|
|
625
688
|
}));
|
|
626
689
|
}
|
|
627
690
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
628
|
-
const finalizedLoop =
|
|
691
|
+
const finalizedLoop = await finalizeCurrentLoop(preflightExitDecision, now());
|
|
629
692
|
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
630
693
|
return {
|
|
631
694
|
loop: finalizedLoop,
|
|
@@ -674,10 +737,12 @@ export async function runMartin(input) {
|
|
|
674
737
|
}));
|
|
675
738
|
}
|
|
676
739
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
677
|
-
const
|
|
678
|
-
await
|
|
740
|
+
const terminationEnvelope = await persistGuardStop(poisoningExitDecision);
|
|
741
|
+
const finalizedLoop = await finalizeCurrentLoop(poisoningExitDecision, now());
|
|
742
|
+
const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope };
|
|
743
|
+
await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
|
|
679
744
|
return {
|
|
680
|
-
loop:
|
|
745
|
+
loop: finalizedWithEnvelope,
|
|
681
746
|
decision: poisoningExitDecision
|
|
682
747
|
};
|
|
683
748
|
}
|
|
@@ -738,7 +803,7 @@ export async function runMartin(input) {
|
|
|
738
803
|
}));
|
|
739
804
|
}
|
|
740
805
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
741
|
-
const finalizedLoop =
|
|
806
|
+
const finalizedLoop = await finalizeCurrentLoop(exitDecision, now());
|
|
742
807
|
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
743
808
|
return {
|
|
744
809
|
loop: finalizedLoop,
|
|
@@ -1011,6 +1076,9 @@ export async function runMartin(input) {
|
|
|
1011
1076
|
const changedFiles = tracksWorkspaceMutations
|
|
1012
1077
|
? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
|
|
1013
1078
|
: [];
|
|
1079
|
+
for (const filePath of normalizeFileEffectPaths(changedFiles)) {
|
|
1080
|
+
touchedDuringExecution.add(filePath);
|
|
1081
|
+
}
|
|
1014
1082
|
// Routing economics: detect first meaningful workspace delta
|
|
1015
1083
|
if (!firstDeltaDetected && changedFiles.length > 0) {
|
|
1016
1084
|
const meaningfulFile = changedFiles.find((f) => !isMartinLoopMetadata(f));
|
|
@@ -1099,10 +1167,12 @@ export async function runMartin(input) {
|
|
|
1099
1167
|
}));
|
|
1100
1168
|
}
|
|
1101
1169
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
1102
|
-
const
|
|
1103
|
-
await
|
|
1170
|
+
const terminationEnvelope = await persistGuardStop(filesystemExitDecision);
|
|
1171
|
+
const finalizedLoop = await finalizeCurrentLoop(filesystemExitDecision, now());
|
|
1172
|
+
const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope };
|
|
1173
|
+
await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
|
|
1104
1174
|
return {
|
|
1105
|
-
loop:
|
|
1175
|
+
loop: finalizedWithEnvelope,
|
|
1106
1176
|
decision: filesystemExitDecision
|
|
1107
1177
|
};
|
|
1108
1178
|
}
|
|
@@ -1177,10 +1247,12 @@ export async function runMartin(input) {
|
|
|
1177
1247
|
}));
|
|
1178
1248
|
}
|
|
1179
1249
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
1180
|
-
const
|
|
1181
|
-
await
|
|
1250
|
+
const terminationEnvelope = await persistGuardStop(approvalExitDecision);
|
|
1251
|
+
const finalizedLoop = await finalizeCurrentLoop(approvalExitDecision, now());
|
|
1252
|
+
const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope };
|
|
1253
|
+
await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
|
|
1182
1254
|
return {
|
|
1183
|
-
loop:
|
|
1255
|
+
loop: finalizedWithEnvelope,
|
|
1184
1256
|
decision: approvalExitDecision
|
|
1185
1257
|
};
|
|
1186
1258
|
}
|
|
@@ -1320,7 +1392,7 @@ export async function runMartin(input) {
|
|
|
1320
1392
|
}));
|
|
1321
1393
|
}
|
|
1322
1394
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
1323
|
-
const finalizedLoop =
|
|
1395
|
+
const finalizedLoop = await finalizeCurrentLoop(patchExitDecision, now());
|
|
1324
1396
|
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1325
1397
|
return {
|
|
1326
1398
|
loop: finalizedLoop,
|
|
@@ -1414,7 +1486,7 @@ export async function runMartin(input) {
|
|
|
1414
1486
|
// Drain pending diagnostic ledger writes before receipt signing.
|
|
1415
1487
|
await stopSignalMonitorAndDrainDiagnostics();
|
|
1416
1488
|
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1417
|
-
const finalizedLoop =
|
|
1489
|
+
const finalizedLoop = await finalizeCurrentLoop(decision, now(), routingInput);
|
|
1418
1490
|
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1419
1491
|
return {
|
|
1420
1492
|
loop: finalizedLoop,
|
|
@@ -1422,28 +1494,11 @@ export async function runMartin(input) {
|
|
|
1422
1494
|
};
|
|
1423
1495
|
}
|
|
1424
1496
|
}
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
status: "exited",
|
|
1429
|
-
reason: "Martin exited because the loop exhausted its configured iteration budget."
|
|
1430
|
-
};
|
|
1431
|
-
if (input.store) {
|
|
1432
|
-
await input.store.appendLedger(loop.loopId, makeLedgerEvent({
|
|
1433
|
-
kind: "run.exited",
|
|
1434
|
-
runId: loop.loopId,
|
|
1435
|
-
payload: createRunExitPayload(decision)
|
|
1436
|
-
}));
|
|
1497
|
+
const exhaustedTerminal = await evaluateCheckpoint("before_retry");
|
|
1498
|
+
if (!exhaustedTerminal) {
|
|
1499
|
+
throw new Error("Loop exhausted its configured iterations without an exit-policy decision.");
|
|
1437
1500
|
}
|
|
1438
|
-
|
|
1439
|
-
await stopSignalMonitorAndDrainDiagnostics();
|
|
1440
|
-
const routingInput = { runStartMs, firstDeltaDetected, firstDeltaTimestampMs, firstDeltaFilePath, firstDeltaChangeType, preDeltaCostUsd };
|
|
1441
|
-
const finalizedLoop = finalizeLoop(loop, decision, now(), idFactory, routingInput);
|
|
1442
|
-
await persistLoopRecordIfSupported(input.store, finalizedLoop);
|
|
1443
|
-
return {
|
|
1444
|
-
loop: finalizedLoop,
|
|
1445
|
-
decision
|
|
1446
|
-
};
|
|
1501
|
+
return await terminateRun(exhaustedTerminal);
|
|
1447
1502
|
}
|
|
1448
1503
|
finally {
|
|
1449
1504
|
if (wallTimer !== undefined)
|
|
@@ -1499,11 +1554,14 @@ function safetyLeashReasonCode(decision, safetySurface) {
|
|
|
1499
1554
|
return `${safetySurface}_safety_block`;
|
|
1500
1555
|
}
|
|
1501
1556
|
}
|
|
1502
|
-
function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInput) {
|
|
1557
|
+
function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInput, fileEffects) {
|
|
1503
1558
|
const finalized = appendLoopEvent(loop, {
|
|
1504
1559
|
type: "run.completed",
|
|
1505
1560
|
lifecycleState: decision.lifecycleState,
|
|
1506
|
-
payload:
|
|
1561
|
+
payload: {
|
|
1562
|
+
...createRunExitPayload(decision),
|
|
1563
|
+
...(fileEffects ? { fileEffects } : {})
|
|
1564
|
+
}
|
|
1507
1565
|
}, { now: timestamp, idFactory });
|
|
1508
1566
|
const routingEconomics = routingEconomicsInput
|
|
1509
1567
|
? buildRoutingEconomics(finalized, routingEconomicsInput, decision)
|
|
@@ -1522,6 +1580,49 @@ function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInpu
|
|
|
1522
1580
|
...(routingEconomics ? { routingEconomics } : {})
|
|
1523
1581
|
};
|
|
1524
1582
|
}
|
|
1583
|
+
function observeRunFileEffects(input) {
|
|
1584
|
+
const touchedDuringExecution = normalizeFileEffectPaths(input.touchedDuringExecution);
|
|
1585
|
+
if (!input.repoRoot || !input.boundary) {
|
|
1586
|
+
return {
|
|
1587
|
+
observationStatus: "unavailable",
|
|
1588
|
+
touchedDuringExecution,
|
|
1589
|
+
netChangedAtCompletion: [],
|
|
1590
|
+
revertedBeforeCompletion: [],
|
|
1591
|
+
observationReason: input.unavailableReason ??
|
|
1592
|
+
"A run-wide file-effect observation boundary was not available."
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
try {
|
|
1596
|
+
const netChangedAtCompletion = normalizeFileEffectPaths(listAttemptChangedFilesSinceBoundary({
|
|
1597
|
+
repoRoot: input.repoRoot,
|
|
1598
|
+
boundary: input.boundary
|
|
1599
|
+
}));
|
|
1600
|
+
const netChanged = new Set(netChangedAtCompletion);
|
|
1601
|
+
return {
|
|
1602
|
+
observationStatus: "observed",
|
|
1603
|
+
touchedDuringExecution,
|
|
1604
|
+
netChangedAtCompletion,
|
|
1605
|
+
revertedBeforeCompletion: touchedDuringExecution.filter((filePath) => !netChanged.has(filePath))
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
catch (error) {
|
|
1609
|
+
return {
|
|
1610
|
+
observationStatus: "unavailable",
|
|
1611
|
+
touchedDuringExecution,
|
|
1612
|
+
netChangedAtCompletion: [],
|
|
1613
|
+
revertedBeforeCompletion: [],
|
|
1614
|
+
observationReason: `Final file-effect observation was unavailable: ${fileEffectErrorMessage(error)}`
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function normalizeFileEffectPaths(paths) {
|
|
1619
|
+
return [...new Set([...paths]
|
|
1620
|
+
.map((filePath) => filePath.replaceAll("\\", "/").replace(/^\.\//u, "").trim())
|
|
1621
|
+
.filter(Boolean))].sort();
|
|
1622
|
+
}
|
|
1623
|
+
function fileEffectErrorMessage(error) {
|
|
1624
|
+
return error instanceof Error ? error.message : String(error);
|
|
1625
|
+
}
|
|
1525
1626
|
function buildRoutingEconomics(loop, input, decision) {
|
|
1526
1627
|
const totalCost = loop.cost.actualUsd;
|
|
1527
1628
|
const preworkCost = roundUsd(Math.max(input.preDeltaCostUsd, 0));
|
|
@@ -1614,10 +1715,17 @@ function mergeCostProvenance(previous, current) {
|
|
|
1614
1715
|
return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
|
|
1615
1716
|
}
|
|
1616
1717
|
function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
|
|
1617
|
-
|
|
1618
|
-
|
|
1718
|
+
const adapterReported = normalizeFileEffectPaths(result.execution?.changedFiles ?? []);
|
|
1719
|
+
try {
|
|
1720
|
+
const gitObserved = listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
|
|
1721
|
+
return normalizeFileEffectPaths([...adapterReported, ...gitObserved]);
|
|
1722
|
+
}
|
|
1723
|
+
catch (error) {
|
|
1724
|
+
if (result.execution?.changedFiles !== undefined) {
|
|
1725
|
+
return adapterReported;
|
|
1726
|
+
}
|
|
1727
|
+
throw error;
|
|
1619
1728
|
}
|
|
1620
|
-
return listAttemptChangedFilesSinceBoundary({ repoRoot, boundary: rollbackBoundary });
|
|
1621
1729
|
}
|
|
1622
1730
|
function buildPatchDiff(result, changedFiles) {
|
|
1623
1731
|
if (result.execution?.patch) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ExecutionMode, LoopRecord, ReceiptIntegritySummary, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1 } from "../contracts/index.js";
|
|
1
|
+
import type { ExecutionMode, ExitKind, LoopRecord, ReceiptIntegritySummary, TestIntegrityStatus, TestIntegrityVerdict, VerifiedHandoffFileEffectsV1, VerifiedHandoffLifecycleTruthV1, VerifiedHandoffOutcome, VerifiedHandoffRecoveryV1, VerifiedHandoffRequirementV1, VerifiedHandoffScopeV1, VerifiedHandoffTestIntegrityV1, VerifiedHandoffV1 } from "../contracts/index.js";
|
|
2
2
|
export declare function toTestIntegrityVerdict(status: TestIntegrityStatus): TestIntegrityVerdict;
|
|
3
|
+
export declare function deriveVerifiedHandoffLifecycleTruth(loop: LoopRecord): VerifiedHandoffLifecycleTruthV1;
|
|
3
4
|
export interface VerifierExecutionBinding {
|
|
4
5
|
runId: string;
|
|
5
6
|
workspaceId: string;
|
|
@@ -25,6 +26,9 @@ export interface BoundVerifierEvidence {
|
|
|
25
26
|
export declare function verifierActuallyPassed(evidence: BoundVerifierEvidence | null | undefined, expected: VerifierExecutionBinding): boolean;
|
|
26
27
|
export declare function resolveVerifiedHandoffOutcome(input: {
|
|
27
28
|
lifecycleState: LoopRecord["lifecycleState"];
|
|
29
|
+
sourceStatus?: LoopRecord["status"];
|
|
30
|
+
terminationClass?: VerifiedHandoffLifecycleTruthV1["terminationClass"];
|
|
31
|
+
reasonCode?: ExitKind | string;
|
|
28
32
|
executionStatus?: "completed" | "write_blocked" | "sandbox_blocked" | "policy_rejected" | "failed";
|
|
29
33
|
verificationStatus: BuildVerifiedHandoffInput["verification"]["status"];
|
|
30
34
|
receiptIntegrity: ReceiptIntegritySummary["state"];
|
|
@@ -65,6 +69,7 @@ export interface BuildVerifiedHandoffInput {
|
|
|
65
69
|
definitionOfDonePreSatisfied?: boolean;
|
|
66
70
|
evidenceContradicted?: boolean;
|
|
67
71
|
changedFiles?: string[];
|
|
72
|
+
fileEffects?: Partial<VerifiedHandoffFileEffectsV1>;
|
|
68
73
|
scope?: Partial<VerifiedHandoffScopeV1>;
|
|
69
74
|
testIntegrity?: Partial<VerifiedHandoffTestIntegrityV1>;
|
|
70
75
|
requirements?: VerifiedHandoffRequirementV1[];
|
|
@@ -28,7 +28,66 @@ const STOPPED_LIFECYCLE_STATES = new Set([
|
|
|
28
28
|
"diminishing_returns",
|
|
29
29
|
"stuck_exit",
|
|
30
30
|
"human_escalation",
|
|
31
|
+
"wall_clock",
|
|
32
|
+
"error_threshold",
|
|
33
|
+
"external_event",
|
|
31
34
|
]);
|
|
35
|
+
export function deriveVerifiedHandoffLifecycleTruth(loop) {
|
|
36
|
+
const envelope = loop.terminationEnvelope;
|
|
37
|
+
if (envelope === undefined) {
|
|
38
|
+
return {
|
|
39
|
+
executionStatus: loop.status,
|
|
40
|
+
lifecycleState: loop.lifecycleState,
|
|
41
|
+
terminationClass: "none",
|
|
42
|
+
matchedReasons: [],
|
|
43
|
+
exhaustedLimits: [],
|
|
44
|
+
evidenceAvailable: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (envelope.class === "guard_stop") {
|
|
48
|
+
return {
|
|
49
|
+
executionStatus: loop.status,
|
|
50
|
+
lifecycleState: loop.lifecycleState,
|
|
51
|
+
terminationClass: "guard_stop",
|
|
52
|
+
reasonCode: envelope.guard.reasonCode,
|
|
53
|
+
reason: envelope.guard.reason,
|
|
54
|
+
matchedReasons: [],
|
|
55
|
+
exhaustedLimits: [],
|
|
56
|
+
evidenceAvailable: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const primary = envelope.exit.primary;
|
|
60
|
+
const primaryMatch = envelope.exit.matches.find((match) => match.kind === primary);
|
|
61
|
+
const exhausted = new Set();
|
|
62
|
+
for (const match of envelope.exit.matches) {
|
|
63
|
+
if (match.kind === "turn_cap") {
|
|
64
|
+
exhausted.add("iterations");
|
|
65
|
+
}
|
|
66
|
+
else if (match.kind === "wall_clock") {
|
|
67
|
+
exhausted.add("wall_clock");
|
|
68
|
+
}
|
|
69
|
+
else if (match.kind === "budget_cap") {
|
|
70
|
+
if (match.evidence["usdExceeded"] === true) {
|
|
71
|
+
exhausted.add("usd");
|
|
72
|
+
}
|
|
73
|
+
if (match.evidence["tokensExceeded"] === true) {
|
|
74
|
+
exhausted.add("tokens");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const exhaustedLimits = ["usd", "tokens", "iterations", "wall_clock"]
|
|
79
|
+
.filter((limit) => exhausted.has(limit));
|
|
80
|
+
return {
|
|
81
|
+
executionStatus: loop.status,
|
|
82
|
+
lifecycleState: loop.lifecycleState,
|
|
83
|
+
terminationClass: "operational_exit",
|
|
84
|
+
...(primary ? { reasonCode: primary } : {}),
|
|
85
|
+
...(primaryMatch?.reason ? { reason: primaryMatch.reason } : {}),
|
|
86
|
+
matchedReasons: [...envelope.exit.matched],
|
|
87
|
+
exhaustedLimits,
|
|
88
|
+
evidenceAvailable: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
32
91
|
export function verifierActuallyPassed(evidence, expected) {
|
|
33
92
|
if (!evidence?.passed || !evidence.binding || !evidence.steps) {
|
|
34
93
|
return false;
|
|
@@ -58,12 +117,19 @@ export function verifierActuallyPassed(evidence, expected) {
|
|
|
58
117
|
}
|
|
59
118
|
export function resolveVerifiedHandoffOutcome(input) {
|
|
60
119
|
if (STOPPED_LIFECYCLE_STATES.has(input.lifecycleState) ||
|
|
120
|
+
input.terminationClass === "guard_stop" ||
|
|
121
|
+
(input.terminationClass === "operational_exit" &&
|
|
122
|
+
input.reasonCode !== undefined &&
|
|
123
|
+
input.reasonCode !== "goal_met") ||
|
|
61
124
|
input.executionStatus === "write_blocked" ||
|
|
62
125
|
input.executionStatus === "sandbox_blocked" ||
|
|
63
126
|
input.executionStatus === "policy_rejected") {
|
|
64
127
|
return "STOPPED";
|
|
65
128
|
}
|
|
66
|
-
if (input.
|
|
129
|
+
if (input.sourceStatus !== "completed" ||
|
|
130
|
+
input.terminationClass !== "operational_exit" ||
|
|
131
|
+
input.reasonCode !== "goal_met" ||
|
|
132
|
+
input.executionStatus === "failed" ||
|
|
67
133
|
input.governanceClaimEligible === false ||
|
|
68
134
|
input.evidenceContradicted === true ||
|
|
69
135
|
(input.mutationRequired === true &&
|
|
@@ -145,6 +211,18 @@ export function buildVerifiedHandoff(input) {
|
|
|
145
211
|
const generatedAt = input.generatedAt ?? new Date().toISOString();
|
|
146
212
|
const changedFiles = input.changedFiles ?? [];
|
|
147
213
|
const unresolvedWork = input.unresolvedWork ?? [];
|
|
214
|
+
const lifecycleTruth = deriveVerifiedHandoffLifecycleTruth(input.loop);
|
|
215
|
+
const fileEffects = {
|
|
216
|
+
observationStatus: input.fileEffects?.observationStatus ?? "unavailable",
|
|
217
|
+
touchedDuringExecution: [...(input.fileEffects?.touchedDuringExecution ?? [])],
|
|
218
|
+
netChangedAtCompletion: [...(input.fileEffects?.netChangedAtCompletion ?? [])],
|
|
219
|
+
revertedBeforeCompletion: [...(input.fileEffects?.revertedBeforeCompletion ?? [])],
|
|
220
|
+
...(input.fileEffects?.observationReason
|
|
221
|
+
? { observationReason: input.fileEffects.observationReason }
|
|
222
|
+
: input.fileEffects?.observationStatus === "observed"
|
|
223
|
+
? {}
|
|
224
|
+
: { observationReason: "File-effect evidence was not recorded for this run." }),
|
|
225
|
+
};
|
|
148
226
|
const scope = {
|
|
149
227
|
status: input.scope?.status ?? "NOT_EVALUATED",
|
|
150
228
|
allowedPaths: input.scope?.allowedPaths ?? input.loop.task.allowedPaths ?? [],
|
|
@@ -163,12 +241,35 @@ export function buildVerifiedHandoff(input) {
|
|
|
163
241
|
"Test integrity was not evaluated for this run.",
|
|
164
242
|
};
|
|
165
243
|
const executionBoundary = deriveVerifiedHandoffExecutionBoundary(input.loop);
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
244
|
+
const expectedVerifierBinding = input.verification.binding
|
|
245
|
+
? {
|
|
246
|
+
runId: input.loop.loopId,
|
|
247
|
+
workspaceId: input.loop.workspaceId,
|
|
248
|
+
...(input.loop.attempts.at(-1)?.attemptId
|
|
249
|
+
? { attemptId: input.loop.attempts.at(-1)?.attemptId }
|
|
250
|
+
: {}),
|
|
251
|
+
cwd: input.loop.receiptScope?.workingDirectory ??
|
|
252
|
+
input.loop.task.repoRoot ??
|
|
253
|
+
input.verification.binding.cwd,
|
|
254
|
+
...(input.loop.receiptScope?.runsRoot
|
|
255
|
+
? { runsRoot: input.loop.receiptScope.runsRoot }
|
|
256
|
+
: {}),
|
|
257
|
+
...(input.loop.task.executionProfile
|
|
258
|
+
? { executionProfile: input.loop.task.executionProfile }
|
|
259
|
+
: {}),
|
|
260
|
+
...(input.loop.task.allowedNetworkDomains?.length
|
|
261
|
+
? { allowedNetworkDomains: [...input.loop.task.allowedNetworkDomains] }
|
|
262
|
+
: {}),
|
|
263
|
+
commands: [...input.loop.task.verificationPlan],
|
|
264
|
+
}
|
|
265
|
+
: undefined;
|
|
266
|
+
const verifierStepsActuallyPassed = expectedVerifierBinding
|
|
267
|
+
? verifierActuallyPassed({
|
|
268
|
+
passed: input.verification.status === "passed",
|
|
269
|
+
binding: input.verification.binding,
|
|
270
|
+
steps: input.verification.steps,
|
|
271
|
+
}, expectedVerifierBinding)
|
|
272
|
+
: false;
|
|
172
273
|
const effectiveVerificationStatus = input.verification.status === "passed" && !verifierStepsActuallyPassed
|
|
173
274
|
? input.verification.steps.length === 0
|
|
174
275
|
? "not_run"
|
|
@@ -182,6 +283,9 @@ export function buildVerifiedHandoff(input) {
|
|
|
182
283
|
];
|
|
183
284
|
const outcome = resolveVerifiedHandoffOutcome({
|
|
184
285
|
lifecycleState: input.loop.lifecycleState,
|
|
286
|
+
sourceStatus: input.loop.status,
|
|
287
|
+
terminationClass: lifecycleTruth.terminationClass,
|
|
288
|
+
reasonCode: lifecycleTruth.reasonCode,
|
|
185
289
|
executionStatus: input.executionStatus,
|
|
186
290
|
verificationStatus: effectiveVerificationStatus,
|
|
187
291
|
receiptIntegrity: input.receiptIntegrity.state,
|
|
@@ -218,6 +322,8 @@ export function buildVerifiedHandoff(input) {
|
|
|
218
322
|
status: input.loop.status,
|
|
219
323
|
lifecycleState: input.loop.lifecycleState,
|
|
220
324
|
},
|
|
325
|
+
lifecycleTruth,
|
|
326
|
+
fileEffects,
|
|
221
327
|
verification: {
|
|
222
328
|
status: toEvidenceStatus(effectiveVerificationStatus),
|
|
223
329
|
summary: input.verification.summary,
|
|
@@ -50,17 +50,57 @@ function formatCost(usd, provenance) {
|
|
|
50
50
|
function resolveHandoffAuthority(handoff) {
|
|
51
51
|
const executionMode = handoff.executionMode ?? "simulated";
|
|
52
52
|
const governanceClaimEligible = executionMode === "governed" && handoff.governanceClaimEligible === true;
|
|
53
|
-
const
|
|
53
|
+
const lifecycleTruth = handoff.lifecycleTruth;
|
|
54
|
+
const canonicalEvidenceSupportsVerified = governanceClaimEligible &&
|
|
55
|
+
handoff.sourceStatus.status === "completed" &&
|
|
56
|
+
handoff.sourceStatus.lifecycleState === "completed" &&
|
|
57
|
+
lifecycleTruth !== undefined &&
|
|
58
|
+
lifecycleTruth.evidenceAvailable === true &&
|
|
59
|
+
lifecycleTruth.terminationClass === "operational_exit" &&
|
|
60
|
+
lifecycleTruth.reasonCode === "goal_met" &&
|
|
61
|
+
handoff.fileEffects !== undefined &&
|
|
62
|
+
handoff.verification.status === "PASSED" &&
|
|
63
|
+
handoff.verification.checks.length > 0 &&
|
|
64
|
+
handoff.verification.checks.every((check) => check.status === "PASSED") &&
|
|
65
|
+
handoff.receiptIntegrity.state === "verified" &&
|
|
66
|
+
handoff.unresolvedWork.length === 0;
|
|
67
|
+
const effectiveOutcome = handoff.outcome === "VERIFIED" && !canonicalEvidenceSupportsVerified
|
|
54
68
|
? "NEEDS_REVIEW"
|
|
55
69
|
: handoff.outcome;
|
|
56
70
|
return { executionMode, governanceClaimEligible, effectiveOutcome };
|
|
57
71
|
}
|
|
72
|
+
function formatExecution(handoff) {
|
|
73
|
+
return `${humanize(handoff.sourceStatus.status)} / ${humanize(handoff.sourceStatus.lifecycleState)}`;
|
|
74
|
+
}
|
|
75
|
+
function formatTerminationReason(handoff) {
|
|
76
|
+
const truth = handoff.lifecycleTruth;
|
|
77
|
+
if (!truth?.evidenceAvailable)
|
|
78
|
+
return "unavailable";
|
|
79
|
+
if (!truth.reasonCode)
|
|
80
|
+
return truth.terminationClass === "none" ? "none" : "unknown";
|
|
81
|
+
return `${humanize(truth.reasonCode)}${truth.reason ? `: ${truth.reason}` : ""}`;
|
|
82
|
+
}
|
|
83
|
+
function formatExhaustedLimits(handoff) {
|
|
84
|
+
const truth = handoff.lifecycleTruth;
|
|
85
|
+
if (!truth?.evidenceAvailable)
|
|
86
|
+
return "unavailable";
|
|
87
|
+
return truth.exhaustedLimits.length > 0
|
|
88
|
+
? truth.exhaustedLimits.map(humanize).join(", ")
|
|
89
|
+
: "none";
|
|
90
|
+
}
|
|
91
|
+
function formatFileEffectList(handoff, field) {
|
|
92
|
+
const effects = handoff.fileEffects;
|
|
93
|
+
if (!effects || effects.observationStatus === "unavailable")
|
|
94
|
+
return "unavailable";
|
|
95
|
+
return effects[field].length > 0 ? effects[field].join(", ") : "none";
|
|
96
|
+
}
|
|
58
97
|
export function renderVerifiedHandoff(handoff, options = {}) {
|
|
59
98
|
const environment = options.environment ?? {};
|
|
60
99
|
const width = terminalWidth(options.width);
|
|
61
100
|
const bounded = (line) => truncateVisible(line, width);
|
|
62
101
|
const { executionMode, governanceClaimEligible, effectiveOutcome } = resolveHandoffAuthority(handoff);
|
|
63
102
|
const outcome = effectiveOutcome.replace("_", " ");
|
|
103
|
+
const labelWidth = 30;
|
|
64
104
|
const checks = handoff.verification.checks.map((check) => ({
|
|
65
105
|
check: check.command,
|
|
66
106
|
status: paint(checkSymbol(check.status) + " " + check.status.replace("_", " "), evidenceTone(check.status), environment),
|
|
@@ -72,16 +112,22 @@ export function renderVerifiedHandoff(handoff, options = {}) {
|
|
|
72
112
|
paint(outcome, outcomeTone(effectiveOutcome), environment),
|
|
73
113
|
horizontalRule(width, "━"),
|
|
74
114
|
"",
|
|
75
|
-
bounded("Task".padEnd(
|
|
76
|
-
bounded("Run".padEnd(
|
|
77
|
-
bounded("Execution Mode".padEnd(
|
|
78
|
-
bounded("Governed Claim".padEnd(
|
|
79
|
-
bounded("
|
|
80
|
-
bounded("
|
|
81
|
-
bounded("
|
|
82
|
-
bounded("
|
|
83
|
-
bounded("
|
|
84
|
-
bounded("
|
|
115
|
+
bounded("Task".padEnd(labelWidth) + handoff.task.objective),
|
|
116
|
+
bounded("Run".padEnd(labelWidth) + handoff.loopId),
|
|
117
|
+
bounded("Execution Mode".padEnd(labelWidth) + executionMode.replaceAll("_", " ")),
|
|
118
|
+
bounded("Governed Claim".padEnd(labelWidth) + (governanceClaimEligible ? "ELIGIBLE" : "INELIGIBLE")),
|
|
119
|
+
bounded("Execution".padEnd(labelWidth) + formatExecution(handoff)),
|
|
120
|
+
bounded("Verification".padEnd(labelWidth) + handoff.verification.status.replace("_", " ")),
|
|
121
|
+
bounded("Receipt Integrity".padEnd(labelWidth) + handoff.receiptIntegrity.state),
|
|
122
|
+
bounded("Termination Reason".padEnd(labelWidth) + formatTerminationReason(handoff)),
|
|
123
|
+
bounded("Exhausted Limits".padEnd(labelWidth) + formatExhaustedLimits(handoff)),
|
|
124
|
+
bounded("Touched During Execution".padEnd(labelWidth) + formatFileEffectList(handoff, "touchedDuringExecution")),
|
|
125
|
+
bounded("Net Changed at Completion".padEnd(labelWidth) + formatFileEffectList(handoff, "netChangedAtCompletion")),
|
|
126
|
+
bounded("Reverted Before Completion".padEnd(labelWidth) + formatFileEffectList(handoff, "revertedBeforeCompletion")),
|
|
127
|
+
bounded("Scope".padEnd(labelWidth) + handoff.scope.status.replaceAll("_", " ")),
|
|
128
|
+
bounded("Test Integrity".padEnd(labelWidth) + handoff.testIntegrity.verdict),
|
|
129
|
+
bounded("Attempts".padEnd(labelWidth) + handoff.usage.attempts),
|
|
130
|
+
bounded("Cost".padEnd(labelWidth) +
|
|
85
131
|
formatCost(handoff.usage.actualUsd, handoff.usage.costProvenance)),
|
|
86
132
|
];
|
|
87
133
|
if (checks.length > 0) {
|
|
@@ -123,10 +169,16 @@ export function renderVerifiedHandoffMarkdown(handoff, options = {}) {
|
|
|
123
169
|
`| Run | ${handoff.loopId} |`,
|
|
124
170
|
`| Execution Mode | ${executionMode.replaceAll("_", " ")} |`,
|
|
125
171
|
`| Governed Claim | ${governanceClaimEligible ? "ELIGIBLE" : "INELIGIBLE"} |`,
|
|
172
|
+
`| Execution | ${formatExecution(handoff)} |`,
|
|
126
173
|
`| Verification | ${handoff.verification.status.replace("_", " ")} |`,
|
|
174
|
+
`| Receipt Integrity | ${handoff.receiptIntegrity.state} |`,
|
|
175
|
+
`| Termination Reason | ${formatTerminationReason(handoff)} |`,
|
|
176
|
+
`| Exhausted Limits | ${formatExhaustedLimits(handoff)} |`,
|
|
177
|
+
`| Touched During Execution | ${formatFileEffectList(handoff, "touchedDuringExecution")} |`,
|
|
178
|
+
`| Net Changed at Completion | ${formatFileEffectList(handoff, "netChangedAtCompletion")} |`,
|
|
179
|
+
`| Reverted Before Completion | ${formatFileEffectList(handoff, "revertedBeforeCompletion")} |`,
|
|
127
180
|
`| Scope | ${handoff.scope.status.replaceAll("_", " ")} |`,
|
|
128
181
|
`| Test Integrity | ${handoff.testIntegrity.verdict} |`,
|
|
129
|
-
`| Receipt Integrity | ${handoff.receiptIntegrity.state} |`,
|
|
130
182
|
`| Attempts | ${handoff.usage.attempts} |`,
|
|
131
183
|
`| Cost | ${formatCost(handoff.usage.actualUsd, handoff.usage.costProvenance)} |`,
|
|
132
184
|
];
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/Keesan12/martin-loop",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.8.
|
|
10
|
+
"version": "0.8.4",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "@martinloop/mcp",
|
|
15
|
-
"version": "0.8.
|
|
15
|
+
"version": "0.8.4",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|