@martinloop/mcp 0.8.10 → 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.
@@ -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),
@@ -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";
@@ -293,6 +293,39 @@ export interface RunMartinInput {
293
293
  observedUsageGovernor?: MartinObservedUsageGovernor;
294
294
  /** Optional parent cancellation signal. Relayed into the active governed attempt. */
295
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>;
296
329
  }
297
330
  export interface RunMartinResult {
298
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
  });
@@ -1118,7 +1205,8 @@ export async function runMartin(input) {
1118
1205
  const rollbackBoundary = tracksWorkspaceMutations
1119
1206
  ? await captureRollbackBoundary({
1120
1207
  repoRoot: request.context.repoRoot,
1121
- capturedAt: attemptStartedAt
1208
+ capturedAt: attemptStartedAt,
1209
+ managedRoots: managedRollbackRoots
1122
1210
  })
1123
1211
  : undefined;
1124
1212
  // Persist attempt.started state before blocking on provider execution.
@@ -1353,10 +1441,14 @@ export async function runMartin(input) {
1353
1441
  let weakenedTestFiles = [];
1354
1442
  try {
1355
1443
  changedFiles = tracksWorkspaceMutations
1356
- ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
1444
+ ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary, managedRollbackRoots)
1357
1445
  : [];
1358
1446
  ignoredChanges = tracksWorkspaceMutations
1359
- ? listIgnoredFileChangesSinceBoundary({ repoRoot: request.context.repoRoot, boundary: rollbackBoundary })
1447
+ ? listIgnoredFileChangesSinceBoundary({
1448
+ repoRoot: request.context.repoRoot,
1449
+ boundary: rollbackBoundary,
1450
+ managedRoots: managedRollbackRoots
1451
+ })
1360
1452
  : ignoredChanges;
1361
1453
  escapingLinks =
1362
1454
  tracksWorkspaceMutations && request.context.repoRoot
@@ -1383,6 +1475,7 @@ export async function runMartin(input) {
1383
1475
  rollbackOutcome = (await restoreRollbackBoundary({
1384
1476
  repoRoot: request.context.repoRoot,
1385
1477
  boundary: rollbackBoundary,
1478
+ managedRoots: managedRollbackRoots,
1386
1479
  restoredAt: attemptCompletedAt,
1387
1480
  decision: "DISCARD",
1388
1481
  removePaths: ignoredChanges.created,
@@ -1496,6 +1589,7 @@ export async function runMartin(input) {
1496
1589
  rollbackOutcome = await restoreRollbackBoundary({
1497
1590
  repoRoot: request.context.repoRoot,
1498
1591
  boundary: rollbackBoundary,
1592
+ managedRoots: managedRollbackRoots,
1499
1593
  restoredAt: attemptCompletedAt,
1500
1594
  decision: patchDecision.decision,
1501
1595
  removePaths: violatingIgnoredPaths(filesystemDecision.violations, ignoredChanges.created),
@@ -1593,6 +1687,7 @@ export async function runMartin(input) {
1593
1687
  rollbackOutcome = await restoreRollbackBoundary({
1594
1688
  repoRoot: request.context.repoRoot,
1595
1689
  boundary: rollbackBoundary,
1690
+ managedRoots: managedRollbackRoots,
1596
1691
  restoredAt: attemptCompletedAt,
1597
1692
  decision: patchDecision.decision
1598
1693
  }) ?? undefined;
@@ -1725,6 +1820,7 @@ export async function runMartin(input) {
1725
1820
  rollbackOutcome = await restoreRollbackBoundary({
1726
1821
  repoRoot: request.context.repoRoot,
1727
1822
  boundary: rollbackBoundary,
1823
+ managedRoots: managedRollbackRoots,
1728
1824
  restoredAt: attemptCompletedAt,
1729
1825
  decision: patchDecision?.decision ?? "DISCARD"
1730
1826
  });
@@ -1734,6 +1830,7 @@ export async function runMartin(input) {
1734
1830
  rollbackOutcome = await restoreRollbackBoundary({
1735
1831
  repoRoot: request.context.repoRoot,
1736
1832
  boundary: rollbackBoundary,
1833
+ managedRoots: managedRollbackRoots,
1737
1834
  restoredAt: attemptCompletedAt,
1738
1835
  decision: "DISCARD"
1739
1836
  });
@@ -2002,6 +2099,7 @@ export async function runMartin(input) {
2002
2099
  await restoreRollbackBoundary({
2003
2100
  repoRoot: task.repoRoot,
2004
2101
  boundary: preCandidateBoundary,
2102
+ managedRoots: managedRollbackRoots,
2005
2103
  restoredAt: now(),
2006
2104
  decision: "DISCARD"
2007
2105
  });
@@ -2092,11 +2190,11 @@ function finalizeLoop(loop, decision, timestamp, idFactory, routingEconomicsInpu
2092
2190
  };
2093
2191
  }
2094
2192
  function observeRunFileEffects(input) {
2095
- const touchedDuringExecution = normalizeFileEffectPaths(input.touchedDuringExecution);
2193
+ const observedTouchedDuringExecution = normalizeFileEffectPaths(input.touchedDuringExecution);
2096
2194
  if (!input.repoRoot || !input.boundary) {
2097
2195
  return {
2098
2196
  observationStatus: "unavailable",
2099
- touchedDuringExecution,
2197
+ touchedDuringExecution: observedTouchedDuringExecution,
2100
2198
  netChangedAtCompletion: [],
2101
2199
  revertedBeforeCompletion: [],
2102
2200
  observationReason: input.unavailableReason ??
@@ -2104,9 +2202,12 @@ function observeRunFileEffects(input) {
2104
2202
  };
2105
2203
  }
2106
2204
  try {
2205
+ const exclusions = authenticateManagedPathExclusions(input.repoRoot, input.boundary, input.managedRoots);
2206
+ const touchedDuringExecution = filterManagedRollbackPaths(observedTouchedDuringExecution, exclusions);
2107
2207
  const netChangedAtCompletion = normalizeFileEffectPaths(listAttemptChangedFilesSinceBoundary({
2108
2208
  repoRoot: input.repoRoot,
2109
2209
  boundary: input.boundary,
2210
+ managedRoots: input.managedRoots,
2110
2211
  includeBaselineSnapshotChanges: true
2111
2212
  }));
2112
2213
  const netChanged = new Set(netChangedAtCompletion);
@@ -2120,7 +2221,7 @@ function observeRunFileEffects(input) {
2120
2221
  catch (error) {
2121
2222
  return {
2122
2223
  observationStatus: "unavailable",
2123
- touchedDuringExecution,
2224
+ touchedDuringExecution: observedTouchedDuringExecution,
2124
2225
  netChangedAtCompletion: [],
2125
2226
  revertedBeforeCompletion: [],
2126
2227
  observationReason: `Final file-effect observation was unavailable: ${fileEffectErrorMessage(error)}`
@@ -2235,11 +2336,15 @@ function violatingIgnoredPaths(violations, ignoredPaths) {
2235
2336
  const violating = new Set(violations.map((violation) => violation.file?.replace(/\/$/u, "")).filter(Boolean));
2236
2337
  return ignoredPaths.filter((filePath) => violating.has(filePath.replace(/\/$/u, "")));
2237
2338
  }
2238
- function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
2239
- 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);
2240
2344
  const gitObserved = listAttemptChangedFilesSinceBoundary({
2241
2345
  repoRoot,
2242
2346
  boundary: rollbackBoundary,
2347
+ managedRoots,
2243
2348
  includeBaselineSnapshotChanges: true
2244
2349
  });
2245
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),
@@ -1,18 +1,28 @@
1
1
  import type { LoopRecord, RollbackBoundaryArtifact } from "../contracts/index.js";
2
2
  export interface RunExecutionLease {
3
- schemaVersion: "martin.execution-lease.v1";
3
+ schemaVersion: "martin.execution-lease.v2";
4
4
  runId: string;
5
5
  token: string;
6
6
  owner: string;
7
7
  pid: number;
8
8
  hostname: string;
9
9
  acquiredAt: string;
10
+ capabilities: {
11
+ runControl: boolean;
12
+ };
13
+ processInstanceId: string;
14
+ heartbeatAt: string;
15
+ heartbeatSequence: number;
10
16
  }
11
17
  export interface AcquiredRunExecutionLease {
12
18
  lease: RunExecutionLease;
13
19
  path: string;
14
20
  release(): Promise<void>;
15
21
  }
22
+ export interface ActiveRunExecutionLease {
23
+ lease: RunExecutionLease;
24
+ path: string;
25
+ }
16
26
  export interface ResumeVerifierFailure {
17
27
  command: string;
18
28
  exitCode?: number;
@@ -36,4 +46,18 @@ export declare function acquireRunExecutionLease(input: {
36
46
  now?: () => string;
37
47
  host?: string;
38
48
  pid?: number;
49
+ runControlCapable?: boolean;
50
+ }, hooks?: {
51
+ beforeStaleReclaim?: () => Promise<void>;
39
52
  }): Promise<AcquiredRunExecutionLease>;
53
+ /**
54
+ * Returns the persisted execution lease only when its schema, run identity,
55
+ * supported owner and local process liveness are all authoritative. Control
56
+ * surfaces use this to avoid accepting commands for an abandoned or incapable
57
+ * executor.
58
+ */
59
+ export declare function readActiveRunExecutionLease(input: {
60
+ runsRoot: string;
61
+ runId: string;
62
+ host?: string;
63
+ }): Promise<ActiveRunExecutionLease | undefined>;