@martinloop/mcp 0.8.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +2 -0
  2. package/dist/package-version.d.ts +1 -1
  3. package/dist/package-version.js +1 -1
  4. package/dist/server-validation.js +2 -3
  5. package/dist/server.d.ts +1 -0
  6. package/dist/server.js +181 -24
  7. package/dist/tools/get-status.d.ts +3 -0
  8. package/dist/tools/get-status.js +11 -2
  9. package/dist/tools/logs.d.ts +2 -0
  10. package/dist/tools/logs.js +25 -3
  11. package/dist/tools/run-controls.d.ts +51 -6
  12. package/dist/tools/run-controls.js +905 -35
  13. package/dist/tools/run-loop.js +7 -0
  14. package/dist/vendor/adapters/process-supervisor.d.ts +12 -0
  15. package/dist/vendor/adapters/process-supervisor.js +40 -2
  16. package/dist/vendor/adapters/verifier-only.js +4 -0
  17. package/dist/vendor/contracts/index.d.ts +8 -0
  18. package/dist/vendor/contracts/index.js +1 -0
  19. package/dist/vendor/contracts/swarm-timing.d.ts +7 -0
  20. package/dist/vendor/contracts/swarm-timing.js +22 -0
  21. package/dist/vendor/core/index.d.ts +37 -2
  22. package/dist/vendor/core/index.js +120 -14
  23. package/dist/vendor/core/persistence/index.d.ts +1 -1
  24. package/dist/vendor/core/persistence/index.js +1 -1
  25. package/dist/vendor/core/persistence/integrity.d.ts +1 -0
  26. package/dist/vendor/core/persistence/integrity.js +1 -1
  27. package/dist/vendor/core/policy.js +11 -1
  28. package/dist/vendor/core/resume.d.ts +25 -1
  29. package/dist/vendor/core/resume.js +187 -13
  30. package/dist/vendor/core/rollback.d.ts +14 -0
  31. package/dist/vendor/core/rollback.js +91 -34
  32. package/dist/vendor/core/swarm/live-runtime.js +2 -0
  33. package/dist/vendor/core/verified-handoff.js +62 -2
  34. package/package.json +3 -7
  35. package/server.json +2 -2
@@ -4,6 +4,7 @@ import { normalizeSafePathPatterns, resolveSafeRepoRoot } from "../server-valida
4
4
  import { MartinToolError } from "./tool-errors.js";
5
5
  import { buildArtifactSummary, buildVerificationSummary, buildLoopPreview, buildRunRecordPaths, getEngineAvailability, resolveExecutionMode } from "./tool-support.js";
6
6
  import { normalizeLoopBudget } from "./workflow-governance.js";
7
+ import { createFileRunControlSource } from "./run-controls.js";
7
8
  let proofModeVerifierSpawnImpl;
8
9
  let runStoreOverrideForTests;
9
10
  export function __setProofModeVerifierSpawnImplForTests(spawnImpl) {
@@ -210,6 +211,12 @@ export async function runLoopTool(input) {
210
211
  workspaceId: input.workspaceId ?? "ws_mcp",
211
212
  projectId: input.projectId ?? "proj_mcp",
212
213
  store: runStoreOverrideForTests ?? createFileRunStore({ runsRoot }),
214
+ // The in-memory store override is a verification-only composition with no
215
+ // durable run directory to serialize. Production always couples the file
216
+ // store and file control source over the same runs root.
217
+ runControlSource: runStoreOverrideForTests
218
+ ? undefined
219
+ : createFileRunControlSource(runsRoot),
213
220
  receiptScope,
214
221
  task: {
215
222
  title: input.objective.slice(0, 100),
@@ -64,4 +64,16 @@ export declare function spawnSupervisedProcessForTest(command: string, args: rea
64
64
  * @internal - deterministic pure classification for process-supervisor tests
65
65
  */
66
66
  export declare function classifyPosixProcessGroupSnapshot(output: string, ownedGroupPid: number): "closed" | "alive" | "absent" | "invalid";
67
+ type WindowsProcessTreeRow = {
68
+ ProcessId: number;
69
+ ParentProcessId: number;
70
+ };
71
+ /**
72
+ * Classify descendants that remain visible after a failed termination attempt.
73
+ * Two point-in-time snapshots cannot prove closure across the interval because
74
+ * a descendant may be created and reparented between them. Absence is therefore
75
+ * indeterminate and must retain the original termination failure.
76
+ * @internal - deterministic pure classification for process-supervisor tests
77
+ */
78
+ export declare function classifyWindowsDescendantPostcondition(initialRows: WindowsProcessTreeRow[], postTerminationRows: WindowsProcessTreeRow[], rootPid: number): "closed" | "alive" | "invalid";
67
79
  export {};
@@ -456,6 +456,39 @@ function isErrno(error, code) {
456
456
  return typeof error === "object" && error !== null && "code" in error
457
457
  && String(error.code) === code;
458
458
  }
459
+ /**
460
+ * Classify descendants that remain visible after a failed termination attempt.
461
+ * Two point-in-time snapshots cannot prove closure across the interval because
462
+ * a descendant may be created and reparented between them. Absence is therefore
463
+ * indeterminate and must retain the original termination failure.
464
+ * @internal - deterministic pure classification for process-supervisor tests
465
+ */
466
+ export function classifyWindowsDescendantPostcondition(initialRows, postTerminationRows, rootPid) {
467
+ if (!isOwnedPid(rootPid))
468
+ return "invalid";
469
+ const allRows = [...initialRows, ...postTerminationRows];
470
+ if (allRows.some((row) => !isOwnedPid(row.ProcessId) || !Number.isSafeInteger(row.ParentProcessId))) {
471
+ return "invalid";
472
+ }
473
+ const ownedLineage = new Set([rootPid]);
474
+ const expandLineage = (rows) => {
475
+ let changed = true;
476
+ while (changed) {
477
+ changed = false;
478
+ for (const row of rows) {
479
+ if (ownedLineage.has(row.ParentProcessId) && !ownedLineage.has(row.ProcessId)) {
480
+ ownedLineage.add(row.ProcessId);
481
+ changed = true;
482
+ }
483
+ }
484
+ }
485
+ };
486
+ expandLineage(initialRows);
487
+ expandLineage(postTerminationRows);
488
+ return postTerminationRows.some((row) => ownedLineage.has(row.ProcessId))
489
+ ? "alive"
490
+ : "invalid";
491
+ }
459
492
  async function terminateWindowsDescendantsAfterRootExit(rootPid, graceMs, waitImpl) {
460
493
  let rows;
461
494
  try {
@@ -471,11 +504,16 @@ async function terminateWindowsDescendantsAfterRootExit(rootPid, graceMs, waitIm
471
504
  childrenByParent.set(row.ParentProcessId, children);
472
505
  }
473
506
  const directChildren = childrenByParent.get(rootPid) ?? [];
507
+ let taskkillFailure;
474
508
  for (const childPid of directChildren) {
475
509
  const failure = await runTaskkill(childPid, spawnProcess, graceMs, waitImpl);
476
- if (failure)
477
- return failure;
510
+ taskkillFailure ??= failure;
478
511
  }
512
+ // Once taskkill has failed, a later process-table snapshot cannot prove the
513
+ // complete descendant set closed. Return the bounded failure immediately;
514
+ // starting another PowerShell query here could otherwise stall cleanup.
515
+ if (taskkillFailure)
516
+ return taskkillFailure;
479
517
  return { state: "closed" };
480
518
  }
481
519
  function readWindowsProcessTree() {
@@ -49,6 +49,10 @@ export function createVerifierOnlyAdapter(options = {}) {
49
49
  providerId: "verifier",
50
50
  model: "verify-only",
51
51
  transport: "cli",
52
+ budgetPreflight: {
53
+ pricePerMTokenUsd: 0,
54
+ basis: "Verifier-only execution launches no model provider."
55
+ },
52
56
  capabilities: createAdapterCapabilities({
53
57
  usageSettlement: true,
54
58
  diffArtifacts: true,
@@ -497,6 +497,12 @@ export interface RollbackBoundaryArtifact {
497
497
  trackedDirtyFiles: string[];
498
498
  untrackedFiles: string[];
499
499
  snapshots: RollbackFileSnapshot[];
500
+ /**
501
+ * Exact repo-relative roots owned by MartinLoop persistence for this run.
502
+ * These roots are not agent workspace mutations and must remain excluded
503
+ * from observation and rollback after the boundary is persisted/resumed.
504
+ */
505
+ managedPathExclusions?: string[];
500
506
  /**
501
507
  * Git-ignored files present at the boundary. Dependency and build output
502
508
  * directories are recorded as a single collapsed `dir/` entry.
@@ -557,4 +563,6 @@ export type { ChainIntegrityState, ContextCircuitBreakResult, ContextExclusionDe
557
563
  export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
558
564
  export type { MissionStatus, MissionDecision, MissionBudget, MissionCost, MissionRunLink, MissionRunRole, MissionApproval, MissionOutcome, MissionEvent, MissionEventKind, MissionRecord, MissionDraft } from './mission.js';
559
565
  export { SWARM_LIVE_ENGINES, SWARM_ORCHESTRATION_STRATEGIES, SWARM_EVENT_TYPES, SWARM_PARENT_RECEIPT_SCHEMA_VERSION, SWARM_SCHEMA_VERSION, computeSwarmLivePlanHash, createSwarmLivePlan, createSwarmRunRecord, normalizeSwarmPathPattern, swarmConcretePathMatchesPattern, swarmPathPatternContains, swarmPathPatternsOverlap, validateSwarmConcretePath, validateSwarmChildReceiptLink, validateSwarmDeterministicDemoReceiptEvidence, validateSwarmChildContract, validateSwarmLiveEvent, validateSwarmLivePlan, validateSwarmLiveRevision, validateSwarmParentContract, validateSwarmRunRecord } from "./swarm.js";
566
+ export { SWARM_CHILD_MAX_WALL_CLOCK_MS, SWARM_PARENT_VERIFIER_ALLOWANCE_MS, resolveSwarmDeadlineMs, } from "./swarm-timing.js";
567
+ export type { SwarmExecutionShape } from "./swarm-timing.js";
560
568
  export type { SwarmAgentRecord, SwarmAgentStatus, SwarmBudgetLedger, SwarmBudgetLease, SwarmBudgetUsage, SwarmCandidate, SwarmChildContract, SwarmChildReceiptLink, SwarmChildWorkspaceRecord, SwarmCleanupRecord, SwarmCleanupState, SwarmConcretePathResult, SwarmConflictRecord, SwarmDependencyWaiver, SwarmDemoReferentialBindingState, SwarmDeterministicDemoReceiptEvidence, SwarmEvent, SwarmEventType, SwarmGlobalVerification, SwarmGlobalVerificationCommandState, SwarmIntegrationWorkspaceRecord, SwarmIntegrationStrategy, SwarmLeaseStatus, SwarmLiveEngine, SwarmLiveEngineProfile, SwarmLiveEvent, SwarmLivePlan, SwarmLivePlanDraft, SwarmLiveRevision, SwarmOrchestrationStrategy, SwarmOutcome, SwarmOutcomeState, SwarmParentContract, SwarmParentReceipt, SwarmPatchAdmission, SwarmPatchAdmissionReasonCode, SwarmPatchAdmissionState, SwarmPathPatternResult, SwarmPermissions, SwarmProcessCloseState, SwarmRecoveryPolicy, SwarmReceiptBlockedAction, SwarmReceiptReassignment, SwarmRunDraft, SwarmRunRecord, SwarmScope, SwarmStopDisposition, SwarmStopPolicy, SwarmTaskNode, SwarmTaskStatus, SwarmValidationError, SwarmValidationResult, SwarmVerifierMutationState, SwarmVerifierSubprocessResult, SwarmVerificationRecord, SwarmVerifierAuthority, SwarmVerifierWorkspaceRecord, SwarmWorkspaceKind, SwarmWorkspaceRecord, SwarmWorkspaceRemovalState, SwarmWorkspaceState } from "./swarm.js";
@@ -283,3 +283,4 @@ export { EVIDENCE_STATUSES, EXECUTION_MODES, TEST_INTEGRITY_STATUSES, TEST_INTEG
283
283
  export { HANDOFF_SCHEMA_VERSION } from "./context-handoff.js";
284
284
  export { MISSION_SCHEMA_VERSION, MISSION_STATUSES, ALLOWED_MISSION_TRANSITIONS, createMissionRecord, isMissionTransitionAllowed } from './mission.js';
285
285
  export { SWARM_LIVE_ENGINES, SWARM_ORCHESTRATION_STRATEGIES, SWARM_EVENT_TYPES, SWARM_PARENT_RECEIPT_SCHEMA_VERSION, SWARM_SCHEMA_VERSION, computeSwarmLivePlanHash, createSwarmLivePlan, createSwarmRunRecord, normalizeSwarmPathPattern, swarmConcretePathMatchesPattern, swarmPathPatternContains, swarmPathPatternsOverlap, validateSwarmConcretePath, validateSwarmChildReceiptLink, validateSwarmDeterministicDemoReceiptEvidence, validateSwarmChildContract, validateSwarmLiveEvent, validateSwarmLivePlan, validateSwarmLiveRevision, validateSwarmParentContract, validateSwarmRunRecord } from "./swarm.js";
286
+ export { SWARM_CHILD_MAX_WALL_CLOCK_MS, SWARM_PARENT_VERIFIER_ALLOWANCE_MS, resolveSwarmDeadlineMs, } from "./swarm-timing.js";
@@ -0,0 +1,7 @@
1
+ export declare const SWARM_CHILD_MAX_WALL_CLOCK_MS = 240000;
2
+ export declare const SWARM_PARENT_VERIFIER_ALLOWANCE_MS = 120000;
3
+ export interface SwarmExecutionShape {
4
+ agents: number;
5
+ maxConcurrency: number;
6
+ }
7
+ export declare function resolveSwarmDeadlineMs(shape: SwarmExecutionShape): number;
@@ -0,0 +1,22 @@
1
+ export const SWARM_CHILD_MAX_WALL_CLOCK_MS = 240_000;
2
+ export const SWARM_PARENT_VERIFIER_ALLOWANCE_MS = 120_000;
3
+ const NODE_MAX_TIMER_DELAY_MS = 2_147_483_647;
4
+ export function resolveSwarmDeadlineMs(shape) {
5
+ assertPositiveSafeInteger(shape.agents, "agents");
6
+ assertPositiveSafeInteger(shape.maxConcurrency, "maxConcurrency");
7
+ if (shape.maxConcurrency > shape.agents) {
8
+ throw new Error("maxConcurrency cannot exceed agents");
9
+ }
10
+ const executionWaves = Math.ceil(shape.agents / shape.maxConcurrency);
11
+ const deadlineMs = (executionWaves * SWARM_CHILD_MAX_WALL_CLOCK_MS
12
+ + SWARM_PARENT_VERIFIER_ALLOWANCE_MS);
13
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs > NODE_MAX_TIMER_DELAY_MS) {
14
+ throw new Error("derived deadline exceeds the Node timer maximum");
15
+ }
16
+ return deadlineMs;
17
+ }
18
+ function assertPositiveSafeInteger(value, label) {
19
+ if (!Number.isSafeInteger(value) || value <= 0) {
20
+ throw new Error(`${label} must be a positive safe integer`);
21
+ }
22
+ }
@@ -7,7 +7,7 @@ import { type RunStore } from "./persistence/index.js";
7
7
  import { type ExitPolicyOverrides } from "./exits.js";
8
8
  import { type ExitSignalSource } from "./exit-signal.js";
9
9
  import { type VerifierExecutionBinding } from "./verified-handoff.js";
10
- import { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
10
+ import { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
11
11
  export type { AgentExecutionIntent, ApprovalPolicy, BudgetPreflightEstimate, BudgetSettlement, CostProvenance, EvidenceVector, ExecutionProfile, FailureClass, InterventionType, PatchDecision, PatchDecisionArtifact, PatchDecisionReasonCode, PatchScore, MutationMode, RollbackBoundaryArtifact, RollbackBoundaryStrategy, RollbackFileSnapshot, RollbackOutcomeArtifact, RollbackOutcomeStatus, PolicyPhase, CallStage, AgentRole, FirstDelta, RoutingEconomics } from "../contracts/index.js";
12
12
  export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
13
13
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary };
@@ -23,7 +23,7 @@ export type { LocalIdentityAuthority } from "./identity.js";
23
23
  export { compileExecutionPolicy } from "./policy-compiler.js";
24
24
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
25
25
  export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
26
- export { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
26
+ export { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
27
27
  export type { AvoidedUsdInput } from "./savings.js";
28
28
  export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
29
29
  export type { RouteDecision, RouteClassificationInput } from "./routing.js";
@@ -206,6 +206,8 @@ export interface MartinAdapter {
206
206
  budgetPreflight?: {
207
207
  /** Estimated token floor required before the adapter can report first-turn usage. */
208
208
  minimumViableTokens?: number;
209
+ /** Adapter-specific provider price used by the admission estimate. */
210
+ pricePerMTokenUsd?: number;
209
211
  basis?: string;
210
212
  };
211
213
  capabilities?: {
@@ -291,6 +293,39 @@ export interface RunMartinInput {
291
293
  observedUsageGovernor?: MartinObservedUsageGovernor;
292
294
  /** Optional parent cancellation signal. Relayed into the active governed attempt. */
293
295
  signal?: AbortSignal;
296
+ /** Optional durable cooperative pause/continue source. */
297
+ runControlSource?: RunControlSource;
298
+ /** Poll interval while a cooperative pause is applied (default 250 ms). */
299
+ runControlPollIntervalMs?: number;
300
+ }
301
+ export interface RunControlCommand {
302
+ controlId: string;
303
+ loopId: string;
304
+ action: "pause" | "continue";
305
+ requestedAt: string;
306
+ /** Explicit local-process boundary; not an authenticated human identity. */
307
+ principal: "local_mcp_client";
308
+ receiptHash: string;
309
+ reason?: string;
310
+ expectedPreviousControlId?: string;
311
+ }
312
+ export interface RunControlSnapshot {
313
+ pause?: RunControlCommand;
314
+ continuedBy?: RunControlCommand;
315
+ }
316
+ export interface RunControlApplication {
317
+ loopId: string;
318
+ controlId: string;
319
+ action: "pause_applied" | "continue_applied";
320
+ appliedAt: string;
321
+ relatedControlId?: string;
322
+ receiptHash: string;
323
+ }
324
+ export interface RunControlSource {
325
+ poll(runId: string): Promise<RunControlSnapshot>;
326
+ recordApplication(application: RunControlApplication): Promise<void>;
327
+ /** Serialize terminal intent against new control receipts before persistence. */
328
+ recordTerminalIntent?(runId: string): Promise<void>;
294
329
  }
295
330
  export interface RunMartinResult {
296
331
  loop: LoopRecord;
@@ -2,10 +2,10 @@ import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs, ap
2
2
  import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, isSafeUnverifiedDiscard, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe } from "./policy.js";
3
3
  import { evaluateChangeApprovalLeash, evaluateFilesystemLeash, evaluateSecretLeash, redactSecretsFromText, resolveExecutionProfile, evaluateVerificationLeash } from "./leash.js";
4
4
  import { buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations } from "./grounding.js";
5
- import { captureRollbackBoundary, findEscapingLinks, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
5
+ import { authenticateManagedPathExclusions, captureRollbackBoundary, filterManagedRollbackPaths, findEscapingLinks, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
6
6
  import { findWeakenedTestFiles } from "./test-integrity.js";
7
7
  import { compilePromptPacket } from "./compiler.js";
8
- import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
8
+ import { makeLedgerEvent, resolveReceiptIntegrityRoot, resolveRunsRoot, runDir } from "./persistence/index.js";
9
9
  import { createDefaultExitPolicy, evaluateExitPolicy, hashProgressState, toLegacyExitDecision } from "./exits.js";
10
10
  import { createFileExitSignalSource, isTerminalExitSignal, startExitSignalMonitor } from "./exit-signal.js";
11
11
  import { persistTerminationEnvelope } from "./termination-store.js";
@@ -14,7 +14,7 @@ import { runContextIntegrityPrecheck } from "./context-integrity.js";
14
14
  import { decideCircuitBreak } from "./trajectory.js";
15
15
  import { verifyContextHandoff, decideContextCircuitBreak } from "./context-handoff.js";
16
16
  import { verifierActuallyPassed, } from "./verified-handoff.js";
17
- import { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
17
+ import { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure } from "./resume.js";
18
18
  export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
19
19
  export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary };
20
20
  export { describeMalformedVerifier } from "./verifier-input.js";
@@ -24,7 +24,7 @@ export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, ver
24
24
  export { compileExecutionPolicy } from "./policy-compiler.js";
25
25
  export { assessTrajectory, decideCircuitBreak } from "./trajectory.js";
26
26
  export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
27
- export { acquireRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
27
+ export { acquireRunExecutionLease, readActiveRunExecutionLease, evaluateRunResumability, loadResumeCandidateBoundary, loadResumeVerifierFailure };
28
28
  // ─── Routing economics ─────────────────────────────────────────────────────
29
29
  export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
30
30
  // ─── Test Integrity Validation ──────────────────────────────────────────────
@@ -217,6 +217,7 @@ export async function runMartin(input) {
217
217
  runsRoot: input.store.runsRoot,
218
218
  runId: loop.loopId,
219
219
  owner: resumeLoop ? "runMartin:resume" : "runMartin",
220
+ runControlCapable: input.runControlSource !== undefined,
220
221
  now: () => loop.updatedAt
221
222
  })
222
223
  : undefined;
@@ -236,6 +237,9 @@ export async function runMartin(input) {
236
237
  }, { now: now(), idFactory });
237
238
  const adapterChain = [input.adapter, ...(input.fallbackAdapters ?? [])];
238
239
  const runsRoot = input.store?.runsRoot ?? resolveRunsRoot();
240
+ const managedRollbackRoots = input.store?.runsRoot
241
+ ? [input.store.runsRoot, resolveReceiptIntegrityRoot()]
242
+ : [];
239
243
  const observesWorkspaceMutations = task.repoRoot !== undefined &&
240
244
  adapterChain.some((adapter) => adapter.metadata.capabilities?.workspaceMutations !== false);
241
245
  const touchedDuringExecution = new Set();
@@ -247,7 +251,8 @@ export async function runMartin(input) {
247
251
  try {
248
252
  completionBoundary = await captureRollbackBoundary({
249
253
  repoRoot: task.repoRoot,
250
- capturedAt: loop.createdAt
254
+ capturedAt: loop.createdAt,
255
+ managedRoots: managedRollbackRoots
251
256
  });
252
257
  if (!completionBoundary) {
253
258
  completionBoundaryReason = "A run-wide file-effect observation boundary was not available.";
@@ -265,6 +270,7 @@ export async function runMartin(input) {
265
270
  const fileEffects = observeRunFileEffects({
266
271
  repoRoot: task.repoRoot,
267
272
  boundary: completionBoundary,
273
+ managedRoots: managedRollbackRoots,
268
274
  touchedDuringExecution,
269
275
  unavailableReason: completionBoundaryReason
270
276
  });
@@ -675,7 +681,84 @@ export async function runMartin(input) {
675
681
  const evaluation = evaluateExitPolicy(exitPolicy, currentExitSnapshot(phase, result));
676
682
  if (!evaluation.shouldExit)
677
683
  return undefined;
678
- return await selectTerminalEvaluation(evaluation);
684
+ const terminal = await selectTerminalEvaluation(evaluation);
685
+ await input.runControlSource?.recordTerminalIntent?.(loop.loopId);
686
+ return terminal;
687
+ }
688
+ async function waitForControlPoll(intervalMs) {
689
+ if (runController.signal.aborted)
690
+ return;
691
+ await new Promise((resolve) => {
692
+ let settled = false;
693
+ const finish = () => {
694
+ if (settled)
695
+ return;
696
+ settled = true;
697
+ clearTimeout(timer);
698
+ runController.signal.removeEventListener("abort", finish);
699
+ resolve();
700
+ };
701
+ const timer = setTimeout(finish, intervalMs);
702
+ timer.unref?.();
703
+ runController.signal.addEventListener("abort", finish, { once: true });
704
+ if (runController.signal.aborted)
705
+ finish();
706
+ });
707
+ }
708
+ async function awaitCooperativeRunControl() {
709
+ const source = input.runControlSource;
710
+ if (!source)
711
+ return undefined;
712
+ const initial = await source.poll(loop.loopId);
713
+ const pause = initial.pause;
714
+ if (!pause)
715
+ return undefined;
716
+ if (pause.loopId !== loop.loopId || pause.action !== "pause") {
717
+ throw new Error("Run control integrity check failed before pause application.");
718
+ }
719
+ await source.recordApplication({
720
+ loopId: loop.loopId,
721
+ controlId: pause.controlId,
722
+ action: "pause_applied",
723
+ appliedAt: now(),
724
+ receiptHash: pause.receiptHash
725
+ });
726
+ const pollIntervalMs = input.runControlPollIntervalMs ?? 250;
727
+ while (true) {
728
+ const terminal = await evaluateCheckpoint("pre_attempt");
729
+ if (terminal)
730
+ return terminal;
731
+ await waitForControlPoll(pollIntervalMs);
732
+ // Cancellation aborts the wait immediately. Reconcile terminal state
733
+ // before polling cooperative receipts because the production control
734
+ // source intentionally clears pause/continue after a cancel receipt.
735
+ const awakenedTerminal = await evaluateCheckpoint("pre_attempt");
736
+ if (awakenedTerminal)
737
+ return awakenedTerminal;
738
+ const snapshot = await source.poll(loop.loopId);
739
+ if (snapshot.pause) {
740
+ if (snapshot.pause.controlId !== pause.controlId || snapshot.pause.loopId !== loop.loopId) {
741
+ throw new Error("Run control integrity check failed while pause was applied.");
742
+ }
743
+ continue;
744
+ }
745
+ const continuedBy = snapshot.continuedBy;
746
+ if (!continuedBy ||
747
+ continuedBy.loopId !== loop.loopId ||
748
+ continuedBy.action !== "continue" ||
749
+ continuedBy.expectedPreviousControlId !== pause.controlId) {
750
+ throw new Error("Run control integrity check failed while releasing pause.");
751
+ }
752
+ await source.recordApplication({
753
+ loopId: loop.loopId,
754
+ controlId: continuedBy.controlId,
755
+ action: "continue_applied",
756
+ relatedControlId: pause.controlId,
757
+ appliedAt: now(),
758
+ receiptHash: continuedBy.receiptHash
759
+ });
760
+ return await evaluateCheckpoint("pre_attempt");
761
+ }
679
762
  }
680
763
  /**
681
764
  * Blocker 1 / Blocker 2 — Authoritative retained-candidate terminal restoration.
@@ -711,6 +794,7 @@ export async function runMartin(input) {
711
794
  const rollbackOutcome = await restoreRollbackBoundary({
712
795
  repoRoot: task.repoRoot,
713
796
  boundary: capturedBoundary,
797
+ managedRoots: managedRollbackRoots,
714
798
  restoredAt,
715
799
  decision: "DISCARD"
716
800
  });
@@ -867,6 +951,9 @@ export async function runMartin(input) {
867
951
  if (preRunTerminal !== undefined)
868
952
  return await terminateRun(preRunTerminal);
869
953
  while (loop.attempts.length < loop.budget.maxIterations) {
954
+ const controlTerminal = await awaitCooperativeRunControl();
955
+ if (controlTerminal !== undefined)
956
+ return await terminateRun(controlTerminal);
870
957
  const distilled = distillContext(loop, {
871
958
  maxRecentAttempts: useCompressedContext ? 1 : (input.maxRecentAttempts ?? 3)
872
959
  });
@@ -880,6 +967,7 @@ export async function runMartin(input) {
880
967
  remainingBudgetUsd: distilled.constraints.remainingBudgetUsd,
881
968
  remainingTokens: distilled.constraints.remainingTokens,
882
969
  minimumViableTokens: executingAdapter.metadata.budgetPreflight?.minimumViableTokens,
970
+ pricePerMTokenUsd: executingAdapter.metadata.budgetPreflight?.pricePerMTokenUsd,
883
971
  // D2: pass remaining iterations so the preflight can compute a budget-aware cap
884
972
  // rather than using the old rigid maxUsd * 0.25 that blocked viable attempts.
885
973
  remainingIterations: distilled.constraints.remainingIterations
@@ -1117,7 +1205,8 @@ export async function runMartin(input) {
1117
1205
  const rollbackBoundary = tracksWorkspaceMutations
1118
1206
  ? await captureRollbackBoundary({
1119
1207
  repoRoot: request.context.repoRoot,
1120
- capturedAt: attemptStartedAt
1208
+ capturedAt: attemptStartedAt,
1209
+ managedRoots: managedRollbackRoots
1121
1210
  })
1122
1211
  : undefined;
1123
1212
  // Persist attempt.started state before blocking on provider execution.
@@ -1352,10 +1441,14 @@ export async function runMartin(input) {
1352
1441
  let weakenedTestFiles = [];
1353
1442
  try {
1354
1443
  changedFiles = tracksWorkspaceMutations
1355
- ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
1444
+ ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary, managedRollbackRoots)
1356
1445
  : [];
1357
1446
  ignoredChanges = tracksWorkspaceMutations
1358
- ? listIgnoredFileChangesSinceBoundary({ repoRoot: request.context.repoRoot, boundary: rollbackBoundary })
1447
+ ? listIgnoredFileChangesSinceBoundary({
1448
+ repoRoot: request.context.repoRoot,
1449
+ boundary: rollbackBoundary,
1450
+ managedRoots: managedRollbackRoots
1451
+ })
1359
1452
  : ignoredChanges;
1360
1453
  escapingLinks =
1361
1454
  tracksWorkspaceMutations && request.context.repoRoot
@@ -1382,6 +1475,7 @@ export async function runMartin(input) {
1382
1475
  rollbackOutcome = (await restoreRollbackBoundary({
1383
1476
  repoRoot: request.context.repoRoot,
1384
1477
  boundary: rollbackBoundary,
1478
+ managedRoots: managedRollbackRoots,
1385
1479
  restoredAt: attemptCompletedAt,
1386
1480
  decision: "DISCARD",
1387
1481
  removePaths: ignoredChanges.created,
@@ -1495,6 +1589,7 @@ export async function runMartin(input) {
1495
1589
  rollbackOutcome = await restoreRollbackBoundary({
1496
1590
  repoRoot: request.context.repoRoot,
1497
1591
  boundary: rollbackBoundary,
1592
+ managedRoots: managedRollbackRoots,
1498
1593
  restoredAt: attemptCompletedAt,
1499
1594
  decision: patchDecision.decision,
1500
1595
  removePaths: violatingIgnoredPaths(filesystemDecision.violations, ignoredChanges.created),
@@ -1592,6 +1687,7 @@ export async function runMartin(input) {
1592
1687
  rollbackOutcome = await restoreRollbackBoundary({
1593
1688
  repoRoot: request.context.repoRoot,
1594
1689
  boundary: rollbackBoundary,
1690
+ managedRoots: managedRollbackRoots,
1595
1691
  restoredAt: attemptCompletedAt,
1596
1692
  decision: patchDecision.decision
1597
1693
  }) ?? undefined;
@@ -1724,6 +1820,7 @@ export async function runMartin(input) {
1724
1820
  rollbackOutcome = await restoreRollbackBoundary({
1725
1821
  repoRoot: request.context.repoRoot,
1726
1822
  boundary: rollbackBoundary,
1823
+ managedRoots: managedRollbackRoots,
1727
1824
  restoredAt: attemptCompletedAt,
1728
1825
  decision: patchDecision?.decision ?? "DISCARD"
1729
1826
  });
@@ -1733,6 +1830,7 @@ export async function runMartin(input) {
1733
1830
  rollbackOutcome = await restoreRollbackBoundary({
1734
1831
  repoRoot: request.context.repoRoot,
1735
1832
  boundary: rollbackBoundary,
1833
+ managedRoots: managedRollbackRoots,
1736
1834
  restoredAt: attemptCompletedAt,
1737
1835
  decision: "DISCARD"
1738
1836
  });
@@ -2001,6 +2099,7 @@ export async function runMartin(input) {
2001
2099
  await restoreRollbackBoundary({
2002
2100
  repoRoot: task.repoRoot,
2003
2101
  boundary: preCandidateBoundary,
2102
+ managedRoots: managedRollbackRoots,
2004
2103
  restoredAt: now(),
2005
2104
  decision: "DISCARD"
2006
2105
  });
@@ -2091,11 +2190,11 @@ function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInpu
2091
2190
  };
2092
2191
  }
2093
2192
  function observeRunFileEffects(input) {
2094
- const touchedDuringExecution = normalizeFileEffectPaths(input.touchedDuringExecution);
2193
+ const observedTouchedDuringExecution = normalizeFileEffectPaths(input.touchedDuringExecution);
2095
2194
  if (!input.repoRoot || !input.boundary) {
2096
2195
  return {
2097
2196
  observationStatus: "unavailable",
2098
- touchedDuringExecution,
2197
+ touchedDuringExecution: observedTouchedDuringExecution,
2099
2198
  netChangedAtCompletion: [],
2100
2199
  revertedBeforeCompletion: [],
2101
2200
  observationReason: input.unavailableReason ??
@@ -2103,9 +2202,12 @@ function observeRunFileEffects(input) {
2103
2202
  };
2104
2203
  }
2105
2204
  try {
2205
+ const exclusions = authenticateManagedPathExclusions(input.repoRoot, input.boundary, input.managedRoots);
2206
+ const touchedDuringExecution = filterManagedRollbackPaths(observedTouchedDuringExecution, exclusions);
2106
2207
  const netChangedAtCompletion = normalizeFileEffectPaths(listAttemptChangedFilesSinceBoundary({
2107
2208
  repoRoot: input.repoRoot,
2108
2209
  boundary: input.boundary,
2210
+ managedRoots: input.managedRoots,
2109
2211
  includeBaselineSnapshotChanges: true
2110
2212
  }));
2111
2213
  const netChanged = new Set(netChangedAtCompletion);
@@ -2119,7 +2221,7 @@ function observeRunFileEffects(input) {
2119
2221
  catch (error) {
2120
2222
  return {
2121
2223
  observationStatus: "unavailable",
2122
- touchedDuringExecution,
2224
+ touchedDuringExecution: observedTouchedDuringExecution,
2123
2225
  netChangedAtCompletion: [],
2124
2226
  revertedBeforeCompletion: [],
2125
2227
  observationReason: `Final file-effect observation was unavailable: ${fileEffectErrorMessage(error)}`
@@ -2234,11 +2336,15 @@ function violatingIgnoredPaths(violations, ignoredPaths) {
2234
2336
  const violating = new Set(violations.map((violation) => violation.file?.replace(/\/$/u, "")).filter(Boolean));
2235
2337
  return ignoredPaths.filter((filePath) => violating.has(filePath.replace(/\/$/u, "")));
2236
2338
  }
2237
- function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
2238
- const adapterReported = normalizeFileEffectPaths(result.execution?.changedFiles ?? []);
2339
+ function resolveChangedFiles(result, repoRoot, rollbackBoundary, managedRoots = []) {
2340
+ const exclusions = repoRoot && rollbackBoundary
2341
+ ? authenticateManagedPathExclusions(repoRoot, rollbackBoundary, managedRoots)
2342
+ : [];
2343
+ const adapterReported = filterManagedRollbackPaths(normalizeFileEffectPaths(result.execution?.changedFiles ?? []), exclusions);
2239
2344
  const gitObserved = listAttemptChangedFilesSinceBoundary({
2240
2345
  repoRoot,
2241
2346
  boundary: rollbackBoundary,
2347
+ managedRoots,
2242
2348
  includeBaselineSnapshotChanges: true
2243
2349
  });
2244
2350
  return normalizeFileEffectPaths([...adapterReported, ...gitObserved]);
@@ -1,6 +1,6 @@
1
1
  export { makeLedgerEvent } from "./ledger.js";
2
2
  export type { LedgerEvent, LedgerEventDraft, LedgerEventKind } from "./ledger.js";
3
- export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
3
+ export { resolveReceiptIntegrityRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
4
4
  export type { ReceiptIntegrityChainEntry, StoredReceiptIntegrityMaterial } from "./integrity.js";
5
5
  export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
6
6
  export type { AttemptArtifacts, RunContract, RunStore } from "./store.js";
@@ -1,5 +1,5 @@
1
1
  export { makeLedgerEvent } from "./ledger.js";
2
- export { resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
2
+ export { resolveReceiptIntegrityRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./integrity.js";
3
3
  export { artifactDir, createFileRunStore, resolveRunsRoot, runDir } from "./store.js";
4
4
  export { readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile } from "./runs-reader.js";
5
5
  export { compileAndPersistContext } from "./compiler.js";
@@ -35,6 +35,7 @@ export declare function verifyReceiptIntegrityFromFiles(input: {
35
35
  runsRoot: string;
36
36
  loopRecordPath: string;
37
37
  ledgerPath: string;
38
+ integrityPath?: string;
38
39
  }): Promise<ReceiptIntegritySummary>;
39
40
  export declare function resolveReceiptIntegrityPath(runsRoot: string, runId: string): string;
40
41
  export declare function ensurePrivateIntegrityKey(runsRoot: string, keyFileName: string): Promise<[string, string] | undefined>;
@@ -40,7 +40,7 @@ export async function writeReceiptIntegrityMaterial(input) {
40
40
  return material;
41
41
  }
42
42
  export async function verifyReceiptIntegrityFromFiles(input) {
43
- const integrityPath = resolveReceiptIntegrityPath(input.runsRoot, input.runId);
43
+ const integrityPath = input.integrityPath ?? resolveReceiptIntegrityPath(input.runsRoot, input.runId);
44
44
  const [rawMaterial, rawLoopRecord, rawLedger, key] = await Promise.all([
45
45
  readFile(integrityPath, "utf8").catch(() => null),
46
46
  readFile(input.loopRecordPath, "utf8").catch(() => null),
@@ -385,7 +385,10 @@ function roundUsd(value) {
385
385
  return Math.round(value * 100) / 100;
386
386
  }
387
387
  export function evaluateBudgetPreflight(input) {
388
- const pricePerMToken = input.pricePerMTokenUsd ?? 3.0;
388
+ const declaredPrice = input.pricePerMTokenUsd;
389
+ const validDeclaredPrice = declaredPrice === undefined
390
+ || (Number.isFinite(declaredPrice) && declaredPrice >= 0);
391
+ const pricePerMToken = validDeclaredPrice ? (declaredPrice ?? 3.0) : 3.0;
389
392
  const rawPromptTokens = Math.ceil(input.promptCharCount / 4);
390
393
  const estimatedPromptTokens = Math.ceil(rawPromptTokens * 1.2);
391
394
  const estimatedToolOverheadTokens = 800 + input.attemptCount * 200;
@@ -407,6 +410,13 @@ export function evaluateBudgetPreflight(input) {
407
410
  estimatedAttemptCostUsd,
408
411
  provenance
409
412
  };
413
+ if (!validDeclaredPrice) {
414
+ return {
415
+ allowed: false,
416
+ reason: "Preflight: invalid adapter price; pricePerMTokenUsd must be finite and non-negative.",
417
+ estimate
418
+ };
419
+ }
410
420
  if (input.remainingTokens !== undefined
411
421
  && estimatedMinimumViableTokens > 0
412
422
  && estimatedTotalTokens > input.remainingTokens) {