@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.bfa11ebe98c6

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 (160) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1562 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  30. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  31. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  32. package/dist/runtime/attempt-path-operation.js +94 -0
  33. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  34. package/dist/runtime/attempt-private-directory.js +54 -0
  35. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  36. package/dist/runtime/attempt-process-supervisor.js +67 -20
  37. package/dist/runtime/canonical-source-file.d.ts +3 -0
  38. package/dist/runtime/canonical-source-file.js +54 -0
  39. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  40. package/dist/runtime/docker-engine-probe.js +333 -0
  41. package/dist/runtime/generated-output-reset.d.ts +10 -0
  42. package/dist/runtime/generated-output-reset.js +62 -8
  43. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  44. package/dist/runtime/host-database-capability-boundary.js +62 -0
  45. package/dist/runtime/host-diagnostics.d.ts +11 -0
  46. package/dist/runtime/host-diagnostics.js +191 -0
  47. package/dist/runtime/host-execution-root.d.ts +4 -0
  48. package/dist/runtime/host-execution-root.js +39 -0
  49. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  50. package/dist/runtime/host-listener-inspector.js +121 -60
  51. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  52. package/dist/runtime/host-runtime-contract.js +107 -53
  53. package/dist/runtime/host-runtime-lease.d.ts +90 -8
  54. package/dist/runtime/host-runtime-lease.js +1097 -108
  55. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  56. package/dist/runtime/host-runtime-promotion.js +59 -12
  57. package/dist/runtime/memory-capacity.d.ts +70 -0
  58. package/dist/runtime/memory-capacity.js +123 -0
  59. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  60. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  61. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  62. package/dist/runtime/node-host-runtime-adapter.js +28 -11
  63. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  64. package/dist/runtime/pinned-directory-process.js +61 -0
  65. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  66. package/dist/runtime/pinned-source-directory.js +94 -0
  67. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  68. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  69. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  70. package/dist/runtime/prepared-runtime-identity.js +218 -0
  71. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  72. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  73. package/dist/runtime/runtime-isolation-lease.js +4 -1
  74. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  75. package/dist/runtime/workspace-resource-estimator.js +174 -0
  76. package/dist/sandbox/preflight.d.ts +31 -1
  77. package/dist/sandbox/preflight.js +237 -51
  78. package/dist/sandbox/repository-lock.d.ts +2 -1
  79. package/dist/sandbox/repository-lock.js +17 -8
  80. package/dist/sandbox/runtime.d.ts +6 -1
  81. package/dist/sandbox/runtime.js +132 -103
  82. package/dist/sandbox/sandbox.types.d.ts +7 -0
  83. package/dist/sandbox/workspace.service.js +2 -0
  84. package/dist/session/managed-attempt-tools.d.ts +17 -1
  85. package/dist/session/managed-attempt-tools.js +481 -31
  86. package/dist/session/managed-build-coordinator.d.ts +5 -0
  87. package/dist/session/managed-build-coordinator.js +105 -0
  88. package/dist/session/managed-build-receipt.d.ts +21 -0
  89. package/dist/session/managed-build-receipt.js +337 -0
  90. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  91. package/dist/session/managed-verification-timeouts.js +17 -0
  92. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  93. package/dist/session/phase-completion.artifacts.js +224 -22
  94. package/dist/session/phase-completion.d.ts +11 -1
  95. package/dist/session/phase-completion.helpers.d.ts +5 -0
  96. package/dist/session/phase-completion.helpers.js +30 -7
  97. package/dist/session/phase-completion.js +14 -3
  98. package/dist/session/phase-completion.prompts.js +17 -18
  99. package/dist/session/phase-completion.runtime.d.ts +3 -1
  100. package/dist/session/phase-completion.runtime.js +118 -16
  101. package/dist/session/phase-completion.schemas.js +49 -2
  102. package/dist/session/phase-completion.validation.d.ts +1 -0
  103. package/dist/session/phase-completion.validation.js +111 -21
  104. package/dist/session/pre-publication-validation.d.ts +75 -0
  105. package/dist/session/pre-publication-validation.js +657 -0
  106. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  107. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  108. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  109. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  110. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  111. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  112. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  113. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  114. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  115. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  116. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  117. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  118. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  119. package/dist/session/provider-cli/provider-cli.runtime.d.ts +8 -1
  120. package/dist/session/provider-cli/provider-cli.runtime.js +40 -3
  121. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  122. package/dist/session/provider-cli/provider-cli.service.js +771 -68
  123. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  124. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  125. package/dist/session/provider-cli/provider-output-state.js +361 -17
  126. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  127. package/dist/session/provider-cli/provider-usage.js +28 -5
  128. package/dist/session/session-redaction.js +434 -9
  129. package/dist/session/session.service.js +145 -23
  130. package/dist/session/session.types.d.ts +147 -2
  131. package/dist/session/session.types.js +587 -7
  132. package/dist/session/unreferenced-exports.d.ts +12 -0
  133. package/dist/session/unreferenced-exports.js +59 -0
  134. package/dist/shared/format/format-bytes.d.ts +1 -0
  135. package/dist/shared/format/format-bytes.js +9 -0
  136. package/dist/shared/git/git.js +2 -1
  137. package/dist/shared/path/path.d.ts +1 -0
  138. package/dist/shared/path/path.js +18 -0
  139. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  140. package/dist/shared/process/dotnet-toolchain.js +307 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  142. package/dist/shared/process/host-process-supervisor.js +57 -9
  143. package/dist/shared/process/process.d.ts +2 -0
  144. package/dist/shared/process/process.js +12 -7
  145. package/dist/shared/provider-capabilities.d.ts +3 -0
  146. package/dist/shared/provider-capabilities.js +85 -0
  147. package/dist/shared/redaction/redaction.d.ts +1 -0
  148. package/dist/shared/redaction/redaction.js +39 -3
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/dist/worktree/dependency-warmer.d.ts +41 -9
  152. package/dist/worktree/dependency-warmer.js +497 -22
  153. package/dist/worktree/index.d.ts +1 -1
  154. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  155. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  156. package/dist/worktree/worktree.service.d.ts +7 -0
  157. package/dist/worktree/worktree.service.js +253 -21
  158. package/dist/worktree/worktree.types.d.ts +7 -1
  159. package/package.json +7 -5
  160. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -1,10 +1,14 @@
1
+ import { createHash, randomUUID } from "node:crypto";
1
2
  import { existsSync, realpathSync } from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
6
+ import { assertHostDatabaseCapabilityBoundary } from "../runtime/host-database-capability-boundary.js";
7
+ import { RepositoryCoordinatorLock } from "../sandbox/repository-lock.js";
5
8
  import { NodeProcessRunner } from "../shared/process/process.js";
6
9
  import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
7
10
  import { isSubPath } from "../shared/path/path.js";
11
+ import { defaultWorktreeCoordinatorStateRoot } from "./worktree-paths.js";
8
12
  const SKIP_DIR_NAMES = new Set([
9
13
  ".git",
10
14
  "node_modules",
@@ -24,6 +28,11 @@ const NATIVE_MODULE_HINTS = [
24
28
  "@swc/core"
25
29
  ];
26
30
  const MAX_COMMAND_FAILURE_OUTPUT_LENGTH = 8_000;
31
+ const DEPENDENCY_RECEIPT_SCHEMA_VERSION = 1;
32
+ const DEPENDENCY_PREPARATION_CONTRACT = "managed-worktree-isolated-v1";
33
+ const WINDOWS_COMMAND_ENVIRONMENT_PATH_LIMIT = 8_191;
34
+ const WINDOWS_PACKAGE_MANAGER_INTERNAL_PATH_RESERVE = 512;
35
+ const dependencyPreparationCoordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot());
27
36
  export async function detectWarmPaths(repoRoot, warmPathOverrides = []) {
28
37
  const entries = new Map();
29
38
  await collectDetectedPaths(repoRoot, repoRoot, entries, 0);
@@ -51,6 +60,7 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
51
60
  const linked = [];
52
61
  const skipped = [];
53
62
  const fallbacks = [];
63
+ const receipts = [];
54
64
  const restoredDotNetRoots = new Set();
55
65
  const installedNodeRoots = new Set();
56
66
  for (const entry of warmPaths) {
@@ -67,8 +77,10 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
67
77
  continue;
68
78
  }
69
79
  installedNodeRoots.add(packageKey);
70
- const install = await runInstallFallback({ ...entry, packageRoot: installPackageRoot }, worktreePath, "Managed worktrees require isolated Node dependencies.", executor);
71
- fallbacks.push(install);
80
+ const preparation = await ensureInstallFallback({ ...entry, packageRoot: installPackageRoot }, worktreePath, "Managed worktrees require isolated Node dependencies.", executor, request.preparationPolicy);
81
+ receipts.push(preparation.receipt);
82
+ if (preparation.install)
83
+ fallbacks.push(preparation.install);
72
84
  continue;
73
85
  }
74
86
  if (entry.ecosystem === "dotnet") {
@@ -80,15 +92,19 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
80
92
  continue;
81
93
  }
82
94
  restoredDotNetRoots.add(packageKey);
83
- const install = await runInstallFallback(entry, worktreePath, "Managed worktrees require isolated .NET build outputs.", executor);
84
- fallbacks.push(install);
95
+ const preparation = await ensureInstallFallback(entry, worktreePath, "Managed worktrees require isolated .NET build outputs.", executor, request.preparationPolicy);
96
+ receipts.push(preparation.receipt);
97
+ if (preparation.install)
98
+ fallbacks.push(preparation.install);
85
99
  continue;
86
100
  }
87
101
  const sourceExists = await directoryExists(sourcePath);
88
102
  const fallbackReason = await resolveFallbackReason(entry, baseCheckoutPath, worktreePath, sourceExists);
89
103
  if (fallbackReason) {
90
- const install = await runInstallFallback(entry, worktreePath, fallbackReason, executor);
91
- fallbacks.push(install);
104
+ const preparation = await ensureInstallFallback(entry, worktreePath, fallbackReason, executor, request.preparationPolicy);
105
+ receipts.push(preparation.receipt);
106
+ if (preparation.install)
107
+ fallbacks.push(preparation.install);
92
108
  continue;
93
109
  }
94
110
  if (!sourceExists) {
@@ -98,7 +114,7 @@ export async function warmWorktreeDependencies(request, executor = defaultComman
98
114
  await createDirectoryLink(sourcePath, targetPath, allowedRoot);
99
115
  linked.push(entry.relativePath);
100
116
  }
101
- return { linked, skipped, fallbacks };
117
+ return { linked, skipped, fallbacks, receipts };
102
118
  }
103
119
  async function removeLegacyDirectoryLink(targetPath, allowedRoot) {
104
120
  const resolvedTarget = path.resolve(targetPath);
@@ -225,28 +241,249 @@ export async function lockfilesDiffer(baseCheckoutPath, worktreePath, packageRoo
225
241
  }
226
242
  return false;
227
243
  }
244
+ async function ensureInstallFallback(entry, worktreePath, reason, executor, policy) {
245
+ const startedAt = performance.now();
246
+ const cwd = path.resolve(worktreePath, entry.packageRoot === "." ? "" : entry.packageRoot);
247
+ assertWithinRoot(path.resolve(worktreePath), cwd, "Dependency package root");
248
+ const command = await installFallbackCommand(entry, cwd);
249
+ const outputMarker = dependencyOutputMarker(entry.ecosystem, cwd);
250
+ const receiptPath = path.join(outputMarker, ".forge-dependency-preparation.json");
251
+ const fingerprint = await dependencyPreparationFingerprint(entry, worktreePath, cwd, command, policy);
252
+ return await dependencyPreparationCoordinator.withLock(receiptPath, async () => {
253
+ const existing = await readDependencyPreparationReceipt(receiptPath);
254
+ if (existing?.fingerprint === fingerprint
255
+ && existing.ecosystem === entry.ecosystem
256
+ && existing.packageRoot === entry.packageRoot
257
+ && existing.command === command
258
+ && await directoryExists(outputMarker)) {
259
+ await markNpmCiRestored(outputMarker, command);
260
+ return {
261
+ install: null,
262
+ receipt: {
263
+ ecosystem: entry.ecosystem,
264
+ packageRoot: entry.packageRoot,
265
+ command,
266
+ cacheHit: true,
267
+ fingerprint,
268
+ receiptPath,
269
+ durationMs: elapsedMilliseconds(startedAt),
270
+ },
271
+ };
272
+ }
273
+ const install = await runInstallFallback(entry, worktreePath, reason, executor);
274
+ // npm is allowed to omit node_modules when the package graph is empty. Keep
275
+ // an isolated, ignored output directory so the durable receipt still has a
276
+ // concrete marker whose removal invalidates reuse.
277
+ if (entry.ecosystem === "node") {
278
+ await fs.mkdir(outputMarker, { recursive: true });
279
+ }
280
+ if (!(await directoryExists(outputMarker))) {
281
+ throw new Error(`dependency_preparation_output_missing: ${install.command} succeeded without creating ${outputMarker}.`);
282
+ }
283
+ const receipt = {
284
+ schemaVersion: DEPENDENCY_RECEIPT_SCHEMA_VERSION,
285
+ fingerprint,
286
+ ecosystem: entry.ecosystem,
287
+ packageRoot: entry.packageRoot,
288
+ command,
289
+ outputMarker: path.relative(cwd, outputMarker).replaceAll("\\", "/"),
290
+ completedAt: new Date().toISOString(),
291
+ };
292
+ await persistDependencyPreparationReceipt(receiptPath, receipt);
293
+ await markNpmCiRestored(outputMarker, command);
294
+ return {
295
+ install,
296
+ receipt: {
297
+ ecosystem: entry.ecosystem,
298
+ packageRoot: entry.packageRoot,
299
+ command,
300
+ cacheHit: false,
301
+ fingerprint,
302
+ receiptPath,
303
+ durationMs: elapsedMilliseconds(startedAt),
304
+ },
305
+ };
306
+ });
307
+ }
308
+ async function markNpmCiRestored(outputMarker, command) {
309
+ if (command !== "npm ci")
310
+ return;
311
+ // Forge.Api's RestoreForgeBridge target uses this stamp as its incremental
312
+ // output. Worktree preparation has already installed and verified the same
313
+ // package graph, so an immediate dotnet build must not repeat npm ci.
314
+ await fs.writeFile(path.join(outputMarker, ".forge-restore.stamp"), "", { mode: 0o600 });
315
+ }
316
+ function elapsedMilliseconds(startedAt) {
317
+ return Math.max(0, Math.round((performance.now() - startedAt) * 1000) / 1000);
318
+ }
319
+ async function installFallbackCommand(entry, cwd) {
320
+ if (entry.ecosystem === "node") {
321
+ if (await pathExists(path.join(cwd, "pnpm-lock.yaml")))
322
+ return "pnpm install --frozen-lockfile";
323
+ return await pathExists(path.join(cwd, "package-lock.json")) ? "npm ci" : "npm install";
324
+ }
325
+ if (entry.ecosystem === "python") {
326
+ return await pathExists(path.join(cwd, "requirements.txt"))
327
+ ? "python -m venv .venv && pip install -r requirements.txt"
328
+ : "python -m venv .venv";
329
+ }
330
+ return "dotnet restore";
331
+ }
332
+ function dependencyOutputMarker(ecosystem, cwd) {
333
+ if (ecosystem === "node")
334
+ return path.join(cwd, "node_modules");
335
+ if (ecosystem === "python")
336
+ return path.join(cwd, ".venv");
337
+ return path.join(cwd, "obj");
338
+ }
339
+ async function dependencyPreparationFingerprint(entry, worktreePath, cwd, command, policy) {
340
+ const inputFiles = await dependencyInputFiles(entry.ecosystem, worktreePath, cwd);
341
+ const inputDigests = await Promise.all(inputFiles.map(async (file) => ({
342
+ path: path.relative(worktreePath, file).replaceAll("\\", "/"),
343
+ digest: await sha256File(file),
344
+ })));
345
+ const toolchainVersion = await dependencyToolchainVersion(entry.ecosystem, cwd, policy);
346
+ const trustDomain = policy?.trustDomain ?? process.env.FORGE_TRUST_DOMAIN_ID ?? "default";
347
+ const invalidationEpoch = policy?.invalidationEpoch ?? process.env.FORGE_DEPENDENCY_CACHE_EPOCH ?? "default";
348
+ const contract = {
349
+ schemaVersion: DEPENDENCY_RECEIPT_SCHEMA_VERSION,
350
+ preparationContract: DEPENDENCY_PREPARATION_CONTRACT,
351
+ ecosystem: entry.ecosystem,
352
+ packageRoot: entry.packageRoot.replaceAll("\\", "/"),
353
+ command,
354
+ inputDigests,
355
+ toolchainVersion,
356
+ nodeRuntime: process.version,
357
+ platform: process.platform,
358
+ architecture: process.arch,
359
+ osRelease: os.release(),
360
+ isolation: policy?.isolation ?? process.env.FORGE_RUNTIME_MODE ?? "managed_host_process",
361
+ trustDomainFingerprint: sha256Text(trustDomain),
362
+ invalidationEpochFingerprint: sha256Text(invalidationEpoch),
363
+ };
364
+ return `sha256:${sha256Text(JSON.stringify(contract))}`;
365
+ }
366
+ async function dependencyInputFiles(ecosystem, worktreePath, cwd) {
367
+ const result = new Set();
368
+ const directNames = ecosystem === "node"
369
+ ? ["package.json", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "yarn.lock", ".npmrc", ".yarnrc", ".yarnrc.yml", ".nvmrc", ".node-version"]
370
+ : ecosystem === "python"
371
+ ? ["pyproject.toml", "requirements.txt", "requirements-dev.txt", "poetry.lock", "Pipfile", "Pipfile.lock", "uv.lock", "pip.conf", "pyvenv.cfg"]
372
+ : ["global.json", "NuGet.Config", "nuget.config", "Directory.Build.props", "Directory.Build.targets", "Directory.Packages.props", "packages.lock.json"];
373
+ let current = cwd;
374
+ const root = path.resolve(worktreePath);
375
+ while (isSubPath(root, current)) {
376
+ for (const name of directNames) {
377
+ const candidate = path.join(current, name);
378
+ if (await pathExists(candidate))
379
+ result.add(candidate);
380
+ }
381
+ if (ecosystem === "dotnet" && current === cwd) {
382
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
383
+ for (const item of entries) {
384
+ if (item.isFile() && (/\.(?:csproj|fsproj|vbproj|sln|slnx)$/iu.test(item.name))) {
385
+ result.add(path.join(current, item.name));
386
+ }
387
+ }
388
+ }
389
+ if (current === root)
390
+ break;
391
+ current = path.dirname(current);
392
+ }
393
+ return [...result].sort((left, right) => left.localeCompare(right));
394
+ }
395
+ async function dependencyToolchainVersion(ecosystem, cwd, policy) {
396
+ if (ecosystem === "node") {
397
+ const manager = await pathExists(path.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : "npm";
398
+ const override = policy?.toolchainVersions?.[manager];
399
+ if (override)
400
+ return `${manager}@${override};node@${process.version}`;
401
+ const launch = manager === "pnpm" ? resolvePnpmLaunch(["--version"]) : resolveNpmLaunch(["--version"]);
402
+ return `${manager}@${await readToolVersion(cwd, launch.command, launch.args)};node@${process.version}`;
403
+ }
404
+ const override = policy?.toolchainVersions?.[ecosystem];
405
+ if (override)
406
+ return `${ecosystem}@${override}`;
407
+ const launch = ecosystem === "dotnet"
408
+ ? { command: "dotnet", args: ["--version"] }
409
+ : { command: "python", args: ["--version"] };
410
+ return `${ecosystem}@${await readToolVersion(cwd, launch.command, launch.args)}`;
411
+ }
412
+ async function readToolVersion(cwd, command, args) {
413
+ return await defaultProcessRunner.run({ command, cwd, args })
414
+ .then(result => result.exitCode === 0
415
+ ? (result.stdout.trim() || result.stderr.trim() || "unknown").slice(0, 256)
416
+ : `unavailable:${result.exitCode}`)
417
+ .catch(() => "unavailable");
418
+ }
419
+ async function readDependencyPreparationReceipt(receiptPath) {
420
+ try {
421
+ const parsed = JSON.parse(await fs.readFile(receiptPath, "utf8"));
422
+ return parsed.schemaVersion === DEPENDENCY_RECEIPT_SCHEMA_VERSION
423
+ && typeof parsed.fingerprint === "string"
424
+ && typeof parsed.ecosystem === "string"
425
+ && typeof parsed.packageRoot === "string"
426
+ && typeof parsed.command === "string"
427
+ && typeof parsed.outputMarker === "string"
428
+ ? parsed
429
+ : null;
430
+ }
431
+ catch (error) {
432
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
433
+ return null;
434
+ throw error;
435
+ }
436
+ }
437
+ async function persistDependencyPreparationReceipt(receiptPath, receipt) {
438
+ await fs.mkdir(path.dirname(receiptPath), { recursive: true });
439
+ const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
440
+ await fs.writeFile(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
441
+ try {
442
+ await fs.rename(temporaryPath, receiptPath);
443
+ }
444
+ finally {
445
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
446
+ }
447
+ }
448
+ function sha256Text(value) {
449
+ return createHash("sha256").update(value).digest("hex");
450
+ }
228
451
  async function runInstallFallback(entry, worktreePath, reason, executor) {
452
+ assertHostDatabaseCapabilityBoundary();
229
453
  const cwd = path.join(worktreePath, entry.packageRoot === "." ? "" : entry.packageRoot);
230
454
  if (entry.ecosystem === "node") {
231
455
  const pnpmLockPath = path.join(cwd, "pnpm-lock.yaml");
232
456
  if (await pathExists(pnpmLockPath)) {
233
457
  const args = ["install", "--frozen-lockfile"];
234
458
  const launch = resolvePnpmLaunch(args);
235
- await executor.run(cwd, launch.command, launch.args, { env: unattendedPackageManagerEnvironment() });
459
+ await executor.run(cwd, launch.command, launch.args, {
460
+ env: unattendedPackageManagerEnvironment(process.env, {
461
+ packageRoot: cwd,
462
+ requiredPathDirectories: launch.requiredPathDirectories,
463
+ }),
464
+ windowsVerbatimArguments: launch.windowsVerbatimArguments,
465
+ });
236
466
  return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `pnpm ${args.join(" ")}` };
237
467
  }
238
468
  const lockPath = path.join(cwd, "package-lock.json");
239
469
  const args = (await pathExists(lockPath)) ? ["ci"] : ["install"];
240
470
  const launch = resolveNpmLaunch(args);
241
- await executor.run(cwd, launch.command, launch.args, { env: unattendedPackageManagerEnvironment() });
471
+ await executor.run(cwd, launch.command, launch.args, {
472
+ env: unattendedPackageManagerEnvironment(process.env, {
473
+ packageRoot: cwd,
474
+ requiredPathDirectories: launch.requiredPathDirectories,
475
+ }),
476
+ windowsVerbatimArguments: launch.windowsVerbatimArguments,
477
+ });
242
478
  return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `npm ${args.join(" ")}` };
243
479
  }
244
480
  if (entry.ecosystem === "python") {
245
- await executor.run(cwd, "python", ["-m", "venv", ".venv"]);
481
+ const environment = unattendedDependencyEnvironment();
482
+ await executor.run(cwd, "python", ["-m", "venv", ".venv"], { env: environment });
246
483
  const requirements = path.join(cwd, "requirements.txt");
247
484
  if (await pathExists(requirements)) {
248
485
  const pip = process.platform === "win32" ? path.join(".venv", "Scripts", "pip") : path.join(".venv", "bin", "pip");
249
- await executor.run(cwd, pip, ["install", "-r", "requirements.txt"]);
486
+ await executor.run(cwd, pip, ["install", "-r", "requirements.txt"], { env: environment });
250
487
  return {
251
488
  ecosystem: entry.ecosystem,
252
489
  packageRoot: entry.packageRoot,
@@ -261,7 +498,7 @@ async function runInstallFallback(entry, worktreePath, reason, executor) {
261
498
  command: "python -m venv .venv"
262
499
  };
263
500
  }
264
- await executor.run(cwd, "dotnet", ["restore"]);
501
+ await executor.run(cwd, "dotnet", ["restore"], { env: unattendedDependencyEnvironment() });
265
502
  return {
266
503
  ecosystem: entry.ecosystem,
267
504
  packageRoot: entry.packageRoot,
@@ -360,14 +597,37 @@ async function sha256File(filePath) {
360
597
  export function resolveNpmLaunch(args, options = {}) {
361
598
  const env = options.env ?? process.env;
362
599
  const execPath = options.execPath ?? process.execPath;
600
+ const platform = options.platform ?? process.platform;
601
+ const pathExists = options.pathExists ?? existsSync;
363
602
  const npmExecPath = env.npm_execpath;
364
- if (npmExecPath) {
603
+ const npmExecPathIsAbsolute = platform === "win32"
604
+ ? path.win32.isAbsolute(npmExecPath ?? "")
605
+ : path.posix.isAbsolute(npmExecPath ?? "");
606
+ const npmExecPathBaseName = platform === "win32"
607
+ ? path.win32.basename(npmExecPath ?? "")
608
+ : path.posix.basename(npmExecPath ?? "");
609
+ if (npmExecPath
610
+ && npmExecPathIsAbsolute
611
+ && npmExecPathBaseName.toLowerCase() === "npm-cli.js"
612
+ && pathExists(npmExecPath)) {
365
613
  return { command: execPath, args: [npmExecPath, ...args] };
366
614
  }
367
- const platform = options.platform ?? process.platform;
368
615
  if (platform === "win32") {
369
- const comSpec = options.env ? env.ComSpec ?? "cmd.exe" : env.ComSpec;
370
- return buildWindowsCmdLaunch("npm.cmd", args, comSpec);
616
+ const command = resolveWindowsPackageManagerCommand("npm.cmd", env, pathExists, [path.win32.dirname(execPath)]);
617
+ if (!command) {
618
+ throw new Error("Windows npm launcher could not be resolved before PATH compaction.");
619
+ }
620
+ const cli = resolveWindowsPackageManagerCli("npm", command, execPath, pathExists);
621
+ if (cli)
622
+ return { command: execPath, args: [cli, ...args] };
623
+ const comSpec = options.env
624
+ ? windowsEnvironmentValue(env, "ComSpec") ?? "cmd.exe"
625
+ : windowsEnvironmentValue(env, "ComSpec");
626
+ return {
627
+ ...buildWindowsCmdLaunch(command, args, comSpec),
628
+ requiredPathDirectories: [path.win32.dirname(command)],
629
+ windowsVerbatimArguments: true,
630
+ };
371
631
  }
372
632
  for (const candidate of options.candidates ?? defaultNpmCliCandidates(execPath, options.homeDir ?? os.homedir())) {
373
633
  const resolvedCandidate = tryResolveNpmCandidate(candidate);
@@ -377,9 +637,26 @@ export function resolveNpmLaunch(args, options = {}) {
377
637
  }
378
638
  return { command: "npm", args };
379
639
  }
380
- export function resolvePnpmLaunch(args) {
381
- if (process.platform === "win32") {
382
- return buildWindowsCmdLaunch("pnpm.cmd", args, process.env.ComSpec);
640
+ export function resolvePnpmLaunch(args, options = {}) {
641
+ const platform = options.platform ?? process.platform;
642
+ if (platform === "win32") {
643
+ const env = options.env ?? process.env;
644
+ const execPath = options.execPath ?? process.execPath;
645
+ const command = resolveWindowsPackageManagerCommand("pnpm.cmd", env, options.pathExists ?? existsSync, [path.win32.dirname(execPath)]);
646
+ if (!command) {
647
+ throw new Error("Windows pnpm launcher could not be resolved before PATH compaction.");
648
+ }
649
+ const cli = resolveWindowsPackageManagerCli("pnpm", command, execPath, options.pathExists ?? existsSync);
650
+ if (cli)
651
+ return { command: execPath, args: [cli, ...args] };
652
+ const comSpec = options.env
653
+ ? windowsEnvironmentValue(env, "ComSpec") ?? "cmd.exe"
654
+ : windowsEnvironmentValue(env, "ComSpec");
655
+ return {
656
+ ...buildWindowsCmdLaunch(command, args, comSpec),
657
+ requiredPathDirectories: [path.win32.dirname(command)],
658
+ windowsVerbatimArguments: true,
659
+ };
383
660
  }
384
661
  return { command: "pnpm", args };
385
662
  }
@@ -394,8 +671,200 @@ export function formatCommandFailureOutput(stdout, stderr) {
394
671
  }
395
672
  return `[last ${MAX_COMMAND_FAILURE_OUTPUT_LENGTH} characters]\n${output.slice(-MAX_COMMAND_FAILURE_OUTPUT_LENGTH)}`;
396
673
  }
397
- export function unattendedPackageManagerEnvironment(env = process.env) {
398
- return { ...env, CI: "true" };
674
+ export function unattendedPackageManagerEnvironment(env = process.env, options = {}) {
675
+ const result = { ...unattendedDependencyEnvironment(env), CI: "true" };
676
+ if ((options.platform ?? process.platform) !== "win32")
677
+ return result;
678
+ const pathValue = windowsEnvironmentValues(env, "Path").join(";");
679
+ for (const key of Object.keys(result)) {
680
+ if (key.toLowerCase() === "path")
681
+ delete result[key];
682
+ }
683
+ result.PATH = compactWindowsPackageManagerPath(pathValue, env, options);
684
+ return result;
685
+ }
686
+ const dependencyEnvironmentAllowlist = new Set([
687
+ "appdata",
688
+ "comspec",
689
+ "corepack_home",
690
+ "dotnet_cli_home",
691
+ "dotnet_root",
692
+ "home",
693
+ "homedrive",
694
+ "homepath",
695
+ "http_proxy",
696
+ "https_proxy",
697
+ "localappdata",
698
+ "no_proxy",
699
+ "npm_config_cache",
700
+ "nuget_http_cache_path",
701
+ "nuget_packages",
702
+ "path",
703
+ "pathext",
704
+ "programdata",
705
+ "programfiles",
706
+ "programfiles(x86)",
707
+ "programw6432",
708
+ "ssl_cert_dir",
709
+ "ssl_cert_file",
710
+ "systemdrive",
711
+ "systemroot",
712
+ "temp",
713
+ "tmp",
714
+ "tmpdir",
715
+ "userdomain",
716
+ "username",
717
+ "userprofile",
718
+ "windir",
719
+ ]);
720
+ export function unattendedDependencyEnvironment(env = process.env) {
721
+ return Object.fromEntries(Object.entries(env).filter(([name, value]) => typeof value === "string"
722
+ && (dependencyEnvironmentAllowlist.has(name.toLowerCase()) || name.toUpperCase().startsWith("LC_"))));
723
+ }
724
+ export function compactWindowsPackageManagerPath(pathValue, env = process.env, options = {}) {
725
+ const pathExists = options.pathExists ?? existsSync;
726
+ const maximumPathLength = options.maximumPathLength
727
+ ?? windowsPackageManagerPathBudget(options.packageRoot ?? process.cwd());
728
+ const execPath = options.execPath ?? process.execPath;
729
+ if (!Number.isSafeInteger(maximumPathLength) || maximumPathLength < 1) {
730
+ throw new Error(`Windows package-manager PATH budget is exhausted before launch (${maximumPathLength}).`);
731
+ }
732
+ const normalize = windowsPathNormalizer(env);
733
+ const inherited = [];
734
+ const seen = new Set();
735
+ for (const candidate of pathValue.split(";").map(normalize)) {
736
+ if (candidate.length === 0 || !path.win32.isAbsolute(candidate) || !pathExists(candidate))
737
+ continue;
738
+ const key = candidate.toLowerCase();
739
+ if (seen.has(key))
740
+ continue;
741
+ inherited.push(candidate);
742
+ seen.add(key);
743
+ }
744
+ const nodeDirectory = normalize(path.win32.dirname(execPath));
745
+ if (!nodeDirectory || !path.win32.isAbsolute(nodeDirectory) || !pathExists(nodeDirectory)) {
746
+ throw new Error(`Windows Node runtime directory is unavailable: ${nodeDirectory || "<empty>"}.`);
747
+ }
748
+ const systemRoot = normalize(windowsEnvironmentValue(env, "SystemRoot") ?? "");
749
+ const requiredCandidates = [
750
+ nodeDirectory,
751
+ normalize(path.win32.dirname(windowsEnvironmentValue(env, "ComSpec") ?? "")),
752
+ systemRoot ? path.win32.join(systemRoot, "System32") : "",
753
+ systemRoot,
754
+ ].filter(candidate => candidate.length > 0
755
+ && path.win32.isAbsolute(candidate)
756
+ && pathExists(candidate));
757
+ const explicitRequired = (options.requiredPathDirectories ?? []).map(normalize);
758
+ for (const candidate of explicitRequired) {
759
+ if (!candidate || !path.win32.isAbsolute(candidate) || !pathExists(candidate)) {
760
+ throw new Error(`Windows package-manager required PATH directory is unavailable: ${candidate || "<empty>"}.`);
761
+ }
762
+ requiredCandidates.push(candidate);
763
+ }
764
+ const required = new Map();
765
+ for (const candidate of requiredCandidates)
766
+ required.set(candidate.toLowerCase(), candidate);
767
+ const ordered = [...inherited];
768
+ for (const candidate of required.values()) {
769
+ if (!seen.has(candidate.toLowerCase()))
770
+ ordered.push(candidate);
771
+ }
772
+ const requiredLength = joinedWindowsPathLength([...required.values()]);
773
+ if (requiredLength > maximumPathLength) {
774
+ throw new Error(`Windows package-manager PATH cannot retain required runtime directories within ${maximumPathLength} characters.`);
775
+ }
776
+ const selected = new Set(required.keys());
777
+ let selectedLength = requiredLength;
778
+ for (const candidate of inherited) {
779
+ const key = candidate.toLowerCase();
780
+ if (selected.has(key))
781
+ continue;
782
+ const nextLength = selectedLength + (selected.size === 0 ? 0 : 1) + candidate.length;
783
+ if (nextLength > maximumPathLength)
784
+ continue;
785
+ selected.add(key);
786
+ selectedLength = nextLength;
787
+ }
788
+ return ordered.filter(candidate => selected.has(candidate.toLowerCase())).join(";");
789
+ }
790
+ export function windowsPackageManagerLifecyclePathPrefix(packageRoot) {
791
+ const entries = [];
792
+ let current = path.win32.resolve(packageRoot);
793
+ while (true) {
794
+ entries.push(path.win32.join(current, "node_modules", ".bin"));
795
+ const parent = path.win32.dirname(current);
796
+ if (parent === current)
797
+ break;
798
+ current = parent;
799
+ }
800
+ return entries.join(";");
801
+ }
802
+ export function windowsPackageManagerPathBudget(packageRoot) {
803
+ const lifecyclePrefix = windowsPackageManagerLifecyclePathPrefix(packageRoot);
804
+ return WINDOWS_COMMAND_ENVIRONMENT_PATH_LIMIT
805
+ - lifecyclePrefix.length
806
+ - 1
807
+ - WINDOWS_PACKAGE_MANAGER_INTERNAL_PATH_RESERVE;
808
+ }
809
+ function resolveWindowsPackageManagerCommand(commandName, env, pathExists, fallbackDirectories) {
810
+ const normalize = windowsPathNormalizer(env);
811
+ const directories = [
812
+ ...windowsEnvironmentValues(env, "Path").flatMap(value => value.split(";")),
813
+ ...fallbackDirectories,
814
+ ].map(normalize);
815
+ const seen = new Set();
816
+ for (const directory of directories) {
817
+ if (!directory || !path.win32.isAbsolute(directory))
818
+ continue;
819
+ const key = directory.toLowerCase();
820
+ if (seen.has(key))
821
+ continue;
822
+ seen.add(key);
823
+ const candidate = path.win32.join(directory, commandName);
824
+ if (pathExists(candidate))
825
+ return candidate;
826
+ }
827
+ return null;
828
+ }
829
+ function resolveWindowsPackageManagerCli(manager, commandPath, execPath, pathExists) {
830
+ const commandDirectory = path.win32.dirname(commandPath);
831
+ const execDirectory = path.win32.dirname(execPath);
832
+ const candidates = manager === "npm"
833
+ ? [
834
+ path.win32.join(commandDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
835
+ path.win32.join(execDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
836
+ path.win32.join(execDirectory, "node_modules", "corepack", "dist", "npm.js"),
837
+ ]
838
+ : [
839
+ path.win32.join(commandDirectory, "node_modules", "pnpm", "bin", "pnpm.mjs"),
840
+ path.win32.join(execDirectory, "node_modules", "pnpm", "bin", "pnpm.mjs"),
841
+ path.win32.join(execDirectory, "node_modules", "corepack", "dist", "pnpm.js"),
842
+ ];
843
+ return candidates.find(pathExists) ?? null;
844
+ }
845
+ function windowsPathNormalizer(env) {
846
+ const environment = new Map(Object.entries(env).map(([key, value]) => [key.toLowerCase(), value ?? ""]));
847
+ return (value) => {
848
+ const trimmed = value.trim();
849
+ const unquoted = trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')
850
+ ? trimmed.slice(1, -1).trim()
851
+ : trimmed;
852
+ return unquoted.replace(/%([^%]+)%/gu, (match, name) => environment.get(name.toLowerCase()) || match);
853
+ };
854
+ }
855
+ function windowsEnvironmentValues(env, name) {
856
+ const normalized = name.toLowerCase();
857
+ return Object.entries(env)
858
+ .filter(([key, value]) => key.toLowerCase() === normalized && Boolean(value))
859
+ .map(([, value]) => value);
860
+ }
861
+ function windowsEnvironmentValue(env, name) {
862
+ return windowsEnvironmentValues(env, name)[0];
863
+ }
864
+ function joinedWindowsPathLength(entries) {
865
+ return entries.length === 0
866
+ ? 0
867
+ : entries.reduce((total, entry) => total + entry.length, entries.length - 1);
399
868
  }
400
869
  function tryResolveNpmCandidate(candidate) {
401
870
  try {
@@ -419,7 +888,13 @@ function defaultNpmCliCandidates(execPath, homeDir) {
419
888
  }
420
889
  const defaultCommandExecutor = {
421
890
  async run(cwd, command, args, options) {
422
- const result = await defaultProcessRunner.run({ command, cwd, args, env: options?.env });
891
+ const result = await defaultProcessRunner.run({
892
+ command,
893
+ cwd,
894
+ args,
895
+ env: options?.env,
896
+ windowsVerbatimArguments: options?.windowsVerbatimArguments,
897
+ });
423
898
  if (result.exitCode !== 0) {
424
899
  const output = formatCommandFailureOutput(result.stdout, result.stderr);
425
900
  const details = output.length > 0 ? `\n${output}` : " no output was captured";
@@ -1,5 +1,5 @@
1
1
  export { GitWorktreeService } from "./worktree.service.js";
2
2
  export { managedRepoRoot, resolveManagedWorktreeRoot, resolveManagedWorktreeRootForRepo, } from "./worktree-paths.js";
3
3
  export { detectWarmPaths, warmWorktreeDependencies, createDirectoryLink, lockfilesDiffer, requiresInstallFallback } from "./dependency-warmer.js";
4
- export type { WarmDependenciesResult, WarmPathEntry, InstallFallbackResult } from "./dependency-warmer.js";
4
+ export type { WarmDependenciesResult, WarmPathEntry, InstallFallbackResult, DependencyPreparationPolicy, DependencyPreparationReceiptResult, } from "./dependency-warmer.js";
5
5
  export type { WorktreeService } from "./worktree.base.js";
@@ -0,0 +1,4 @@
1
+ import { GitClient } from "../shared/git/git.js";
2
+ import type { WorktreePrepareRequest } from "./worktree.types.js";
3
+ export declare function storyQaReferenceRoot(storyId: string, runId: string): string;
4
+ export declare function materializeStoryQaReferences(worktreePath: string, request: WorktreePrepareRequest, git?: GitClient): Promise<void>;