@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.
@@ -1 +1 @@
1
- export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.2";
1
+ export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.4";
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by build — do not edit by hand.
2
2
  // Source of truth: packages/mcp/package.json#version
3
- export const MARTIN_MCP_PACKAGE_VERSION = "0.8.2";
3
+ export const MARTIN_MCP_PACKAGE_VERSION = "0.8.4";
@@ -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: verification.status === "passed"
40
- ? "passed"
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: resolveReceiptIntegrity(detail.loop),
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: buildVerifiedHandoff({
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.format === "github-pr" ? "## MartinLoop Verified Handoff" : "# MartinLoop Verified Handoff",
131
+ renderVerifiedHandoffMarkdown(output.verifiedHandoff),
117
132
  "",
118
- `Outcome: ${output.verifiedHandoff.outcome}`,
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 { status: "not_run", eventCount: verificationEvents.length, ledgerEventCount: verificationLedgerEvents.length, warnings };
245
- const contradicted = latestEvidence.passed === true && warnings.some((warning) => warning.toLowerCase().includes("conflicts for the latest attempt"));
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" : latestEvidence.passed === true ? "passed" : latestEvidence.passed === false ? "failed" : "not_run",
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
- terminate(`Parent observed-usage governor failed closed: ${error instanceof Error ? error.message : String(error)}.`);
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 (!usageEventSeen && capUsd > 0 && bytesCeiling !== undefined && totalBytes > bytesCeiling) {
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 (!usageEventSeen &&
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
- state.lastTransient = observation.reasonCode;
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 = finalizeLoop(loop, handoffExitDecision, now(), idFactory);
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 finalizedLoop = finalizeLoop(loop, leashExitDecision, now(), idFactory);
317
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
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: finalizedLoop,
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 finalizedLoop = finalizeLoop(loop, secretExitDecision, now(), idFactory);
355
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
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: finalizedLoop,
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 = finalizeLoop(loop, exitDecision, now(), idFactory, routingInput);
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 = finalizeLoop(loop, preflightExitDecision, now(), idFactory);
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 finalizedLoop = finalizeLoop(loop, poisoningExitDecision, now(), idFactory);
678
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
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: finalizedLoop,
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 = finalizeLoop(loop, exitDecision, now(), idFactory);
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 finalizedLoop = finalizeLoop(loop, filesystemExitDecision, now(), idFactory);
1103
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
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: finalizedLoop,
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 finalizedLoop = finalizeLoop(loop, approvalExitDecision, now(), idFactory);
1181
- await persistLoopRecordIfSupported(input.store, finalizedLoop);
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: finalizedLoop,
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 = finalizeLoop(loop, patchExitDecision, now(), idFactory);
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 = finalizeLoop(loop, decision, now(), idFactory, routingInput);
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 decision = {
1426
- shouldExit: true,
1427
- lifecycleState: "budget_exit",
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
- // Drain pending diagnostic ledger writes before receipt signing.
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: createRunExitPayload(decision)
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
- if (result.execution?.changedFiles !== undefined) {
1618
- return result.execution.changedFiles;
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.executionStatus === "failed" ||
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 verifierStepsActuallyPassed = input.verification.steps.length > 0 &&
167
- input.verification.steps.every((step) => step.launched === true &&
168
- step.completed === true &&
169
- step.crashed === false &&
170
- step.timedOut !== true &&
171
- step.exitCode === 0);
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 effectiveOutcome = handoff.outcome === "VERIFIED" && !governanceClaimEligible
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(20) + handoff.task.objective),
76
- bounded("Run".padEnd(20) + handoff.loopId),
77
- bounded("Execution Mode".padEnd(20) + executionMode.replaceAll("_", " ")),
78
- bounded("Governed Claim".padEnd(20) + (governanceClaimEligible ? "ELIGIBLE" : "INELIGIBLE")),
79
- bounded("Verification".padEnd(20) + handoff.verification.status.replace("_", " ")),
80
- bounded("Scope".padEnd(20) + handoff.scope.status.replaceAll("_", " ")),
81
- bounded("Test Integrity".padEnd(20) + handoff.testIntegrity.verdict),
82
- bounded("Receipt Integrity".padEnd(20) + handoff.receiptIntegrity.state),
83
- bounded("Attempts".padEnd(20) + handoff.usage.attempts),
84
- bounded("Cost".padEnd(20) +
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
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.2",
10
+ "version": "0.8.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.8.2",
15
+ "version": "0.8.4",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }