forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6

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 (196) hide show
  1. package/AGENTS.md +18 -7
  2. package/CHANGELOG.md +79 -1
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/README.md +6 -2
  6. package/bin/forge-cmd.js +20 -0
  7. package/bin/forge.js +28 -375
  8. package/docs/INDEX.md +1 -1
  9. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  10. package/docs/guides/MIGRATION.md +4 -4
  11. package/docs/guides/SETUP.md +16 -16
  12. package/docs/reference/COMMANDS.md +8 -5
  13. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  14. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  15. package/docs/reference/INSTALL.md +4 -0
  16. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  17. package/docs/reference/RELEASE.md +5 -3
  18. package/docs/reference/TOOLCHAIN.md +8 -0
  19. package/docs/reference/github-accounts.md +134 -0
  20. package/docs/reference/protected-state-surfaces.md +4 -4
  21. package/docs/reference/shepherd.md +114 -35
  22. package/lefthook.yml +12 -0
  23. package/lib/activation/ensure-forge-home.js +33 -15
  24. package/lib/adapters/pr-state-adapter.js +359 -144
  25. package/lib/audit-evidence.js +71 -110
  26. package/lib/base-remote.js +138 -0
  27. package/lib/beta5-compatibility-evidence.js +1093 -0
  28. package/lib/bun-lockfile-proof.js +413 -0
  29. package/lib/bun-workflow-pins.js +461 -0
  30. package/lib/capabilities/index.js +9 -0
  31. package/lib/capabilities/model.js +141 -0
  32. package/lib/capabilities/probes.js +347 -0
  33. package/lib/capped-jsonl-log.js +236 -0
  34. package/lib/codex-skills.js +2 -2
  35. package/lib/commands/_manifest.js +1 -0
  36. package/lib/commands/_registry.js +50 -20
  37. package/lib/commands/clean.js +252 -32
  38. package/lib/commands/dev.js +4 -33
  39. package/lib/commands/doctor.js +37 -6
  40. package/lib/commands/gate.js +197 -27
  41. package/lib/commands/github.js +215 -0
  42. package/lib/commands/hooks.js +276 -30
  43. package/lib/commands/insights.js +8 -3
  44. package/lib/commands/memory.js +66 -2
  45. package/lib/commands/merge.js +1265 -58
  46. package/lib/commands/plan.js +33 -2
  47. package/lib/commands/pr.js +3 -1
  48. package/lib/commands/preflight.js +21 -4
  49. package/lib/commands/prime.js +21 -8
  50. package/lib/commands/push.js +146 -54
  51. package/lib/commands/recall.js +127 -49
  52. package/lib/commands/recap.js +6 -1
  53. package/lib/commands/release.js +39 -3
  54. package/lib/commands/remember.js +28 -4
  55. package/lib/commands/serve.js +26 -9
  56. package/lib/commands/setup.js +323 -98
  57. package/lib/commands/shepherd.js +591 -73
  58. package/lib/commands/ship.js +36 -91
  59. package/lib/commands/skill.js +127 -11
  60. package/lib/commands/status.js +17 -1
  61. package/lib/commands/team.js +47 -8
  62. package/lib/commands/test.js +187 -38
  63. package/lib/commands/validate.js +65 -21
  64. package/lib/commands/worktree.js +359 -45
  65. package/lib/core/runtime-graph.js +1 -1
  66. package/lib/doc-assertions.js +297 -0
  67. package/lib/existing-tdd-gate.js +253 -0
  68. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  69. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  70. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  71. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  72. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  73. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  74. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  75. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  76. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  77. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  78. package/lib/forge-context.js +1 -4
  79. package/lib/forge-issues.js +134 -32
  80. package/lib/gate-events.js +98 -10
  81. package/lib/git-defaults.js +56 -0
  82. package/lib/github-context.js +308 -0
  83. package/lib/global-flags.js +1 -0
  84. package/lib/harness-capability-matrix.js +3 -3
  85. package/lib/hook-renderer.js +122 -5
  86. package/lib/insights.js +96 -80
  87. package/lib/issue-render.js +19 -0
  88. package/lib/kernel/backing-issue.js +14 -2
  89. package/lib/kernel/broker.js +739 -31
  90. package/lib/kernel/claim-reconciler.js +238 -0
  91. package/lib/kernel/cli-broker-factory.js +12 -1
  92. package/lib/kernel/close-on-merge.js +154 -0
  93. package/lib/kernel/fs-class.js +42 -25
  94. package/lib/kernel/lease-enforcer.js +9 -4
  95. package/lib/kernel/legacy-claim-repair.js +442 -0
  96. package/lib/kernel/live-claim-projection.js +26 -0
  97. package/lib/kernel/migrations.js +118 -3
  98. package/lib/kernel/readiness-model.js +184 -12
  99. package/lib/kernel/schema.js +49 -1
  100. package/lib/kernel/sqlite-driver.js +3435 -172
  101. package/lib/kernel/taxonomy-validator.js +4 -1
  102. package/lib/kernel/windows-private-acl.js +239 -0
  103. package/lib/lefthook-wiring.js +21 -1
  104. package/lib/memory/hygiene.js +191 -0
  105. package/lib/memory/router.js +110 -28
  106. package/lib/memory/usage-evidence.js +4 -0
  107. package/lib/memory-digest.js +106 -15
  108. package/lib/memory-recall-events.js +145 -0
  109. package/lib/memory-recall.js +71 -10
  110. package/lib/merge-rules.js +143 -21
  111. package/lib/npm-publish-workflow.js +465 -0
  112. package/lib/orientation.js +68 -43
  113. package/lib/package-root.js +2 -0
  114. package/lib/plugin-catalog.js +14 -4
  115. package/lib/pr-bundle.js +5 -6
  116. package/lib/pr-monitor/auto-actions.js +169 -28
  117. package/lib/pr-monitor/differ.js +110 -4
  118. package/lib/pr-monitor/events.js +0 -0
  119. package/lib/pr-monitor/flow-monitor.js +1424 -0
  120. package/lib/pr-monitor/gather.js +251 -44
  121. package/lib/pr-monitor/journal.js +18 -39
  122. package/lib/pr-monitor/monitor.js +117 -10
  123. package/lib/pr-monitor/process-identity.js +117 -0
  124. package/lib/pr-monitor/reconcile-executor.js +1129 -470
  125. package/lib/pr-monitor/reconcile.js +0 -0
  126. package/lib/pr-monitor/render-summary.js +293 -0
  127. package/lib/pr-monitor/review-preflight.js +269 -0
  128. package/lib/pr-monitor/shepherd-lease.js +38 -20
  129. package/lib/pr-monitor/verdict.js +438 -0
  130. package/lib/pr-monitor/watch-lifecycle.js +145 -27
  131. package/lib/pr-monitor/watch-owner.js +1414 -0
  132. package/lib/pr-monitor/watch.js +129 -58
  133. package/lib/pr-pull.js +33 -14
  134. package/lib/pr-shepherd.js +51 -11
  135. package/lib/preflight/gates.js +65 -18
  136. package/lib/preflight/runner.js +5 -0
  137. package/lib/project-memory.js +178 -4
  138. package/lib/protected-state-authority.js +1100 -0
  139. package/lib/protected-state-surfaces.js +243 -45
  140. package/lib/release-readiness.js +53 -7
  141. package/lib/review-adapter.js +65 -0
  142. package/lib/shell-utils.js +1 -1
  143. package/lib/skills-sync.js +71 -35
  144. package/lib/smart-merge.js +28 -4
  145. package/lib/symlink-utils.js +74 -26
  146. package/lib/upgrade-safety.js +39 -0
  147. package/lib/using-forge.js +19 -6
  148. package/lib/validation/risk-manifest.js +339 -0
  149. package/lib/workflow/enforce-stage.js +44 -0
  150. package/lib/workflow/plan-authority.js +225 -0
  151. package/package.json +12 -9
  152. package/scripts/commitlint.js +13 -15
  153. package/scripts/doc-asserting-tests.js +158 -0
  154. package/scripts/generate-risk-manifest.js +91 -0
  155. package/scripts/github-context-bridge.sh +10 -0
  156. package/scripts/legacy-claim-repair.js +145 -0
  157. package/scripts/lib/behavioral-eval-runner.js +310 -0
  158. package/scripts/lib/behavioral-eval-runtime.js +457 -0
  159. package/scripts/lib/eval-evidence.js +328 -0
  160. package/scripts/lib/eval-runner.js +81 -41
  161. package/scripts/lib/immutable-eval-corpus.js +309 -0
  162. package/scripts/lib/promotion-evidence-loader.js +94 -0
  163. package/scripts/lib/promotion-scorecard.js +314 -0
  164. package/scripts/npm-release-receipt.js +134 -0
  165. package/scripts/process-tree.js +773 -0
  166. package/scripts/protected-state-check.js +479 -31
  167. package/scripts/run-command-eval.js +29 -1
  168. package/scripts/sync-agent-skills.js +333 -34
  169. package/scripts/sync-d20-audit.js +172 -0
  170. package/scripts/test-full-suite.js +935 -37
  171. package/scripts/test-profile.js +13 -3
  172. package/scripts/test.js +271 -57
  173. package/skills/coverage.json +1 -0
  174. package/skills/review/SKILL.md +6 -11
  175. package/skills/review/evals/scorecard.json +4 -4
  176. package/skills/rollback/SKILL.md +4 -11
  177. package/skills/rollback/evals/scorecard.json +3 -3
  178. package/skills/setup/SKILL.md +18 -0
  179. package/skills/setup/evals/scorecard.json +3 -3
  180. package/skills/shepherd/SKILL.md +39 -16
  181. package/skills/shepherd/evals/scorecard.json +4 -4
  182. package/skills/ship/SKILL.md +4 -12
  183. package/skills/ship/evals/scorecard.json +3 -3
  184. package/skills/validate/SKILL.md +3 -0
  185. package/skills/validate/evals/scorecard.json +1 -1
  186. package/skills/worktree/SKILL.md +6 -1
  187. package/skills/worktree/evals/scorecard.json +2 -2
  188. package/lib/beads-setup.js +0 -538
  189. package/lib/beads-sync-scaffold.js +0 -189
  190. package/lib/pat-setup.js +0 -207
  191. package/lib/pr-monitor/render-sticky.js +0 -206
  192. package/lib/pr-monitor/upsert-sticky.js +0 -169
  193. package/scripts/beads-context.sh +0 -577
  194. package/scripts/beads-migrate-to-dolt.sh +0 -7
  195. package/scripts/beads-upgrade-smoke.sh +0 -284
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -0,0 +1,413 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { execFileSync, spawnSync: defaultSpawnSync } = require('node:child_process');
7
+ const { hashProtectedContent } = require('./protected-state-surfaces');
8
+
9
+ const REGULAR_GIT_MODES = new Set(['100644', '100755']);
10
+ const REPOSITORY_LOCAL_GIT_ENVIRONMENT = new Set([
11
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
12
+ 'GIT_CONFIG',
13
+ 'GIT_CONFIG_PARAMETERS',
14
+ 'GIT_CONFIG_COUNT',
15
+ 'GIT_OBJECT_DIRECTORY',
16
+ 'GIT_DIR',
17
+ 'GIT_WORK_TREE',
18
+ 'GIT_IMPLICIT_WORK_TREE',
19
+ 'GIT_GRAFT_FILE',
20
+ 'GIT_INDEX_FILE',
21
+ 'GIT_NO_REPLACE_OBJECTS',
22
+ 'GIT_REPLACE_REF_BASE',
23
+ 'GIT_PREFIX',
24
+ 'GIT_SHALLOW_FILE',
25
+ 'GIT_COMMON_DIR',
26
+ ]);
27
+ const VERSION_TIMEOUT_MS = 5_000;
28
+ const INSTALL_TIMEOUT_MS = 30_000;
29
+ const SAFE_FAILURE_REASONS = new Set([
30
+ 'Working tree has inputs outside the captured Git index',
31
+ 'Workspace graph contains a symlink or submodule',
32
+ 'Tracked .npmrc is outside the deterministic proof contract',
33
+ 'Indexed bun.lock is missing or is not a regular file',
34
+ 'Proposed bun.lock is empty',
35
+ 'Committed bun.lock is missing or is not a regular file',
36
+ 'Committed bun.lock is empty',
37
+ 'Proposed bun.lock does not differ from committed HEAD',
38
+ 'Indexed root package.json is missing',
39
+ 'Package-manager input is not a regular file',
40
+ 'Indexed root package.json is invalid',
41
+ 'Root packageManager must pin an exact Bun version',
42
+ 'Workspace path is invalid',
43
+ 'Workspace path escapes the proof root',
44
+ 'Local dependency path escapes the proof root',
45
+ 'Bun version check failed',
46
+ 'Bun version is not an exact semantic version',
47
+ 'Installed Bun does not match root packageManager',
48
+ 'Bun lockfile regeneration failed',
49
+ 'Committed HEAD changed during Bun lockfile proof',
50
+ 'Git index changed during Bun lockfile proof',
51
+ 'Regenerated bun.lock is not a regular file',
52
+ 'Regenerated bun.lock does not match the staged content',
53
+ ]);
54
+
55
+ function blocked(reason, proposed = Buffer.alloc(0)) {
56
+ return {
57
+ allowed: false,
58
+ decision: 'blocked',
59
+ path: 'bun.lock',
60
+ requiredSurface: 'lockfiles',
61
+ declaredSurface: null,
62
+ operation: 'staged_edit',
63
+ contentHash: proposed.length ? hashProtectedContent(proposed) : null,
64
+ reason,
65
+ repairHint: 'Stage all intended files and the bun.lock produced by the exact pinned Bun version, then retry.',
66
+ };
67
+ }
68
+
69
+ function allowed(proposed) {
70
+ return {
71
+ allowed: true,
72
+ decision: 'allowed',
73
+ path: 'bun.lock',
74
+ requiredSurface: 'lockfiles',
75
+ declaredSurface: 'lockfiles',
76
+ operation: 'staged_edit',
77
+ contentHash: hashProtectedContent(proposed),
78
+ reason: 'Pinned Bun regeneration from captured inputs matched the indexed bun.lock byte-for-byte.',
79
+ repairHint: null,
80
+ };
81
+ }
82
+
83
+ function parseBunVersion(output) {
84
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(output || '').trim());
85
+ if (!match) throw new Error('Bun version is not an exact semantic version');
86
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
87
+ }
88
+
89
+ function parsePinnedBunVersion(packageJson) {
90
+ const match = /^bun@(\d+\.\d+\.\d+)$/.exec(String(packageJson.packageManager || ''));
91
+ if (!match) throw new Error('Root packageManager must pin an exact Bun version');
92
+ parseBunVersion(match[1]);
93
+ return match[1];
94
+ }
95
+
96
+ function normalizeSeparators(filePath) {
97
+ return String(filePath).replaceAll('\\', '/');
98
+ }
99
+
100
+ function resolveContainedPath(root, relativePath) {
101
+ const raw = String(relativePath || '');
102
+ const normalized = normalizeSeparators(raw);
103
+ if (!normalized || normalized.includes('\0') || isPortableAbsolute(raw) || isPortableAbsolute(normalized)) {
104
+ throw new Error('Path escape or invalid path');
105
+ }
106
+ const rootPath = path.resolve(root);
107
+ const target = path.resolve(rootPath, ...normalized.split('/'));
108
+ if (target === rootPath || !target.startsWith(`${rootPath}${path.sep}`)) {
109
+ throw new Error('Path escape outside proof root');
110
+ }
111
+ return target;
112
+ }
113
+
114
+ function gitEnvironment() {
115
+ return Object.fromEntries(Object.entries(process.env).filter(([key]) => {
116
+ const normalized = key.toUpperCase();
117
+ return !REPOSITORY_LOCAL_GIT_ENVIRONMENT.has(normalized) &&
118
+ !/^GIT_CONFIG_(?:KEY|VALUE)_\d+$/.test(normalized);
119
+ }));
120
+ }
121
+
122
+ function git(projectRoot, args, options = {}) {
123
+ return execFileSync('git', ['-C', projectRoot, ...args], { // NOSONAR S4036 -- Fixed local Git executable with structured non-shell arguments is the trusted repository toolchain boundary.
124
+ encoding: options.encoding === undefined ? null : options.encoding,
125
+ stdio: ['ignore', 'pipe', 'pipe'],
126
+ timeout: options.timeout || 10_000,
127
+ maxBuffer: 16 * 1024 * 1024,
128
+ env: gitEnvironment(),
129
+ });
130
+ }
131
+
132
+ function captureIndexTree(projectRoot) {
133
+ return git(projectRoot, ['write-tree']).toString('utf8').trim();
134
+ }
135
+
136
+ function captureHead(projectRoot) {
137
+ const oid = git(projectRoot, ['rev-parse', '--verify', 'HEAD']).toString('utf8').trim();
138
+ const tree = git(projectRoot, ['rev-parse', '--verify', `${oid}^{tree}`]).toString('utf8').trim();
139
+ return { oid, tree };
140
+ }
141
+
142
+ function parseTreeEntries(projectRoot, tree) {
143
+ return git(projectRoot, ['ls-tree', '-r', '-z', tree])
144
+ .toString('utf8')
145
+ .split('\0')
146
+ .filter(Boolean)
147
+ .map(record => {
148
+ const match = /^(\d+) (blob|tree|commit) ([0-9a-f]+)\t([\s\S]+)$/.exec(record);
149
+ if (!match) throw new Error('Git tree evidence is malformed');
150
+ return { mode: match[1], type: match[2], hash: match[3], path: match[4] };
151
+ });
152
+ }
153
+
154
+ function isLockInput(filePath) {
155
+ return filePath === 'package.json' || filePath.endsWith('/package.json');
156
+ }
157
+
158
+ function assertNoDirtyInputs(projectRoot) {
159
+ const status = git(projectRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']).toString('utf8');
160
+ const records = status.split('\0').filter(Boolean);
161
+ for (let index = 0; index < records.length; index += 1) {
162
+ const record = records[index];
163
+ const state = record.slice(0, 2);
164
+ if (state === '??' || state[1] !== ' ') {
165
+ throw new Error('Working tree has inputs outside the captured Git index');
166
+ }
167
+ if (/[RC]/.test(state)) index += 1;
168
+ }
169
+ }
170
+
171
+ function readBlob(projectRoot, hash) {
172
+ return git(projectRoot, ['cat-file', 'blob', hash]);
173
+ }
174
+
175
+ function isPortableAbsolute(filePath) {
176
+ return path.posix.isAbsolute(filePath) || path.win32.isAbsolute(filePath) || /^[A-Za-z]:/.test(filePath) || /^[/\\]{2}/.test(filePath);
177
+ }
178
+
179
+ function hasTraversal(filePath) {
180
+ return /(^|[/\\{,(|])\.\.(?=$|[/\\},)|])/.test(filePath);
181
+ }
182
+
183
+ function validateWorkspacePaths(rootManifest) {
184
+ const workspaces = Array.isArray(rootManifest.workspaces)
185
+ ? rootManifest.workspaces
186
+ : rootManifest.workspaces?.packages;
187
+ if (workspaces === undefined) return;
188
+ if (!Array.isArray(workspaces)) throw new Error('Root workspaces must be an array');
189
+ for (const workspace of workspaces) {
190
+ if (typeof workspace !== 'string' || !workspace || workspace.includes('\0')) {
191
+ throw new Error('Workspace path is invalid');
192
+ }
193
+ const normalized = normalizeSeparators(workspace);
194
+ if (isPortableAbsolute(normalized) || hasTraversal(normalized)) {
195
+ throw new Error('Workspace path escapes the proof root');
196
+ }
197
+ }
198
+ }
199
+
200
+ function workspaceContainsEntry(workspaces, entryPath) {
201
+ return workspaces.some(workspace => {
202
+ const normalized = normalizeSeparators(workspace);
203
+ const special = normalized.search(/[?*{[!]/);
204
+ const prefix = (special === -1 ? normalized : normalized.slice(0, special)).replace(/\/$/, '');
205
+ return prefix && (entryPath === prefix || entryPath.startsWith(`${prefix}/`));
206
+ });
207
+ }
208
+
209
+ function validateLocalDependencyPaths(manifest) {
210
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
211
+ for (const specifier of Object.values(manifest[field] || {})) {
212
+ if (typeof specifier !== 'string' || !/^(?:file|link):/.test(specifier)) continue;
213
+ const relative = normalizeSeparators(specifier.replace(/^(?:file|link):/, ''));
214
+ if (isPortableAbsolute(relative) || hasTraversal(relative)) {
215
+ throw new Error('Local dependency path escapes the proof root');
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ function writeRegularFile(root, filePath, content) {
222
+ const target = resolveContainedPath(root, filePath);
223
+ fs.mkdirSync(path.dirname(target), { recursive: true });
224
+ const rootReal = fs.realpathSync(root);
225
+ const parentReal = fs.realpathSync(path.dirname(target));
226
+ if (parentReal !== rootReal && !parentReal.startsWith(`${rootReal}${path.sep}`)) {
227
+ throw new Error('Path escape through proof directory');
228
+ }
229
+ fs.writeFileSync(target, content, { flag: 'wx' });
230
+ }
231
+
232
+ function sanitizedEnvironment(proofContainer) {
233
+ const allowedKeys = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'COMSPEC', 'WINDIR', 'TEMP', 'TMP', 'LANG', 'LC_ALL']);
234
+ const env = {};
235
+ for (const [key, value] of Object.entries(process.env)) {
236
+ if (allowedKeys.has(key.toUpperCase())) env[key] = value;
237
+ }
238
+ env.HOME = proofContainer;
239
+ env.USERPROFILE = env.HOME;
240
+ env.APPDATA = resolveContainedPath(proofContainer, '.appdata');
241
+ env.BUN_INSTALL_CACHE_DIR = resolveContainedPath(proofContainer, '.bun-cache');
242
+ return env;
243
+ }
244
+
245
+ function processSucceeded(result) {
246
+ return Boolean(result?.status === 0 && !result.error && !result.signal);
247
+ }
248
+
249
+ function readLockTransition(repoRoot) {
250
+ const head = captureHead(repoRoot);
251
+ const indexTree = captureIndexTree(repoRoot);
252
+ const entries = parseTreeEntries(repoRoot, indexTree);
253
+ const headEntries = parseTreeEntries(repoRoot, head.tree);
254
+ const proposedEntry = entries.find(entry => entry.path === 'bun.lock');
255
+ const baseEntry = headEntries.find(entry => entry.path === 'bun.lock');
256
+ return {
257
+ head,
258
+ indexTree,
259
+ entries,
260
+ proposedEntry,
261
+ proposed: proposedEntry?.type === 'blob' ? readBlob(repoRoot, proposedEntry.hash) : Buffer.alloc(0),
262
+ baseEntry,
263
+ baseLock: baseEntry?.type === 'blob' ? readBlob(repoRoot, baseEntry.hash) : Buffer.alloc(0),
264
+ };
265
+ }
266
+
267
+ function assertLockTransition(transition) {
268
+ if (!transition.proposedEntry || !REGULAR_GIT_MODES.has(transition.proposedEntry.mode)) {
269
+ throw new Error('Indexed bun.lock is missing or is not a regular file');
270
+ }
271
+ if (!transition.proposed.length) throw new Error('Proposed bun.lock is empty');
272
+ if (transition.baseEntry?.type !== 'blob' || !REGULAR_GIT_MODES.has(transition.baseEntry.mode)) {
273
+ throw new Error('Committed bun.lock is missing or is not a regular file');
274
+ }
275
+ if (!transition.baseLock.length) throw new Error('Committed bun.lock is empty');
276
+ if (transition.baseLock.equals(transition.proposed)) {
277
+ throw new Error('Proposed bun.lock does not differ from committed HEAD');
278
+ }
279
+ }
280
+
281
+ function readManifestProofInputs(repoRoot, entries) {
282
+ if (entries.some(entry => entry.path === '.npmrc' || entry.path.endsWith('/.npmrc'))) {
283
+ throw new Error('Tracked .npmrc is outside the deterministic proof contract');
284
+ }
285
+ const inputs = entries.filter(entry => isLockInput(entry.path));
286
+ if (!inputs.some(entry => entry.path === 'package.json')) {
287
+ throw new Error('Indexed root package.json is missing');
288
+ }
289
+ if (inputs.some(entry => !REGULAR_GIT_MODES.has(entry.mode))) {
290
+ throw new Error('Package-manager input is not a regular file');
291
+ }
292
+
293
+ const manifests = new Map();
294
+ for (const entry of inputs) {
295
+ const content = readBlob(repoRoot, entry.hash);
296
+ const manifest = JSON.parse(content.toString('utf8'));
297
+ validateLocalDependencyPaths(manifest);
298
+ manifests.set(entry.path, { content, manifest });
299
+ }
300
+ const rootManifest = manifests.get('package.json')?.manifest;
301
+ if (!rootManifest) throw new Error('Indexed root package.json is invalid');
302
+ validateWorkspacePaths(rootManifest);
303
+ const workspaces = Array.isArray(rootManifest.workspaces)
304
+ ? rootManifest.workspaces
305
+ : rootManifest.workspaces?.packages || [];
306
+ if (entries.some(entry => ['120000', '160000'].includes(entry.mode) && workspaceContainsEntry(workspaces, entry.path))) {
307
+ throw new Error('Workspace graph contains a symlink or submodule');
308
+ }
309
+ return { inputs, manifests, pinnedVersion: parsePinnedBunVersion(rootManifest) };
310
+ }
311
+
312
+ function createProofWorkspace(proofContainer, repoRoot, proofInputs, baseLock) {
313
+ const proofRoot = resolveContainedPath(proofContainer, 'repo');
314
+ fs.mkdirSync(proofRoot);
315
+ for (const entry of proofInputs.inputs) {
316
+ const content = proofInputs.manifests.get(entry.path)?.content || readBlob(repoRoot, entry.hash);
317
+ writeRegularFile(proofRoot, entry.path, content);
318
+ }
319
+ if (baseLock !== null) writeRegularFile(proofRoot, 'bun.lock', baseLock);
320
+ git(proofRoot, ['init', '--quiet']);
321
+
322
+ const env = sanitizedEnvironment(proofContainer);
323
+ fs.mkdirSync(env.HOME, { recursive: true });
324
+ fs.mkdirSync(env.APPDATA, { recursive: true });
325
+ fs.mkdirSync(env.BUN_INSTALL_CACHE_DIR, { recursive: true });
326
+ const emptyConfig = resolveContainedPath(proofContainer, 'empty-bunfig.toml');
327
+ fs.writeFileSync(emptyConfig, '');
328
+ return { proofRoot, env, emptyConfig };
329
+ }
330
+
331
+ function runPinnedBunTransition(spawn, workspace, pinnedVersion) {
332
+ const processOptions = {
333
+ cwd: workspace.proofRoot,
334
+ encoding: 'utf8',
335
+ stdio: ['ignore', 'pipe', 'pipe'],
336
+ env: workspace.env,
337
+ };
338
+ const versionResult = spawn('bun', ['--version'], { ...processOptions, timeout: VERSION_TIMEOUT_MS });
339
+ if (!processSucceeded(versionResult)) throw new Error('Bun version check failed');
340
+ const actualVersion = parseBunVersion(versionResult.stdout);
341
+ if (`${actualVersion.major}.${actualVersion.minor}.${actualVersion.patch}` !== pinnedVersion) {
342
+ throw new Error('Installed Bun does not match root packageManager');
343
+ }
344
+
345
+ const installResult = spawn(
346
+ 'bun',
347
+ ['install', `--cwd=${workspace.proofRoot}`, `--config=${workspace.emptyConfig}`, '--lockfile-only', '--ignore-scripts'],
348
+ { ...processOptions, timeout: INSTALL_TIMEOUT_MS },
349
+ );
350
+ if (!processSucceeded(installResult)) throw new Error('Bun lockfile regeneration failed');
351
+ }
352
+
353
+ function assertTransitionResult(repoRoot, transition, proofRoot) {
354
+ if (captureHead(repoRoot).oid !== transition.head.oid) {
355
+ throw new Error('Committed HEAD changed during Bun lockfile proof');
356
+ }
357
+ if (captureIndexTree(repoRoot) !== transition.indexTree) {
358
+ throw new Error('Git index changed during Bun lockfile proof');
359
+ }
360
+ assertNoDirtyInputs(repoRoot);
361
+
362
+ const generatedPath = resolveContainedPath(proofRoot, 'bun.lock');
363
+ const generatedStat = fs.lstatSync(generatedPath);
364
+ if (!generatedStat.isFile() || generatedStat.isSymbolicLink()) {
365
+ throw new Error('Regenerated bun.lock is not a regular file');
366
+ }
367
+ if (!fs.readFileSync(generatedPath).equals(transition.proposed)) {
368
+ throw new Error('Regenerated bun.lock does not match the staged content');
369
+ }
370
+ }
371
+
372
+ function verifyBunLockfileRegeneration(projectRoot, options = {}) {
373
+ const spawn = options.spawnSync || defaultSpawnSync;
374
+ let proposed = Buffer.alloc(0);
375
+ const proofContainers = [];
376
+
377
+ try {
378
+ const repoRoot = fs.realpathSync(projectRoot);
379
+ assertNoDirtyInputs(repoRoot);
380
+ const transition = readLockTransition(repoRoot);
381
+ proposed = transition.proposed;
382
+ assertLockTransition(transition);
383
+ const proofInputs = readManifestProofInputs(repoRoot, transition.entries);
384
+
385
+ for (const baseLock of [transition.baseLock, null]) {
386
+ const proofContainer = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'forge-bun-lock-proof-')));
387
+ proofContainers.push(proofContainer);
388
+ const workspace = createProofWorkspace(proofContainer, repoRoot, proofInputs, baseLock);
389
+ runPinnedBunTransition(spawn, workspace, proofInputs.pinnedVersion);
390
+ try {
391
+ assertTransitionResult(repoRoot, transition, workspace.proofRoot);
392
+ return allowed(proposed);
393
+ } catch (error) {
394
+ // Only a byte mismatch permits an independent clean dependency refresh proof.
395
+ if (baseLock === null || error.message !== 'Regenerated bun.lock does not match the staged content') throw error;
396
+ }
397
+ }
398
+ } catch (error) {
399
+ const reason = SAFE_FAILURE_REASONS.has(error.message)
400
+ ? error.message
401
+ : 'Bun lockfile proof could not be reconstructed safely';
402
+ return blocked(reason, proposed);
403
+ } finally {
404
+ for (const proofContainer of proofContainers) fs.rmSync(proofContainer, { recursive: true, force: true });
405
+ }
406
+ }
407
+
408
+ module.exports = {
409
+ parseBunVersion,
410
+ parsePinnedBunVersion,
411
+ resolveContainedPath,
412
+ verifyBunLockfileRegeneration,
413
+ };