@martinloop/mcp 0.8.5 → 0.8.7

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.
@@ -2,7 +2,8 @@ import { DEFAULT_AGENT_EXECUTION_INTENT, normalizeProviderExecutionTimeoutMs, ap
2
2
  import { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, 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, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
5
+ import { captureRollbackBoundary, findEscapingLinks, listAttemptChangedFilesSinceBoundary, listIgnoredFileChangesSinceBoundary, restoreRollbackBoundary } from "./rollback.js";
6
+ import { findWeakenedTestFiles } from "./test-integrity.js";
6
7
  import { compilePromptPacket } from "./compiler.js";
7
8
  import { makeLedgerEvent, resolveRunsRoot, runDir } from "./persistence/index.js";
8
9
  import { createDefaultExitPolicy, evaluateExitPolicy, hashProgressState, toLegacyExitDecision } from "./exits.js";
@@ -14,7 +15,8 @@ import { decideCircuitBreak } from "./trajectory.js";
14
15
  import { verifyContextHandoff, decideContextCircuitBreak } from "./context-handoff.js";
15
16
  import { verifierActuallyPassed, } from "./verified-handoff.js";
16
17
  export { DEFAULT_AGENT_EXECUTION_INTENT, DEFAULT_PROVIDER_EXECUTION_TIMEOUT_MS, GOVERNED_AUTONOMOUS_BOUNDARY, normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
17
- export { classifyFailure, computeEvidenceVector, evaluatePatchDecision, evaluateCostGovernor, evaluateBudgetPreflight, inferExit, nextPolicyPhase, policyPhaseToLifecycleState, scorePatchDecision, selectRecoveryRecipe, evaluateVerificationLeash, evaluateFilesystemLeash, evaluateChangeApprovalLeash, evaluateSecretLeash, resolveExecutionProfile, redactSecretsFromText, buildRepoGroundingIndex, loadOrBuildRepoGroundingIndex, queryRepoGroundingIndex, scanPatchForGroundingViolations, captureRollbackBoundary, listAttemptChangedFilesSinceBoundary, restoreRollbackBoundary };
18
+ 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 };
19
+ export { describeMalformedVerifier } from "./verifier-input.js";
18
20
  export { buildContextGraphSnapshot, queryContextGraph } from "./context-graph.js";
19
21
  export { ExternalOutcomeContractError, ExternalOutcomePolicyError, hashExternalOutcomeContract, verifyExternalOutcomes, writeExternalOutcomeEvidence, } from "./external-outcome.js";
20
22
  export { createLocalIdentityAuthority, hasIdentityScope, issueIdentityToken, verifyIdentityToken } from "./identity.js";
@@ -24,7 +26,7 @@ export { calculateAvoidedUsd, calculateLoopAvoidedUsd } from "./savings.js";
24
26
  // ─── Routing economics ─────────────────────────────────────────────────────
25
27
  export { classifyRoute, evaluatePreworkBurnPolicy } from "./routing.js";
26
28
  // ─── Test Integrity Validation ──────────────────────────────────────────────
27
- export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode } from "./test-integrity.js";
29
+ export { validateTestIntegrity, snapshotTestFiles, identifyNewTests, findTestFiles, isTestFile, extractImports, hasTrivialAssertionsOnly, importsPreExistingCode, isTestPath, findWeakenedTestFiles } from "./test-integrity.js";
28
30
  // ─── Repo Style Analyzer ────────────────────────────────────────────────────
29
31
  export { analyzeRepoStyle, buildPromptDirectives } from "./repo-analyzer.js";
30
32
  // ─── Context Integrity Pre-gate ──────────────────────────────────────────────
@@ -32,7 +34,7 @@ export { runContextIntegrityPrecheck } from "./context-integrity.js";
32
34
  // ─── Prompt packet compiler ──────────────────────────────────────────────────
33
35
  export { compilePromptPacket } from "./compiler.js";
34
36
  // ─── Verified Handoff builder ────────────────────────────────────────────────
35
- export { buildVerifiedHandoff, deriveVerifiedHandoffLifecycleTruth, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
37
+ export { buildVerifiedHandoff, NO_FILE_CHANGES_WARNING, deriveVerifiedHandoffLifecycleTruth, deriveVerifiedHandoffExecutionBoundary, resolveVerifiedHandoffOutcome, toTestIntegrityVerdict, verifierActuallyPassed, } from "./verified-handoff.js";
36
38
  // ─── Persistence (RunStore, LedgerEvent, FileRunStore) ──────────────────────
37
39
  export { createFileRunStore, makeLedgerEvent, readAllLoopRecords, readLatestLoopRecord, readLatestLoopRecordFromFile, readLoopRecordsFromFile, resolveRunsRoot, resolveReceiptIntegrityPath, verifyReceiptIntegrityFromFiles, writeReceiptIntegrityMaterial } from "./persistence/index.js";
38
40
  export { compileAndPersistContext } from "./persistence/index.js";
@@ -1073,9 +1075,37 @@ export async function runMartin(input) {
1073
1075
  }
1074
1076
  }));
1075
1077
  }
1076
- const changedFiles = tracksWorkspaceMutations
1077
- ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
1078
- : [];
1078
+ let changedFiles = [];
1079
+ try {
1080
+ changedFiles = tracksWorkspaceMutations
1081
+ ? resolveChangedFiles(result, request.context.repoRoot, rollbackBoundary)
1082
+ : [];
1083
+ }
1084
+ catch (error) {
1085
+ for (const filePath of normalizeFileEffectPaths(result.execution?.changedFiles ?? [])) {
1086
+ touchedDuringExecution.add(filePath);
1087
+ }
1088
+ const observationExitDecision = {
1089
+ shouldExit: true,
1090
+ lifecycleState: "human_escalation",
1091
+ status: "exited",
1092
+ reason: `Workspace change observation became unavailable: ${fileEffectErrorMessage(error)}`,
1093
+ reasonCode: "workspace_observation_unavailable"
1094
+ };
1095
+ if (input.store) {
1096
+ await input.store.appendLedger(loop.loopId, makeLedgerEvent({
1097
+ kind: "run.exited",
1098
+ runId: loop.loopId,
1099
+ payload: createRunExitPayload(observationExitDecision)
1100
+ }));
1101
+ }
1102
+ await stopSignalMonitorAndDrainDiagnostics();
1103
+ const terminationEnvelope = await persistGuardStop(observationExitDecision);
1104
+ const finalizedLoop = await finalizeCurrentLoop(observationExitDecision, now());
1105
+ const finalizedWithEnvelope = { ...finalizedLoop, terminationEnvelope };
1106
+ await persistLoopRecordIfSupported(input.store, finalizedWithEnvelope);
1107
+ return { loop: finalizedWithEnvelope, decision: observationExitDecision };
1108
+ }
1079
1109
  for (const filePath of normalizeFileEffectPaths(changedFiles)) {
1080
1110
  touchedDuringExecution.add(filePath);
1081
1111
  }
@@ -1097,9 +1127,17 @@ export async function runMartin(input) {
1097
1127
  // returned a non-empty list. A repoRoot alone is insufficient — git may fail (e.g. not
1098
1128
  // a git repo) and silently return [], which would falsely trigger no_code_change.
1099
1129
  const changedFileEvidenceAvailable = result.execution?.changedFiles !== undefined || changedFiles.length > 0;
1130
+ const ignoredChanges = tracksWorkspaceMutations
1131
+ ? listIgnoredFileChangesSinceBoundary({ repoRoot: request.context.repoRoot, boundary: rollbackBoundary })
1132
+ : { created: [], modified: [], deleted: [] };
1133
+ const escapingLinks = tracksWorkspaceMutations && request.context.repoRoot
1134
+ ? findEscapingLinks(request.context.repoRoot, [...changedFiles, ...ignoredChanges.created])
1135
+ : [];
1100
1136
  const filesystemDecision = evaluateFilesystemLeash({
1101
1137
  repoRoot: request.context.repoRoot,
1102
1138
  changedFiles,
1139
+ ignoredFiles: [...ignoredChanges.created, ...ignoredChanges.modified, ...ignoredChanges.deleted],
1140
+ escapingLinks,
1103
1141
  allowedPaths: request.context.allowedPaths,
1104
1142
  deniedPaths: request.context.deniedPaths,
1105
1143
  mutationMode: request.context.mutationMode
@@ -1127,7 +1165,14 @@ export async function runMartin(input) {
1127
1165
  repoRoot: request.context.repoRoot,
1128
1166
  boundary: rollbackBoundary,
1129
1167
  restoredAt: attemptCompletedAt,
1130
- decision: patchDecision.decision
1168
+ decision: patchDecision.decision,
1169
+ // Attempt-created ignored violations are removed. Modified or deleted
1170
+ // baseline ignored violations are restored from the bounded in-memory snapshot.
1171
+ removePaths: violatingIgnoredPaths(filesystemDecision.violations, ignoredChanges.created),
1172
+ restoreIgnoredPaths: violatingIgnoredPaths(filesystemDecision.violations, [
1173
+ ...ignoredChanges.modified,
1174
+ ...ignoredChanges.deleted
1175
+ ])
1131
1176
  });
1132
1177
  if (input.store) {
1133
1178
  await input.store.writeAttemptArtifacts(loop.loopId, currentAttemptIndex, {
@@ -1176,8 +1221,16 @@ export async function runMartin(input) {
1176
1221
  decision: filesystemExitDecision
1177
1222
  };
1178
1223
  }
1224
+ const weakenedTestFiles = tracksWorkspaceMutations && request.context.repoRoot && rollbackBoundary?.headRef
1225
+ ? findWeakenedTestFiles({
1226
+ repoRoot: request.context.repoRoot,
1227
+ baseRef: rollbackBoundary.headRef,
1228
+ changedFiles
1229
+ })
1230
+ : [];
1179
1231
  const changeApprovalDecision = evaluateChangeApprovalLeash({
1180
1232
  changedFiles,
1233
+ weakenedTestFiles,
1181
1234
  executionProfile: request.context.executionProfile,
1182
1235
  approvalPolicy: request.context.approvalPolicy
1183
1236
  });
@@ -1550,6 +1603,8 @@ function safetyLeashReasonCode(decision, safetySurface) {
1550
1603
  return "migration_approval_required";
1551
1604
  case "config_change_approval_required":
1552
1605
  return "config_change_approval_required";
1606
+ case "test_change_approval_required":
1607
+ return "test_change_approval_required";
1553
1608
  default:
1554
1609
  return `${safetySurface}_safety_block`;
1555
1610
  }
@@ -1595,7 +1650,8 @@ function observeRunFileEffects(input) {
1595
1650
  try {
1596
1651
  const netChangedAtCompletion = normalizeFileEffectPaths(listAttemptChangedFilesSinceBoundary({
1597
1652
  repoRoot: input.repoRoot,
1598
- boundary: input.boundary
1653
+ boundary: input.boundary,
1654
+ includeBaselineSnapshotChanges: true
1599
1655
  }));
1600
1656
  const netChanged = new Set(netChangedAtCompletion);
1601
1657
  return {
@@ -1714,18 +1770,23 @@ function mergeCostProvenance(previous, current) {
1714
1770
  }
1715
1771
  return COST_PROVENANCE_RANK[current] < COST_PROVENANCE_RANK[previous] ? current : previous;
1716
1772
  }
1773
+ /**
1774
+ * Git-backed observation that degrades the same way `resolveChangedFiles` does:
1775
+ * when Git becomes unavailable after the attempt, fall back only if the adapter
1776
+ * reported its own changed files; otherwise fail closed.
1777
+ */
1778
+ function violatingIgnoredPaths(violations, ignoredPaths) {
1779
+ const violating = new Set(violations.map((violation) => violation.file?.replace(/\/$/u, "")).filter(Boolean));
1780
+ return ignoredPaths.filter((filePath) => violating.has(filePath.replace(/\/$/u, "")));
1781
+ }
1717
1782
  function resolveChangedFiles(result, repoRoot, rollbackBoundary) {
1718
1783
  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;
1728
- }
1784
+ const gitObserved = listAttemptChangedFilesSinceBoundary({
1785
+ repoRoot,
1786
+ boundary: rollbackBoundary,
1787
+ includeBaselineSnapshotChanges: true
1788
+ });
1789
+ return normalizeFileEffectPaths([...adapterReported, ...gitObserved]);
1729
1790
  }
1730
1791
  function buildPatchDiff(result, changedFiles) {
1731
1792
  if (result.execution?.patch) {
@@ -1,6 +1,6 @@
1
1
  import type { ApprovalPolicy, ExecutionProfile, LoopTask } from "../contracts/index.js";
2
2
  export type SafetySurface = "command" | "filesystem" | "secret" | "spend" | "network" | "dependency";
3
- export type SafetyViolationKind = "command_blocked" | "path_not_allowed" | "path_denied" | "path_outside_repo" | "network_blocked" | "dependency_approval_required" | "migration_approval_required" | "config_change_approval_required" | "secret_value" | "protected_path";
3
+ export type SafetyViolationKind = "command_blocked" | "path_not_allowed" | "path_denied" | "path_outside_repo" | "network_blocked" | "dependency_approval_required" | "migration_approval_required" | "config_change_approval_required" | "test_change_approval_required" | "secret_value" | "protected_path";
4
4
  export interface SafetyViolation {
5
5
  kind: SafetyViolationKind;
6
6
  message: string;
@@ -30,6 +30,14 @@ export declare function evaluateVerificationLeash(task: Pick<LoopTask, "verifica
30
30
  export declare function evaluateFilesystemLeash(input: {
31
31
  repoRoot?: string;
32
32
  changedFiles: string[];
33
+ /**
34
+ * Git-ignored files the attempt created or modified. These are checked only
35
+ * against denied paths and protected secret files, never allowed paths, so
36
+ * dependency installs and build output stay unaffected.
37
+ */
38
+ ignoredFiles?: string[];
39
+ /** Symlinks or junctions created inside the repo that resolve outside it. */
40
+ escapingLinks?: string[];
33
41
  allowedPaths?: string[];
34
42
  deniedPaths?: string[];
35
43
  mutationMode?: "edit" | "read_only" | "verify_only";
@@ -43,6 +51,8 @@ export declare function resolveExecutionProfile(input: {
43
51
  }): ResolvedExecutionProfile;
44
52
  export declare function evaluateChangeApprovalLeash(input: {
45
53
  changedFiles: string[];
54
+ /** Existing test files the attempt deleted or removed lines from. */
55
+ weakenedTestFiles?: string[];
46
56
  executionProfile?: ExecutionProfile;
47
57
  approvalPolicy?: ApprovalPolicy;
48
58
  }): SafetyLeashDecision;
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute, relative, resolve } from "node:path";
2
+ import { isInsideDependencyOrBuildDirectory } from "./output-directories.js";
2
3
  const BLOCKED_PATTERNS = [
3
4
  /(^|[\s;&|`(])rm\s+-rf(\s|$)/iu,
4
5
  /git\s+reset\s+--hard/iu,
@@ -186,8 +187,13 @@ export function evaluateVerificationLeash(task) {
186
187
  };
187
188
  }
188
189
  export function evaluateFilesystemLeash(input) {
189
- const violations = [];
190
- for (const rawFile of input.changedFiles) {
190
+ const violations = (input.escapingLinks ?? []).map((link) => ({
191
+ kind: "path_outside_repo",
192
+ file: link,
193
+ message: `Link inside the repo points outside the configured repo root: ${link}`
194
+ }));
195
+ const escapingLinks = new Set(input.escapingLinks ?? []);
196
+ for (const rawFile of input.changedFiles.filter((file) => !escapingLinks.has(file))) {
191
197
  const normalized = normalizeChangedFile(rawFile, input.repoRoot);
192
198
  if (normalized.outsideRepo) {
193
199
  violations.push({
@@ -218,6 +224,10 @@ export function evaluateFilesystemLeash(input) {
218
224
  continue;
219
225
  }
220
226
  const allowedPaths = input.allowedPaths ?? [];
227
+ if (isProtectedSecretFile(file) && !isExplicitlyAllowed(file, allowedPaths)) {
228
+ violations.push(protectedSecretViolation(file));
229
+ continue;
230
+ }
221
231
  if (allowedPaths.length > 0 && !allowedPaths.some((pattern) => matchesPathPattern(file, pattern))) {
222
232
  violations.push({
223
233
  kind: "path_not_allowed",
@@ -226,6 +236,25 @@ export function evaluateFilesystemLeash(input) {
226
236
  });
227
237
  }
228
238
  }
239
+ for (const rawFile of input.ignoredFiles ?? []) {
240
+ const file = normalizePathForMatching(rawFile);
241
+ if (!file) {
242
+ continue;
243
+ }
244
+ if ((input.deniedPaths ?? []).some((pattern) => matchesPathPattern(file, pattern))) {
245
+ violations.push({
246
+ kind: "path_denied",
247
+ file,
248
+ message: `Git-ignored file matches a denylisted path: ${file}`
249
+ });
250
+ continue;
251
+ }
252
+ if (!isInsideDependencyOrBuildDirectory(file) &&
253
+ isProtectedSecretFile(file) &&
254
+ !isExplicitlyAllowed(file, input.allowedPaths ?? [])) {
255
+ violations.push(protectedSecretViolation(file));
256
+ }
257
+ }
229
258
  if (violations.length > 0) {
230
259
  return {
231
260
  allowed: false,
@@ -357,6 +386,15 @@ export function evaluateChangeApprovalLeash(input) {
357
386
  });
358
387
  }
359
388
  }
389
+ if (!input.approvalPolicy?.testChanges) {
390
+ for (const file of input.weakenedTestFiles ?? []) {
391
+ violations.push({
392
+ kind: "test_change_approval_required",
393
+ file,
394
+ message: `Removing or rewriting existing test code requires approval: ${file}`
395
+ });
396
+ }
397
+ }
360
398
  if (violations.length > 0) {
361
399
  return {
362
400
  allowed: false,
@@ -366,7 +404,7 @@ export function evaluateChangeApprovalLeash(input) {
366
404
  profile: profile.name,
367
405
  blockedCommands: [],
368
406
  violations,
369
- reason: "Safety leash blocked dependency, migration, or configuration changes that require approval."
407
+ reason: "Safety leash blocked dependency, migration, configuration, or existing-test changes that require approval."
370
408
  };
371
409
  }
372
410
  return {
@@ -408,6 +446,42 @@ function normalizeChangedFile(file, repoRoot) {
408
446
  outsideRepo: false
409
447
  };
410
448
  }
449
+ const SAFE_ENV_TEMPLATE_SUFFIXES = new Set(["example", "sample", "template", "dist", "defaults"]);
450
+ const PRIVATE_KEY_BASENAMES = new Set(["id_rsa", "id_dsa", "id_ecdsa", "id_ed25519"]);
451
+ const SECRET_MATERIAL_EXTENSIONS = new Set(["pem", "key", "p12", "pfx"]);
452
+ /** Files that conventionally hold credentials or private key material. */
453
+ function isProtectedSecretFile(file) {
454
+ const leaf = normalizePathForMatching(file).split("/").at(-1) ?? "";
455
+ if (leaf === ".env") {
456
+ return true;
457
+ }
458
+ if (leaf.startsWith(".env.")) {
459
+ return !SAFE_ENV_TEMPLATE_SUFFIXES.has(leaf.slice(".env.".length).toLowerCase());
460
+ }
461
+ if (PRIVATE_KEY_BASENAMES.has(leaf)) {
462
+ return true;
463
+ }
464
+ const extension = leaf.includes(".") ? leaf.split(".").at(-1)?.toLowerCase() : undefined;
465
+ return extension !== undefined && SECRET_MATERIAL_EXTENSIONS.has(extension);
466
+ }
467
+ /**
468
+ * A protected file counts as allowed only when an allowed-path entry targets it
469
+ * specifically; broad wildcards such as `**` or `src/*` do not approve secrets.
470
+ */
471
+ function isExplicitlyAllowed(file, allowedPaths) {
472
+ return allowedPaths.some((pattern) => {
473
+ const leafPattern = normalizePathForMatching(pattern).split("/").at(-1) ?? "";
474
+ return !/^\*+$/u.test(leafPattern) && matchesPathPattern(file, pattern);
475
+ });
476
+ }
477
+ function protectedSecretViolation(file) {
478
+ return {
479
+ kind: "protected_path",
480
+ file,
481
+ message: `Changed file is a protected secret or key file: ${file}. ` +
482
+ "Name it explicitly in allowed paths to permit agent writes."
483
+ };
484
+ }
411
485
  function matchesPathPattern(file, pattern) {
412
486
  const normalizedFile = normalizePathForMatching(file);
413
487
  const normalizedPattern = normalizePathForMatching(pattern);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Directory names that hold dependency installs or build output. Git-ignored
3
+ * content under them is tracked as a single collapsed entry and is not checked
4
+ * file by file for protected secrets.
5
+ */
6
+ export declare const DEPENDENCY_AND_BUILD_DIRECTORIES: ReadonlySet<string>;
7
+ export declare function isInsideDependencyOrBuildDirectory(repoPath: string): boolean;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Directory names that hold dependency installs or build output. Git-ignored
3
+ * content under them is tracked as a single collapsed entry and is not checked
4
+ * file by file for protected secrets.
5
+ */
6
+ export const DEPENDENCY_AND_BUILD_DIRECTORIES = new Set([
7
+ "node_modules",
8
+ "bower_components",
9
+ "jspm_packages",
10
+ ".pnpm-store",
11
+ ".yarn",
12
+ ".venv",
13
+ "venv",
14
+ "__pycache__",
15
+ ".mypy_cache",
16
+ ".pytest_cache",
17
+ ".ruff_cache",
18
+ ".tox",
19
+ ".gradle",
20
+ ".next",
21
+ ".nuxt",
22
+ ".turbo",
23
+ ".cache",
24
+ ".parcel-cache",
25
+ "target",
26
+ "dist",
27
+ "build",
28
+ "out",
29
+ "coverage",
30
+ "vendor"
31
+ ]);
32
+ export function isInsideDependencyOrBuildDirectory(repoPath) {
33
+ const segments = repoPath.replace(/\\/gu, "/").split("/").filter(Boolean);
34
+ // A trailing collapsed entry such as `node_modules/` is itself the directory.
35
+ return segments.some((segment) => DEPENDENCY_AND_BUILD_DIRECTORIES.has(segment));
36
+ }
@@ -1,18 +1,42 @@
1
1
  import type { PatchDecision, RollbackBoundaryArtifact, RollbackOutcomeArtifact } from "../contracts/index.js";
2
+ export interface IgnoredFileChanges {
3
+ created: string[];
4
+ modified: string[];
5
+ deleted: string[];
6
+ }
2
7
  export declare class RollbackStateUnavailableError extends Error {
3
8
  constructor(message: string);
4
9
  }
5
10
  export declare function listAttemptChangedFilesSinceBoundary(input: {
6
11
  repoRoot?: string;
7
12
  boundary?: RollbackBoundaryArtifact;
13
+ /** Include content changes to dirty/untracked paths that existed at the boundary. */
14
+ includeBaselineSnapshotChanges?: boolean;
8
15
  }): string[];
9
16
  export declare function captureRollbackBoundary(input: {
10
17
  repoRoot?: string;
11
18
  capturedAt: string;
12
19
  }): Promise<RollbackBoundaryArtifact | undefined>;
20
+ /**
21
+ * Git-ignored files the attempt created or modified since the boundary.
22
+ * Boundaries recorded before the ignored inventory existed report nothing.
23
+ */
24
+ export declare function listIgnoredFileChangesSinceBoundary(input: {
25
+ repoRoot?: string;
26
+ boundary?: RollbackBoundaryArtifact;
27
+ }): IgnoredFileChanges;
13
28
  export declare function restoreRollbackBoundary(input: {
14
29
  repoRoot?: string;
15
30
  boundary?: RollbackBoundaryArtifact;
16
31
  restoredAt: string;
17
32
  decision: PatchDecision;
33
+ /** Extra attempt-created paths (for example ignored files) to delete. */
34
+ removePaths?: string[];
35
+ /** Pre-existing ignored paths to restore from the in-memory boundary. */
36
+ restoreIgnoredPaths?: string[];
18
37
  }): Promise<RollbackOutcomeArtifact | undefined>;
38
+ /**
39
+ * Repo-relative links (symlinks or junctions) among `paths` whose target
40
+ * resolves outside the repository. Paths are first collapsed to the link.
41
+ */
42
+ export declare function findEscapingLinks(repoRoot: string, paths: string[]): string[];