@guilz-dev/belay 0.4.0 → 0.7.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 (240) hide show
  1. package/README.md +27 -2
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +8 -2
  5. package/dist/adapters/shared/gate-runtime.js +579 -181
  6. package/dist/bundle/claude-runtime.mjs +13574 -6334
  7. package/dist/bundle/codex-runtime.mjs +13108 -5903
  8. package/dist/bundle/cursor-runtime.mjs +12676 -5476
  9. package/dist/cli.js +80 -2
  10. package/dist/commands/approve.js +34 -10
  11. package/dist/commands/classify-for-report.js +1 -1
  12. package/dist/commands/config.d.ts +8 -0
  13. package/dist/commands/config.js +134 -11
  14. package/dist/commands/doctor.js +87 -1
  15. package/dist/commands/explain.js +11 -1
  16. package/dist/commands/judge.js +3 -2
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/session.d.ts +20 -0
  21. package/dist/commands/session.js +51 -0
  22. package/dist/commands/status.js +7 -0
  23. package/dist/config-io.js +91 -23
  24. package/dist/conformance/guarantee-posture.d.ts +21 -0
  25. package/dist/conformance/guarantee-posture.js +46 -0
  26. package/dist/conformance/guarantee-table.d.ts +1 -0
  27. package/dist/conformance/guarantee-table.js +20 -9
  28. package/dist/conformance/types.d.ts +1 -0
  29. package/dist/core/approval-replay.d.ts +1 -1
  30. package/dist/core/approval-replay.js +4 -1
  31. package/dist/core/approval-service.d.ts +6 -1
  32. package/dist/core/approval-service.js +87 -24
  33. package/dist/core/approval.d.ts +4 -0
  34. package/dist/core/approval.js +19 -5
  35. package/dist/core/audit-types.d.ts +13 -0
  36. package/dist/core/capability/approval-state-mutation.d.ts +32 -0
  37. package/dist/core/capability/approval-state-mutation.js +165 -0
  38. package/dist/core/capability/approval-v3.d.ts +32 -0
  39. package/dist/core/capability/approval-v3.js +114 -0
  40. package/dist/core/capability/attestation.d.ts +16 -0
  41. package/dist/core/capability/attestation.js +62 -0
  42. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  43. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  44. package/dist/core/capability/boundary-driver-container.d.ts +19 -0
  45. package/dist/core/capability/boundary-driver-container.js +189 -0
  46. package/dist/core/capability/boundary-driver.d.ts +29 -0
  47. package/dist/core/capability/boundary-driver.js +44 -0
  48. package/dist/core/capability/boundary-egress.d.ts +21 -0
  49. package/dist/core/capability/boundary-egress.js +109 -0
  50. package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
  51. package/dist/core/capability/boundary-grant-materialize.js +93 -0
  52. package/dist/core/capability/boundary-profile.d.ts +15 -0
  53. package/dist/core/capability/boundary-profile.js +25 -0
  54. package/dist/core/capability/boundary-run.d.ts +48 -0
  55. package/dist/core/capability/boundary-run.js +51 -0
  56. package/dist/core/capability/boundary-session.d.ts +61 -0
  57. package/dist/core/capability/boundary-session.js +138 -0
  58. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  59. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  60. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  61. package/dist/core/capability/capability-request-hash.js +8 -0
  62. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  63. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  64. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  65. package/dist/core/capability/gate-policy-shadow.js +130 -0
  66. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  67. package/dist/core/capability/gate-shadow-audit.js +1 -0
  68. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  69. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  70. package/dist/core/capability/grant-consumption.d.ts +4 -0
  71. package/dist/core/capability/grant-consumption.js +11 -0
  72. package/dist/core/capability/grant-lease.d.ts +44 -0
  73. package/dist/core/capability/grant-lease.js +243 -0
  74. package/dist/core/capability/grant-loader.d.ts +12 -0
  75. package/dist/core/capability/grant-loader.js +23 -0
  76. package/dist/core/capability/grant-match.d.ts +6 -0
  77. package/dist/core/capability/grant-match.js +96 -0
  78. package/dist/core/capability/grant.d.ts +16 -0
  79. package/dist/core/capability/grant.js +16 -0
  80. package/dist/core/capability/index.d.ts +8 -0
  81. package/dist/core/capability/index.js +7 -0
  82. package/dist/core/capability/limits.d.ts +7 -0
  83. package/dist/core/capability/limits.js +55 -0
  84. package/dist/core/capability/policy-bridge.d.ts +18 -0
  85. package/dist/core/capability/policy-bridge.js +51 -0
  86. package/dist/core/capability/policy-engine.d.ts +77 -0
  87. package/dist/core/capability/policy-engine.js +663 -0
  88. package/dist/core/capability/policy-types.d.ts +20 -0
  89. package/dist/core/capability/policy-types.js +1 -0
  90. package/dist/core/capability/request.d.ts +48 -0
  91. package/dist/core/capability/request.js +1 -0
  92. package/dist/core/capability/resolver.d.ts +4 -0
  93. package/dist/core/capability/resolver.js +4 -0
  94. package/dist/core/classify-subagent.d.ts +2 -1
  95. package/dist/core/classify-subagent.js +38 -2
  96. package/dist/core/classify-tool.js +126 -62
  97. package/dist/core/config.d.ts +34 -3
  98. package/dist/core/config.js +118 -3
  99. package/dist/core/effect-ir/audit.d.ts +9 -0
  100. package/dist/core/effect-ir/audit.js +86 -0
  101. package/dist/core/effect-ir/build.d.ts +31 -0
  102. package/dist/core/effect-ir/build.js +277 -0
  103. package/dist/core/effect-ir/index.d.ts +8 -0
  104. package/dist/core/effect-ir/index.js +7 -0
  105. package/dist/core/effect-ir/normalize.d.ts +4 -0
  106. package/dist/core/effect-ir/normalize.js +104 -0
  107. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  108. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  109. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  110. package/dist/core/effect-ir/package-exec.js +292 -0
  111. package/dist/core/effect-ir/policy.d.ts +13 -0
  112. package/dist/core/effect-ir/policy.js +77 -0
  113. package/dist/core/effect-ir/types.d.ts +55 -0
  114. package/dist/core/effect-ir/types.js +1 -0
  115. package/dist/core/egress/allowlist.d.ts +1 -0
  116. package/dist/core/egress/allowlist.js +8 -0
  117. package/dist/core/egress-approval.js +45 -51
  118. package/dist/core/gate-contract.d.ts +5 -0
  119. package/dist/core/gate-contract.js +5 -0
  120. package/dist/core/gate-engine.js +30 -8
  121. package/dist/core/index.d.ts +3 -1
  122. package/dist/core/index.js +2 -1
  123. package/dist/core/judge-cloud-consent.js +29 -15
  124. package/dist/core/judge-doctor.js +39 -14
  125. package/dist/core/judge-fallback-hints.d.ts +13 -0
  126. package/dist/core/judge-fallback-hints.js +108 -0
  127. package/dist/core/network-endpoint.d.ts +10 -0
  128. package/dist/core/network-endpoint.js +51 -0
  129. package/dist/core/path-utils.d.ts +1 -0
  130. package/dist/core/path-utils.js +5 -2
  131. package/dist/core/process-runner.d.ts +10 -0
  132. package/dist/core/process-runner.js +124 -0
  133. package/dist/core/recover-advice.js +2 -1
  134. package/dist/core/recovery/artifact-store.d.ts +29 -0
  135. package/dist/core/recovery/artifact-store.js +336 -0
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/checkpoint.d.ts +25 -0
  139. package/dist/core/recovery/checkpoint.js +260 -0
  140. package/dist/core/recovery/fail-closed.d.ts +6 -0
  141. package/dist/core/recovery/fail-closed.js +25 -0
  142. package/dist/core/recovery/index.d.ts +5 -0
  143. package/dist/core/recovery/index.js +4 -0
  144. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  145. package/dist/core/recovery/operator-guidance.js +39 -0
  146. package/dist/core/recovery/reconcile.d.ts +3 -0
  147. package/dist/core/recovery/reconcile.js +48 -0
  148. package/dist/core/recovery/resource-identity.d.ts +4 -0
  149. package/dist/core/recovery/resource-identity.js +34 -0
  150. package/dist/core/recovery/restore.d.ts +13 -0
  151. package/dist/core/recovery/restore.js +114 -0
  152. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  153. package/dist/core/recovery/snapshot-node.js +261 -0
  154. package/dist/core/recovery/types.d.ts +104 -0
  155. package/dist/core/recovery/types.js +1 -0
  156. package/dist/core/standing-allow.js +3 -0
  157. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  158. package/dist/core/transactional/apply-observed-changes.js +275 -0
  159. package/dist/core/transactional/backend-selector.d.ts +8 -0
  160. package/dist/core/transactional/backend-selector.js +118 -0
  161. package/dist/core/transactional/backend.d.ts +55 -0
  162. package/dist/core/transactional/backend.js +1 -0
  163. package/dist/core/transactional/diff-evaluator.js +4 -1
  164. package/dist/core/transactional/eligibility.js +4 -0
  165. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  166. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  167. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  168. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  169. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  170. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  171. package/dist/core/transactional/file-clone.d.ts +15 -0
  172. package/dist/core/transactional/file-clone.js +139 -0
  173. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  174. package/dist/core/transactional/file-tree-path.js +50 -0
  175. package/dist/core/transactional/file-tree.d.ts +41 -0
  176. package/dist/core/transactional/file-tree.js +186 -0
  177. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  178. package/dist/core/transactional/git-worktree-backend.js +50 -0
  179. package/dist/core/transactional/git-worktree.d.ts +14 -9
  180. package/dist/core/transactional/git-worktree.js +62 -87
  181. package/dist/core/transactional/index.d.ts +11 -0
  182. package/dist/core/transactional/index.js +10 -0
  183. package/dist/core/transactional/reasons.d.ts +1 -0
  184. package/dist/core/transactional/reasons.js +5 -0
  185. package/dist/core/transactional/runner.js +171 -23
  186. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  187. package/dist/core/transactional/snapshot-node.js +87 -0
  188. package/dist/core/transactional/types.d.ts +21 -1
  189. package/dist/core/types.d.ts +30 -3
  190. package/dist/core/verdict/adapter.d.ts +3 -4
  191. package/dist/core/verdict/adapter.js +24 -12
  192. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  193. package/dist/core/verdict/cursor-acp-client.js +345 -0
  194. package/dist/core/verdict/egress-classify.d.ts +2 -2
  195. package/dist/core/verdict/egress-classify.js +2 -2
  196. package/dist/core/verdict/git-classifier.d.ts +26 -0
  197. package/dist/core/verdict/git-classifier.js +533 -0
  198. package/dist/core/verdict/judge-audit.js +1 -0
  199. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  200. package/dist/core/verdict/judge-baseline.js +1 -0
  201. package/dist/core/verdict/judge-broker-service.js +12 -0
  202. package/dist/core/verdict/judge-catalog.js +1 -0
  203. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  204. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  205. package/dist/core/verdict/judge-cli.js +5 -3
  206. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  207. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  208. package/dist/core/verdict/judge-runtime-config.js +10 -6
  209. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  210. package/dist/core/verdict/judge-session-broker.js +10 -5
  211. package/dist/core/verdict/judge-transport.d.ts +5 -1
  212. package/dist/core/verdict/judge-transport.js +24 -14
  213. package/dist/core/verdict/judge.d.ts +2 -15
  214. package/dist/core/verdict/judge.js +5 -68
  215. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  216. package/dist/core/verdict/launcher-resolve.js +14 -16
  217. package/dist/core/verdict/prescan.d.ts +15 -0
  218. package/dist/core/verdict/prescan.js +68 -0
  219. package/dist/core/verdict/shell-policy.d.ts +26 -0
  220. package/dist/core/verdict/shell-policy.js +40 -0
  221. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  222. package/dist/core/verdict/shell-semantics.js +225 -0
  223. package/dist/core/verdict/types.d.ts +18 -1
  224. package/dist/core/verdict/verdict.js +313 -178
  225. package/dist/corpus/evaluate.js +3 -7
  226. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  227. package/dist/corpus/gate-latency-budget.js +33 -0
  228. package/dist/corpus/mutators.d.ts +1 -1
  229. package/dist/corpus/mutators.js +11 -0
  230. package/dist/corpus/runtime-match.js +1 -3
  231. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  232. package/dist/judge-broker-daemon.js +15 -1
  233. package/dist/services/sandbox-service.d.ts +4 -0
  234. package/dist/services/sandbox-service.js +28 -2
  235. package/dist/types.d.ts +13 -2
  236. package/dist/version.d.ts +1 -1
  237. package/dist/version.js +1 -1
  238. package/package.json +1 -1
  239. package/skills/belay/SKILL.md +8 -3
  240. package/skills/belay/belay-approve.md +10 -7
@@ -1,10 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { randomUUID } from 'node:crypto';
3
- import { existsSync } from 'node:fs';
4
- import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
5
2
  import os from 'node:os';
6
3
  import path from 'node:path';
7
- import { canonicalPath } from '../path-utils.js';
4
+ import { canonicalPath, isPathOutsideRoot, pathWithinRoot } from '../path-utils.js';
5
+ import { runProcessWithBoundedOutput } from '../process-runner.js';
6
+ import { applyObservedChanges, buildObservedChangesFromTransactional, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './apply-observed-changes.js';
7
+ export { TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU };
8
8
  function execGit(repoRoot, args) {
9
9
  return new Promise((resolve, reject) => {
10
10
  const child = spawn('git', ['-C', repoRoot, ...args], {
@@ -24,6 +24,22 @@ function execGit(repoRoot, args) {
24
24
  });
25
25
  });
26
26
  }
27
+ function porcelainRelativePath(line) {
28
+ if (line.length < 4) {
29
+ return null;
30
+ }
31
+ const relativePath = line.slice(3);
32
+ return relativePath || null;
33
+ }
34
+ function isIgnoredDirtyPath(repoRoot, relativePath, ignoreRoots) {
35
+ if (ignoreRoots.length === 0) {
36
+ return false;
37
+ }
38
+ const absolutePath = canonicalPath(path.join(repoRoot, relativePath));
39
+ return ignoreRoots.some((root) => pathWithinRoot(root, absolutePath) ||
40
+ root === absolutePath ||
41
+ pathWithinRoot(absolutePath, root));
42
+ }
27
43
  export async function isGitWorktreeAvailable(repoRoot) {
28
44
  try {
29
45
  await execGit(repoRoot, ['rev-parse', '--git-dir']);
@@ -33,18 +49,33 @@ export async function isGitWorktreeAvailable(repoRoot) {
33
49
  return false;
34
50
  }
35
51
  }
36
- export async function isDirtyWorktree(repoRoot) {
52
+ export async function isDirtyWorktree(repoRoot, options) {
37
53
  try {
38
- const status = await execGit(repoRoot, ['status', '--porcelain', '--untracked-files=no']);
39
- return status.trim().length > 0;
54
+ const status = await execGit(repoRoot, ['status', '--porcelain']);
55
+ const ignoreRoots = (options?.ignoreRoots ?? []).map((root) => canonicalPath(root));
56
+ const repoRootCanonical = canonicalPath(repoRoot);
57
+ for (const line of status.split('\n')) {
58
+ if (!line.trim()) {
59
+ continue;
60
+ }
61
+ const relativePath = porcelainRelativePath(line);
62
+ if (!relativePath) {
63
+ continue;
64
+ }
65
+ if (isIgnoredDirtyPath(repoRootCanonical, relativePath, ignoreRoots)) {
66
+ continue;
67
+ }
68
+ return true;
69
+ }
70
+ return false;
40
71
  }
41
72
  catch {
42
73
  return true;
43
74
  }
44
75
  }
45
- export async function createGitWorktreeSnapshot(repoRoot, stateDir) {
46
- const worktreePath = path.join(stateDir, `tx-${randomUUID().replaceAll('-', '')}`);
47
- await mkdir(stateDir, { recursive: true });
76
+ export async function createGitWorktreeSnapshot(repoRoot, _stateDir) {
77
+ const { mkdtemp, rm } = await import('node:fs/promises');
78
+ const worktreePath = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-'));
48
79
  await execGit(repoRoot, ['worktree', 'add', '--detach', worktreePath, 'HEAD']);
49
80
  return {
50
81
  worktreePath,
@@ -67,7 +98,7 @@ export async function createGitWorktreeSnapshot(repoRoot, stateDir) {
67
98
  export function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
68
99
  const resolvedCwd = canonicalPath(cwd);
69
100
  const relative = path.relative(canonicalPath(repoRoot), resolvedCwd);
70
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
101
+ if (isPathOutsideRoot(relative) || path.isAbsolute(relative)) {
71
102
  return worktreePath;
72
103
  }
73
104
  if (relative === '') {
@@ -76,38 +107,14 @@ export function resolveWorktreeCwd(repoRoot, worktreePath, cwd) {
76
107
  return path.join(worktreePath, relative);
77
108
  }
78
109
  export function runShellCommand(command, cwd, timeoutMs) {
79
- return new Promise((resolve) => {
80
- const child = spawn(command, {
81
- cwd,
82
- shell: true,
83
- stdio: 'ignore',
84
- env: process.env,
85
- });
86
- let timedOut = false;
87
- const timer = setTimeout(() => {
88
- timedOut = true;
89
- child.kill('SIGTERM');
90
- }, timeoutMs);
91
- child.on('error', () => {
92
- clearTimeout(timer);
93
- resolve({ exitCode: 1, signal: null, timedOut });
94
- });
95
- child.on('close', (exitCode, signal) => {
96
- clearTimeout(timer);
97
- resolve({
98
- exitCode,
99
- signal: signal ? String(signal) : null,
100
- timedOut,
101
- });
102
- });
103
- });
110
+ return runProcessWithBoundedOutput(command, [], { cwd, shell: true, env: process.env }, timeoutMs);
104
111
  }
105
112
  function parseStatusLine(line) {
106
113
  if (line.length < 4) {
107
114
  return null;
108
115
  }
109
116
  const status = line.slice(0, 2);
110
- const relativePath = line.slice(3).trim();
117
+ const relativePath = line.slice(3);
111
118
  if (!relativePath) {
112
119
  return null;
113
120
  }
@@ -123,11 +130,17 @@ function parseStatusLine(line) {
123
130
  return { relativePath, kind: 'modified' };
124
131
  }
125
132
  export async function collectWorktreeChanges(worktreePath) {
126
- const status = await execGit(worktreePath, ['status', '--porcelain']);
133
+ const status = await execGit(worktreePath, [
134
+ 'status',
135
+ '--porcelain=v1',
136
+ '-z',
137
+ '-uall',
138
+ '--no-renames',
139
+ ]);
127
140
  const changes = [];
128
141
  const seen = new Set();
129
- for (const line of status.split('\n')) {
130
- if (!line.trim()) {
142
+ for (const line of status.split('\0')) {
143
+ if (!line) {
131
144
  continue;
132
145
  }
133
146
  const change = parseStatusLine(line);
@@ -139,51 +152,13 @@ export async function collectWorktreeChanges(worktreePath) {
139
152
  }
140
153
  return changes;
141
154
  }
142
- async function rollbackAppliedChanges(actions) {
143
- for (const action of [...actions].reverse()) {
144
- try {
145
- if (action.type === 'restore') {
146
- await mkdir(path.dirname(action.target), { recursive: true });
147
- await copyFile(action.backupPath, action.target);
148
- }
149
- else {
150
- await rm(action.target, { force: true });
151
- }
152
- }
153
- catch {
154
- // best effort
155
- }
156
- }
157
- }
158
- export async function applyWorktreeChanges(worktreePath, repoRoot, changes) {
159
- const backupRoot = await mkdtemp(path.join(os.tmpdir(), 'belay-tx-rollback-'));
160
- const rollbackActions = [];
161
- try {
162
- for (const change of changes) {
163
- const target = path.join(repoRoot, change.relativePath);
164
- if (existsSync(target)) {
165
- const backupPath = path.join(backupRoot, change.relativePath);
166
- await mkdir(path.dirname(backupPath), { recursive: true });
167
- await copyFile(target, backupPath);
168
- rollbackActions.push({ type: 'restore', target, backupPath });
169
- }
170
- else if (change.kind !== 'deleted') {
171
- rollbackActions.push({ type: 'remove', target });
172
- }
173
- if (change.kind === 'deleted') {
174
- await rm(target, { force: true });
175
- continue;
176
- }
177
- const source = path.join(worktreePath, change.relativePath);
178
- await mkdir(path.dirname(target), { recursive: true });
179
- await copyFile(source, target);
180
- }
181
- }
182
- catch (error) {
183
- await rollbackAppliedChanges(rollbackActions);
184
- throw error;
185
- }
186
- finally {
187
- await rm(backupRoot, { recursive: true, force: true });
188
- }
155
+ export async function applyWorktreeChanges(worktreePath, repoRoot, changes, options) {
156
+ const observed = options?.observedChanges ??
157
+ (await buildObservedChangesFromTransactional(repoRoot, worktreePath, changes));
158
+ await applyObservedChanges({
159
+ sourceRoot: worktreePath,
160
+ targetRoot: repoRoot,
161
+ changes: observed,
162
+ afterApply: options?.afterApply,
163
+ });
189
164
  }
@@ -1,5 +1,16 @@
1
+ export { applyObservedChanges, buildObservedChangesFromTransactional, TRANSACTIONAL_APPLY_CONFLICT, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './apply-observed-changes.js';
2
+ export type { TransactionalBackend, TransactionalBackendContext, TransactionalBackendId, TransactionalBackendProbe, TransactionalBackendSelection, TransactionalSnapshot, } from './backend.js';
3
+ export { FILE_CHECKPOINT_DISABLED, FILE_CHECKPOINT_DURABLE_REQUIRED, FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, FILE_CHECKPOINT_NON_GIT_DISABLED, FILE_CHECKPOINT_NOT_IMPLEMENTED, probeTransactionalBackends, selectTransactionalBackend, } from './backend-selector.js';
1
4
  export { evaluateTransactionalDiff } from './diff-evaluator.js';
2
5
  export { isTransactionalEligible } from './eligibility.js';
6
+ export { FILE_CHECKPOINT_PROTECTED_PATH_CHANGED, fileCheckpointBackend, } from './file-checkpoint-backend.js';
7
+ export { FILE_CHECKPOINT_CWD_OUTSIDE_ROOT, FILE_CHECKPOINT_GIT_METADATA_CHANGED, FILE_CHECKPOINT_SOURCE_CHANGED, FILE_CHECKPOINT_SPLIT_INDEX_UNSUPPORTED, resolveExecutionCwdRelative, } from './file-checkpoint-git.js';
8
+ export { collectDeadOwnerStaging, type FileCheckpointOwnerMarker, isOwnerProcessAlive, removeDeadOwnerStaging, writeOwnerMarker, } from './file-checkpoint-staging.js';
9
+ export { cloneDirectoryTree, type FileCloneResult, type FileCloneStrategy, probeFileCloneStrategy, } from './file-clone.js';
10
+ export { buildFileTreeIndex, diffFileTreeIndices, FileCheckpointDiagnosticError, type FileTreeEntry, type FileTreeIndex, type ObservedFileChange, readObservedChanges, } from './file-tree.js';
11
+ export { compareRelativePathsBytewise, joinRelativePath, validateRelativePath, } from './file-tree-path.js';
12
+ export { gitWorktreeBackend } from './git-worktree-backend.js';
3
13
  export { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
4
14
  export { runTransactionalExecution } from './runner.js';
15
+ export { hashDirectoryNode, hashFileContent, type PresentSnapshotNode, readSnapshotNode, type SnapshotNode, snapshotNodesEqual, } from './snapshot-node.js';
5
16
  export type { TransactionalDiffEvaluation, TransactionalExecutionResult, TransactionalFileChange, TransactionalRunnerParams, } from './types.js';
@@ -1,4 +1,14 @@
1
+ export { applyObservedChanges, buildObservedChangesFromTransactional, TRANSACTIONAL_APPLY_CONFLICT, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './apply-observed-changes.js';
2
+ export { FILE_CHECKPOINT_DISABLED, FILE_CHECKPOINT_DURABLE_REQUIRED, FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, FILE_CHECKPOINT_NON_GIT_DISABLED, FILE_CHECKPOINT_NOT_IMPLEMENTED, probeTransactionalBackends, selectTransactionalBackend, } from './backend-selector.js';
1
3
  export { evaluateTransactionalDiff } from './diff-evaluator.js';
2
4
  export { isTransactionalEligible } from './eligibility.js';
5
+ export { FILE_CHECKPOINT_PROTECTED_PATH_CHANGED, fileCheckpointBackend, } from './file-checkpoint-backend.js';
6
+ export { FILE_CHECKPOINT_CWD_OUTSIDE_ROOT, FILE_CHECKPOINT_GIT_METADATA_CHANGED, FILE_CHECKPOINT_SOURCE_CHANGED, FILE_CHECKPOINT_SPLIT_INDEX_UNSUPPORTED, resolveExecutionCwdRelative, } from './file-checkpoint-git.js';
7
+ export { collectDeadOwnerStaging, isOwnerProcessAlive, removeDeadOwnerStaging, writeOwnerMarker, } from './file-checkpoint-staging.js';
8
+ export { cloneDirectoryTree, probeFileCloneStrategy, } from './file-clone.js';
9
+ export { buildFileTreeIndex, diffFileTreeIndices, FileCheckpointDiagnosticError, readObservedChanges, } from './file-tree.js';
10
+ export { compareRelativePathsBytewise, joinRelativePath, validateRelativePath, } from './file-tree-path.js';
11
+ export { gitWorktreeBackend } from './git-worktree-backend.js';
3
12
  export { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
4
13
  export { runTransactionalExecution } from './runner.js';
14
+ export { hashDirectoryNode, hashFileContent, readSnapshotNode, snapshotNodesEqual, } from './snapshot-node.js';
@@ -1,4 +1,5 @@
1
1
  export declare const TRANSACTIONAL_ALREADY_APPLIED = "transactional_already_applied";
2
2
  export declare const TRANSACTIONAL_OBSERVED_RISK = "transactional_observed_risk";
3
3
  export declare const TRANSACTIONAL_APPLY_FAILED = "transactional_apply_failed";
4
+ export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, } from '../recovery/fail-closed.js';
4
5
  export declare const TRANSACTIONAL_APPROVAL_BYPASS_REASONS: Set<string>;
@@ -1,8 +1,13 @@
1
1
  export const TRANSACTIONAL_ALREADY_APPLIED = 'transactional_already_applied';
2
2
  export const TRANSACTIONAL_OBSERVED_RISK = 'transactional_observed_risk';
3
3
  export const TRANSACTIONAL_APPLY_FAILED = 'transactional_apply_failed';
4
+ export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, } from '../recovery/fail-closed.js';
4
5
  export const TRANSACTIONAL_APPROVAL_BYPASS_REASONS = new Set([
5
6
  TRANSACTIONAL_OBSERVED_RISK,
6
7
  TRANSACTIONAL_ALREADY_APPLIED,
7
8
  TRANSACTIONAL_APPLY_FAILED,
9
+ 'recovery_substrate_unavailable',
10
+ 'recovery_execution_failed',
11
+ 'recovery_dirty_worktree',
12
+ 'file_checkpoint_isolation_unavailable',
8
13
  ]);
@@ -1,31 +1,91 @@
1
+ import { boundaryMountReadOnlyFromPrediction, runWithBoundaryRunnable, } from '../capability/boundary-run.js';
2
+ import { hostIntegrationBoundaryContext } from '../capability/boundary-session.js';
3
+ import { resolveGuestWorkdir } from '../capability/boundary-workspace-mount.js';
4
+ import { discardPreparedRecoveryCheckpoint, listRecoveryCheckpoints, markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, prepareRecoveryCheckpoint, reconcileRecoveryCheckpoint, } from '../recovery/checkpoint.js';
5
+ import { buildObservedChangesFromTransactional } from './apply-observed-changes.js';
6
+ import { selectTransactionalBackend } from './backend-selector.js';
1
7
  import { evaluateTransactionalDiff } from './diff-evaluator.js';
2
- import { applyWorktreeChanges, collectWorktreeChanges, createGitWorktreeSnapshot, isDirtyWorktree, isGitWorktreeAvailable, resolveWorktreeCwd, runShellCommand, } from './git-worktree.js';
8
+ import { FileCheckpointDiagnosticError } from './file-tree.js';
9
+ import { applyWorktreeChanges, resolveWorktreeCwd, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './git-worktree.js';
3
10
  import { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPLY_FAILED, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
4
11
  export async function runTransactionalExecution(params) {
5
12
  const { predicted, repoRoot, stateDir, command, cwd, timeoutMs, diffContext } = params;
6
- if (!(await isGitWorktreeAvailable(repoRoot))) {
13
+ const backendContext = {
14
+ repoRoot,
15
+ stateDir,
16
+ cwd,
17
+ dirtyIgnoreRoots: params.dirtyIgnoreRoots,
18
+ fileCheckpoint: params.fileCheckpoint,
19
+ durableCheckpointEnabled: params.checkpoint?.enabled === true,
20
+ boundaryAttestation: params.boundaryContext?.attestation ?? null,
21
+ boundaryAttestationFresh: params.boundaryContext?.attestationFresh ?? false,
22
+ boundaryDriverId: params.boundaryContext?.driverId,
23
+ };
24
+ const selection = await selectTransactionalBackend(backendContext);
25
+ if (!selection.backend) {
26
+ const skipReason = params.fileCheckpoint.enabled
27
+ ? (selection.probe.reason ?? selection.skipReason)
28
+ : (selection.skipReason ?? selection.probe.reason);
7
29
  return {
8
30
  ok: false,
9
31
  skipped: true,
10
- skipReason: 'git_worktree_unavailable',
32
+ skipReason: skipReason ?? 'transactional_execution_failed',
11
33
  predicted,
12
34
  result: predicted,
13
35
  };
14
36
  }
15
- if (await isDirtyWorktree(repoRoot)) {
16
- return {
17
- ok: false,
18
- skipped: true,
19
- skipReason: 'dirty_worktree',
20
- predicted,
21
- result: predicted,
22
- };
37
+ if (params.checkpoint?.enabled) {
38
+ try {
39
+ await listRecoveryCheckpoints(stateDir, repoRoot);
40
+ }
41
+ catch (error) {
42
+ return {
43
+ ok: false,
44
+ skipped: true,
45
+ skipReason: error instanceof Error ? error.message : 'recovery_checkpoint_reconcile_failed',
46
+ predicted,
47
+ result: predicted,
48
+ };
49
+ }
23
50
  }
24
51
  let snapshot = null;
25
52
  try {
26
- snapshot = await createGitWorktreeSnapshot(repoRoot, stateDir);
27
- const execCwd = resolveWorktreeCwd(repoRoot, snapshot.worktreePath, cwd);
28
- const shellResult = await runShellCommand(command, execCwd, timeoutMs);
53
+ snapshot = await selection.backend.prepare(backendContext);
54
+ const snapshotAudit = {
55
+ transactionalBackend: snapshot.backend,
56
+ resourceKind: snapshot.resourceKind,
57
+ baselineTreeHash: snapshot.baselineTreeHash,
58
+ snapshotFileCount: snapshot.snapshotFileCount,
59
+ snapshotSourceBytes: snapshot.snapshotSourceBytes,
60
+ snapshotWorkspaceBytes: snapshot.snapshotWorkspaceBytes,
61
+ snapshotCopyStrategy: snapshot.copyStrategy,
62
+ snapshotPrepareMs: snapshot.snapshotPrepareMs,
63
+ };
64
+ const runOptions = snapshot.backend === 'file_checkpoint' && snapshot.executionCwdRelative !== undefined
65
+ ? {
66
+ mountReadOnly: boundaryMountReadOnlyFromPrediction(predicted),
67
+ workspaceMount: {
68
+ hostSourceRoot: snapshot.executionRoot,
69
+ guestTargetRoot: snapshot.resourceRoot,
70
+ cwdRelative: snapshot.executionCwdRelative,
71
+ writable: true,
72
+ hideHostSourcePath: true,
73
+ },
74
+ }
75
+ : {
76
+ mountReadOnly: boundaryMountReadOnlyFromPrediction(predicted),
77
+ };
78
+ const execCwd = snapshot.backend === 'file_checkpoint' && runOptions.workspaceMount
79
+ ? resolveGuestWorkdir(runOptions.workspaceMount)
80
+ : resolveWorktreeCwd(repoRoot, snapshot.executionRoot, cwd);
81
+ const boundaryContext = params.boundaryContext ?? hostIntegrationBoundaryContext(params.repoRoot);
82
+ const shellResult = await runWithBoundaryRunnable(boundaryContext.driver, {
83
+ prepareContext: boundaryContext.prepareContext,
84
+ command,
85
+ cwd: execCwd,
86
+ timeoutMs,
87
+ runOptions,
88
+ });
29
89
  if (shellResult.timedOut) {
30
90
  return {
31
91
  ok: false,
@@ -33,6 +93,7 @@ export async function runTransactionalExecution(params) {
33
93
  skipReason: 'transactional_timed_out',
34
94
  predicted,
35
95
  result: predicted,
96
+ ...snapshotAudit,
36
97
  commandExitCode: shellResult.exitCode,
37
98
  commandSignal: shellResult.signal,
38
99
  timedOut: true,
@@ -45,17 +106,77 @@ export async function runTransactionalExecution(params) {
45
106
  skipReason: 'transactional_command_failed',
46
107
  predicted,
47
108
  result: predicted,
109
+ ...snapshotAudit,
48
110
  commandExitCode: shellResult.exitCode,
49
111
  commandSignal: shellResult.signal,
50
112
  };
51
113
  }
52
- const changes = await collectWorktreeChanges(snapshot.worktreePath);
114
+ const changes = await snapshot.collectChanges();
53
115
  const observed = evaluateTransactionalDiff(changes, diffContext);
54
116
  if (observed.verdict === 'allow') {
117
+ await snapshot.validateSourceState?.();
118
+ const observedChanges = await buildObservedChangesFromTransactional(snapshot.baselineRoot ?? repoRoot, snapshot.executionRoot, changes);
119
+ const checkpoint = params.checkpoint?.enabled && changes.length > 0
120
+ ? await prepareRecoveryCheckpoint({
121
+ stateDir,
122
+ repoRoot,
123
+ baselinePath: snapshot.baselineRoot ?? repoRoot,
124
+ worktreePath: snapshot.executionRoot,
125
+ commandFingerprint: predicted.fingerprint,
126
+ changes,
127
+ protectedRoots: diffContext.protectedRoots,
128
+ config: params.checkpoint,
129
+ backend: snapshot.backend,
130
+ })
131
+ : null;
55
132
  try {
56
- await applyWorktreeChanges(snapshot.worktreePath, repoRoot, changes);
133
+ if (checkpoint) {
134
+ await markRecoveryCheckpointApplying(stateDir, checkpoint);
135
+ }
136
+ let receipt;
137
+ await applyWorktreeChanges(snapshot.executionRoot, repoRoot, changes, {
138
+ observedChanges,
139
+ afterApply: checkpoint
140
+ ? async () => {
141
+ receipt = await markRecoveryCheckpointApplied(stateDir, checkpoint);
142
+ }
143
+ : undefined,
144
+ });
145
+ const result = {
146
+ ...predicted,
147
+ verdict: 'allow',
148
+ reason: TRANSACTIONAL_ALREADY_APPLIED,
149
+ assessment: observed.assessment,
150
+ };
151
+ return {
152
+ ok: true,
153
+ predicted,
154
+ observed,
155
+ result,
156
+ worktreePath: snapshot.executionRoot,
157
+ ...snapshotAudit,
158
+ commandExitCode: shellResult.exitCode,
159
+ commandSignal: shellResult.signal,
160
+ timedOut: shellResult.timedOut,
161
+ ...(checkpoint && receipt
162
+ ? {
163
+ recoveryCheckpointId: checkpoint.checkpointId,
164
+ recoveryBackend: snapshot.backend,
165
+ recoveryProofHash: receipt.proofHash,
166
+ recoveryState: 'applied',
167
+ }
168
+ : {}),
169
+ };
57
170
  }
58
- catch {
171
+ catch (error) {
172
+ if (checkpoint) {
173
+ const recoveryState = await reconcileRecoveryCheckpoint(stateDir, checkpoint.checkpointId);
174
+ if (recoveryState === 'prepared') {
175
+ await discardPreparedRecoveryCheckpoint(stateDir, checkpoint.checkpointId);
176
+ }
177
+ }
178
+ const toctou = error instanceof Error && error.message === TRANSACTIONAL_APPLY_TOCTOU;
179
+ const rollbackFailed = error instanceof Error && error.message === TRANSACTIONAL_APPLY_ROLLBACK_FAILED;
59
180
  const result = {
60
181
  ...predicted,
61
182
  verdict: 'deny_pending_approval',
@@ -64,7 +185,14 @@ export async function runTransactionalExecution(params) {
64
185
  ...observed.assessment,
65
186
  reversibility: 'irreversible',
66
187
  confidence: 1,
67
- signals: [...observed.assessment.signals, 'transactional_apply_failed'],
188
+ signals: [
189
+ ...observed.assessment.signals,
190
+ rollbackFailed
191
+ ? 'transactional_apply_rollback_failed'
192
+ : toctou
193
+ ? 'transactional_apply_toctou'
194
+ : 'transactional_apply_failed',
195
+ ],
68
196
  },
69
197
  };
70
198
  return {
@@ -72,7 +200,8 @@ export async function runTransactionalExecution(params) {
72
200
  predicted,
73
201
  observed,
74
202
  result,
75
- worktreePath: snapshot.worktreePath,
203
+ worktreePath: snapshot.executionRoot,
204
+ ...snapshotAudit,
76
205
  commandExitCode: shellResult.exitCode,
77
206
  commandSignal: shellResult.signal,
78
207
  timedOut: shellResult.timedOut,
@@ -81,8 +210,8 @@ export async function runTransactionalExecution(params) {
81
210
  }
82
211
  const result = {
83
212
  ...predicted,
84
- verdict: observed.verdict === 'allow' ? 'allow' : 'deny_pending_approval',
85
- reason: observed.verdict === 'allow' ? TRANSACTIONAL_ALREADY_APPLIED : TRANSACTIONAL_OBSERVED_RISK,
213
+ verdict: 'deny_pending_approval',
214
+ reason: TRANSACTIONAL_OBSERVED_RISK,
86
215
  assessment: observed.assessment,
87
216
  };
88
217
  return {
@@ -90,7 +219,8 @@ export async function runTransactionalExecution(params) {
90
219
  predicted,
91
220
  observed,
92
221
  result,
93
- worktreePath: snapshot.worktreePath,
222
+ worktreePath: snapshot.executionRoot,
223
+ ...snapshotAudit,
94
224
  commandExitCode: shellResult.exitCode,
95
225
  commandSignal: shellResult.signal,
96
226
  timedOut: shellResult.timedOut,
@@ -101,13 +231,31 @@ export async function runTransactionalExecution(params) {
101
231
  ok: false,
102
232
  skipped: true,
103
233
  skipReason: error instanceof Error ? error.message : 'transactional_execution_failed',
234
+ ...(error instanceof FileCheckpointDiagnosticError ? { skipDetail: error.diagnostic } : {}),
104
235
  predicted,
105
236
  result: predicted,
237
+ ...(snapshot
238
+ ? {
239
+ transactionalBackend: snapshot.backend,
240
+ resourceKind: snapshot.resourceKind,
241
+ baselineTreeHash: snapshot.baselineTreeHash,
242
+ snapshotFileCount: snapshot.snapshotFileCount,
243
+ snapshotSourceBytes: snapshot.snapshotSourceBytes,
244
+ snapshotWorkspaceBytes: snapshot.snapshotWorkspaceBytes,
245
+ snapshotCopyStrategy: snapshot.copyStrategy,
246
+ snapshotPrepareMs: snapshot.snapshotPrepareMs,
247
+ }
248
+ : {}),
106
249
  };
107
250
  }
108
251
  finally {
109
252
  if (snapshot) {
110
- await snapshot.cleanup();
253
+ try {
254
+ await snapshot.cleanup();
255
+ }
256
+ catch {
257
+ // Cleanup is best effort and must not overturn a verified apply or observed-risk result.
258
+ }
111
259
  }
112
260
  }
113
261
  }
@@ -0,0 +1,26 @@
1
+ export type SnapshotNode = {
2
+ kind: 'absent';
3
+ } | {
4
+ kind: 'file';
5
+ mode: number;
6
+ size: number;
7
+ hash: string;
8
+ } | {
9
+ kind: 'symlink';
10
+ target: string;
11
+ hash: string;
12
+ } | {
13
+ kind: 'directory';
14
+ mode: number;
15
+ hash: string;
16
+ };
17
+ export type PresentSnapshotNode = Exclude<SnapshotNode, {
18
+ kind: 'absent';
19
+ }>;
20
+ export declare function hashFileContent(filePath: string): Promise<string>;
21
+ export declare function hashSymlinkTarget(target: string): string;
22
+ export declare function hashDirectoryNode(mode: number): string;
23
+ export declare function hashFileNode(mode: number, contentHash: string): string;
24
+ export declare function snapshotNodesEqual(left: SnapshotNode, right: SnapshotNode): boolean;
25
+ export declare function readSnapshotNode(absolutePath: string): Promise<SnapshotNode>;
26
+ export declare function presentNodeKind(node: PresentSnapshotNode): 'file' | 'symlink' | 'directory';
@@ -0,0 +1,87 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
3
+ import { lstat, readlink } from 'node:fs/promises';
4
+ export async function hashFileContent(filePath) {
5
+ const hash = createHash('sha256');
6
+ await new Promise((resolve, reject) => {
7
+ const stream = createReadStream(filePath);
8
+ stream.on('data', (chunk) => hash.update(chunk));
9
+ stream.on('error', reject);
10
+ stream.on('end', () => resolve());
11
+ });
12
+ return hash.digest('hex');
13
+ }
14
+ export function hashSymlinkTarget(target) {
15
+ return createHash('sha256').update(`symlink:${target}`).digest('hex');
16
+ }
17
+ export function hashDirectoryNode(mode) {
18
+ return createHash('sha256')
19
+ .update(`directory:${mode & 0o777}`)
20
+ .digest('hex');
21
+ }
22
+ export function hashFileNode(mode, contentHash) {
23
+ return createHash('sha256')
24
+ .update(`file:${mode & 0o777}:${contentHash}`)
25
+ .digest('hex');
26
+ }
27
+ export function snapshotNodesEqual(left, right) {
28
+ if (left.kind === 'absent' || right.kind === 'absent') {
29
+ return left.kind === right.kind;
30
+ }
31
+ if (left.kind !== right.kind) {
32
+ return false;
33
+ }
34
+ switch (left.kind) {
35
+ case 'file':
36
+ return (right.kind === 'file' &&
37
+ left.mode === right.mode &&
38
+ left.size === right.size &&
39
+ left.hash === right.hash);
40
+ case 'symlink':
41
+ return right.kind === 'symlink' && left.target === right.target && left.hash === right.hash;
42
+ case 'directory':
43
+ return right.kind === 'directory' && left.mode === right.mode && left.hash === right.hash;
44
+ default:
45
+ return false;
46
+ }
47
+ }
48
+ export async function readSnapshotNode(absolutePath) {
49
+ let info;
50
+ try {
51
+ info = await lstat(absolutePath);
52
+ }
53
+ catch (error) {
54
+ if (['ENOENT', 'ENOTDIR'].includes(error.code ?? '')) {
55
+ return { kind: 'absent' };
56
+ }
57
+ throw error;
58
+ }
59
+ if (info.isSymbolicLink()) {
60
+ const target = await readlink(absolutePath);
61
+ return {
62
+ kind: 'symlink',
63
+ target,
64
+ hash: hashSymlinkTarget(target),
65
+ };
66
+ }
67
+ if (info.isFile()) {
68
+ const contentHash = await hashFileContent(absolutePath);
69
+ return {
70
+ kind: 'file',
71
+ mode: info.mode,
72
+ size: info.size,
73
+ hash: hashFileNode(info.mode, contentHash),
74
+ };
75
+ }
76
+ if (info.isDirectory()) {
77
+ return {
78
+ kind: 'directory',
79
+ mode: info.mode,
80
+ hash: hashDirectoryNode(info.mode),
81
+ };
82
+ }
83
+ throw new Error('file_checkpoint_unsupported_node');
84
+ }
85
+ export function presentNodeKind(node) {
86
+ return node.kind;
87
+ }