@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
@@ -2,7 +2,11 @@ import { createHash } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { isAlias, parseDocument, visit } from "yaml";
5
+ import { readCanonicalSourceFile } from "./canonical-source-file.js";
6
+ import { isGeneratedOutputDirectoryName } from "./generated-output-reset.js";
5
7
  export const hostRuntimeContractPath = ".forge/forge.runtime.yml";
8
+ export const hostRuntimeTestPostgresAdminEnvironment = "FORGE_TEST_POSTGRES_ADMIN_CONNECTION_STRING";
9
+ export const hostRuntimeLegacyTestPostgresEnvironment = "FORGE_TEST_POSTGRES_CONNECTION_STRING";
6
10
  const maximumContractBytes = 64 * 1024;
7
11
  const maximumDepth = 12;
8
12
  const ignoredDiscoveryDirectories = new Set([
@@ -21,35 +25,31 @@ const runtimeKeys = new Set(["hostConcurrency", "ports", "playwright", "browserP
21
25
  const portKeys = new Set(["adapter", "launch", "env", "baseUrlEnv", "format"]);
22
26
  const launchKeys = new Set(["command", "args", "workingDirectory", "environment"]);
23
27
  const playwrightKeys = new Set(["config", "baseUrlPort", "workers"]);
24
- export async function loadHostRuntimeContract(worktreePath) {
28
+ export async function loadHostRuntimeContract(worktreePath, snapshot, directorySnapshot) {
29
+ worktreePath = await fs.realpath(worktreePath);
25
30
  const target = path.join(path.resolve(worktreePath), hostRuntimeContractPath);
26
- let targetStat;
31
+ let source;
27
32
  try {
28
- targetStat = await fs.stat(target);
33
+ const captured = snapshot ? await snapshot(target) : await readCanonicalSourceFile(target, maximumContractBytes);
34
+ if (captured === null)
35
+ return await discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot);
36
+ source = captured;
29
37
  }
30
38
  catch (error) {
31
39
  if (isFileNotFound(error))
32
- return await discoverHostRuntimeContract(worktreePath);
40
+ return await discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot);
33
41
  throw error;
34
42
  }
35
- if (!targetStat.isFile() || targetStat.size < 1 || targetStat.size > maximumContractBytes) {
36
- throw new Error(`runtime_contract_invalid: forge.runtime.yml must be a regular 1-${maximumContractBytes} byte file.`);
37
- }
38
- const [realWorktree, realTarget] = await Promise.all([fs.realpath(worktreePath), fs.realpath(target)]);
39
- const relative = path.relative(realWorktree, realTarget);
40
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
41
- throw new Error("runtime_contract_invalid: forge.runtime.yml must remain inside the prepared worktree.");
42
- }
43
43
  return {
44
- contract: validateHostRuntimeContract(await fs.readFile(realTarget, "utf8")),
44
+ contract: validateHostRuntimeContract(source.toString("utf8")),
45
45
  source: "project",
46
46
  };
47
47
  }
48
- async function discoverHostRuntimeContract(worktreePath) {
48
+ async function discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot) {
49
49
  const root = path.resolve(worktreePath);
50
50
  const [nodeDiscovery, aspNetDiscovery] = await Promise.all([
51
- discoverNodeRuntimes(root),
52
- discoverAspNetRuntimes(root),
51
+ discoverNodeRuntimes(root, snapshot, directorySnapshot),
52
+ discoverAspNetRuntimes(root, snapshot, directorySnapshot),
53
53
  ]);
54
54
  if (nodeDiscovery.ambiguous || aspNetDiscovery.ambiguous) {
55
55
  return ambiguousRuntimeDiscovery();
@@ -99,16 +99,16 @@ async function discoverHostRuntimeContract(worktreePath) {
99
99
  },
100
100
  };
101
101
  }
102
- async function discoverNodeRuntimes(root) {
102
+ async function discoverNodeRuntimes(root, snapshot, directorySnapshot) {
103
103
  const manifests = [...new Set([
104
104
  path.join(root, "package.json"),
105
- ...await findFiles(root, file => path.basename(file).toLowerCase() === "package.json", 4, 500),
105
+ ...await findFiles(root, file => path.basename(file).toLowerCase() === "package.json", 4, 500, directorySnapshot),
106
106
  ])];
107
- const candidates = (await Promise.all(manifests.map(async (manifestPath) => await discoverNodeRuntime(root, manifestPath)))).filter((candidate) => candidate !== null);
107
+ const candidates = (await Promise.all(manifests.map(async (manifestPath) => await discoverNodeRuntime(root, manifestPath, snapshot, directorySnapshot)))).filter((candidate) => candidate !== null);
108
108
  return { candidates: candidates.length === 1 ? candidates : [], ambiguous: candidates.length > 1 };
109
109
  }
110
- async function discoverNodeRuntime(root, manifestPath) {
111
- const source = await readBoundedOptionalFile(manifestPath, 256 * 1024);
110
+ async function discoverNodeRuntime(root, manifestPath, snapshot, directorySnapshot) {
111
+ const source = await readBoundedOptionalFile(manifestPath, 256 * 1024, snapshot);
112
112
  if (source === null)
113
113
  return null;
114
114
  let manifest;
@@ -130,15 +130,15 @@ async function discoverNodeRuntime(root, manifestPath) {
130
130
  const next = Object.hasOwn(dependencies, "next") || /(^|\s)next(?:\s|$)/u.test(script);
131
131
  if (vite === next)
132
132
  return null;
133
- const packageManager = await nearestPackageManager(root, path.dirname(manifestPath));
133
+ const packageManager = await nearestPackageManager(root, path.dirname(manifestPath), snapshot);
134
134
  if (!packageManager)
135
135
  return null;
136
136
  const adapter = vite ? "vite" : "next";
137
137
  const playwrightConfigs = [...new Set((await Promise.all([
138
138
  ...["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"]
139
139
  .flatMap(file => [file, `e2e/${file}`, `tests/e2e/${file}`])
140
- .map(async (candidate) => await optionalFileExists(path.join(root, candidate)) ? path.join(root, candidate) : null),
141
- findFiles(root, file => /^playwright\.config\.(?:ts|js|mjs|cjs)$/iu.test(path.basename(file)), 4, 2_000),
140
+ .map(async (candidate) => await optionalFileExists(path.join(root, candidate), snapshot) ? path.join(root, candidate) : null),
141
+ findFiles(root, file => /^playwright\.config\.(?:ts|js|mjs|cjs)$/iu.test(path.basename(file)), 4, 2_000, directorySnapshot),
142
142
  ])).flat().filter((value) => value !== null))]
143
143
  .map(config => portableRelativePath(root, config));
144
144
  const playwright = playwrightConfigs.length === 1
@@ -182,11 +182,11 @@ async function discoverNodeRuntime(root, manifestPath) {
182
182
  }),
183
183
  };
184
184
  }
185
- async function discoverAspNetRuntimes(root) {
186
- const projects = await findFiles(root, file => file.toLowerCase().endsWith(".csproj"), 3, 200);
185
+ async function discoverAspNetRuntimes(root, snapshot, directorySnapshot) {
186
+ const projects = await findFiles(root, file => file.toLowerCase().endsWith(".csproj"), 3, 200, directorySnapshot);
187
187
  const webProjects = [];
188
188
  for (const projectPath of projects) {
189
- const source = await readBoundedOptionalFile(projectPath, 512 * 1024);
189
+ const source = await readBoundedOptionalFile(projectPath, 512 * 1024, snapshot);
190
190
  if (source && /<Project\s+Sdk=["']Microsoft\.NET\.Sdk\.Web["']/iu.test(source)) {
191
191
  webProjects.push({ relativePath: path.relative(root, projectPath).replaceAll("\\", "/"), source });
192
192
  }
@@ -194,7 +194,7 @@ async function discoverAspNetRuntimes(root) {
194
194
  if (webProjects.length !== 1)
195
195
  return { candidates: [], ambiguous: webProjects.length > 1 };
196
196
  const project = webProjects[0];
197
- const launchProfile = await discoverAspNetLaunchProfile(root, project.relativePath);
197
+ const launchProfile = await discoverAspNetLaunchProfile(root, project.relativePath, snapshot);
198
198
  const contract = {
199
199
  schemaVersion: 1,
200
200
  runtime: {
@@ -288,15 +288,15 @@ function ambiguousRuntimeDiscovery() {
288
288
  },
289
289
  };
290
290
  }
291
- async function nearestPackageManager(root, start) {
291
+ async function nearestPackageManager(root, start, snapshot) {
292
292
  let directory = path.resolve(start);
293
293
  const resolvedRoot = path.resolve(root);
294
294
  while (directory === resolvedRoot || directory.startsWith(`${resolvedRoot}${path.sep}`)) {
295
295
  const lockfiles = (await Promise.all([
296
- optionalFileExists(path.join(directory, "pnpm-lock.yaml")).then(exists => exists ? "pnpm" : null),
297
- optionalFileExists(path.join(directory, "yarn.lock")).then(exists => exists ? "yarn" : null),
298
- optionalFileExists(path.join(directory, "package-lock.json")).then(exists => exists ? "npm" : null),
299
- optionalFileExists(path.join(directory, "npm-shrinkwrap.json")).then(exists => exists ? "npm" : null),
296
+ optionalFileExists(path.join(directory, "pnpm-lock.yaml"), snapshot).then(exists => exists ? "pnpm" : null),
297
+ optionalFileExists(path.join(directory, "yarn.lock"), snapshot).then(exists => exists ? "yarn" : null),
298
+ optionalFileExists(path.join(directory, "package-lock.json"), snapshot).then(exists => exists ? "npm" : null),
299
+ optionalFileExists(path.join(directory, "npm-shrinkwrap.json"), snapshot).then(exists => exists ? "npm" : null),
300
300
  ])).filter((value) => value !== null);
301
301
  const packageManagers = [...new Set(lockfiles)];
302
302
  if (packageManagers.length > 1)
@@ -312,9 +312,9 @@ async function nearestPackageManager(root, start) {
312
312
  function portableRelativePath(root, target) {
313
313
  return path.relative(root, target).replaceAll("\\", "/");
314
314
  }
315
- async function discoverAspNetLaunchProfile(root, projectRelativePath) {
315
+ async function discoverAspNetLaunchProfile(root, projectRelativePath, snapshot) {
316
316
  const target = path.join(root, path.dirname(projectRelativePath), "Properties", "launchSettings.json");
317
- const source = await readBoundedOptionalFile(target, 512 * 1024);
317
+ const source = await readBoundedOptionalFile(target, 512 * 1024, snapshot);
318
318
  if (!source)
319
319
  return { name: null, source: null, environment: {} };
320
320
  try {
@@ -354,6 +354,7 @@ function safeAspNetLaunchEnvironment(name, value) {
354
354
  return false;
355
355
  const normalized = name.toUpperCase();
356
356
  return !normalized.startsWith("FORGE_")
357
+ && !isUnsafeDotnetEnvironmentName(name)
357
358
  && !new Set([
358
359
  "ASPNETCORE_URLS",
359
360
  "ASPNETCORE_HTTP_PORTS",
@@ -368,34 +369,47 @@ function safeAspNetLaunchEnvironment(name, value) {
368
369
  "TMPDIR",
369
370
  ]).has(normalized);
370
371
  }
371
- async function findFiles(root, accepts, maximumDepth, maximumEntries) {
372
+ async function findFiles(root, accepts, maximumDepth, maximumEntries, snapshot) {
372
373
  const results = [];
373
374
  let examined = 0;
374
- const visitDirectory = async (directory, depth) => {
375
- if (depth > maximumDepth || examined >= maximumEntries)
376
- return;
377
- const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
378
- for (const entry of entries) {
379
- if (examined++ >= maximumEntries)
380
- return;
381
- if (entry.isDirectory() && ignoredDiscoveryDirectories.has(entry.name.toLowerCase()))
375
+ const directories = [{ directory: root, depth: 0 }];
376
+ while (directories.length > 0 && examined < maximumEntries) {
377
+ const current = directories.shift();
378
+ if (current.depth > maximumDepth)
379
+ continue;
380
+ const entries = snapshot
381
+ ? await snapshot(current.directory)
382
+ : await fs.readdir(current.directory, { withFileTypes: true }).then(found => found.map(entry => ({
383
+ name: entry.name,
384
+ isDirectory: entry.isDirectory(),
385
+ isFile: entry.isFile(),
386
+ }))).catch(() => []);
387
+ const orderedEntries = [...entries].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
388
+ for (const entry of orderedEntries) {
389
+ if (entry.isDirectory
390
+ && (ignoredDiscoveryDirectories.has(entry.name.toLowerCase()) || isGeneratedOutputDirectoryName(entry.name)))
382
391
  continue;
383
- const target = path.join(directory, entry.name);
384
- if (entry.isDirectory())
385
- await visitDirectory(target, depth + 1);
386
- else if (entry.isFile() && accepts(target))
392
+ if (examined++ >= maximumEntries)
393
+ break;
394
+ const target = path.join(current.directory, entry.name);
395
+ if (entry.isDirectory)
396
+ directories.push({ directory: target, depth: current.depth + 1 });
397
+ else if (entry.isFile && accepts(target))
387
398
  results.push(target);
388
399
  }
389
- };
390
- await visitDirectory(root, 0);
400
+ }
391
401
  return results.sort((left, right) => left.localeCompare(right));
392
402
  }
393
- async function readBoundedOptionalFile(target, maximumBytes) {
403
+ async function readBoundedOptionalFile(target, maximumBytes, snapshot) {
404
+ if (snapshot) {
405
+ const source = await snapshot(target);
406
+ return source && source.length > 0 && source.length <= maximumBytes ? source.toString("utf8") : null;
407
+ }
394
408
  try {
395
409
  const stat = await fs.stat(target);
396
410
  if (!stat.isFile() || stat.size < 1 || stat.size > maximumBytes)
397
411
  return null;
398
- return await fs.readFile(target, "utf8");
412
+ return (await readCanonicalSourceFile(target, maximumBytes)).toString("utf8");
399
413
  }
400
414
  catch (error) {
401
415
  if (isFileNotFound(error))
@@ -403,7 +417,9 @@ async function readBoundedOptionalFile(target, maximumBytes) {
403
417
  throw error;
404
418
  }
405
419
  }
406
- async function optionalFileExists(target) {
420
+ async function optionalFileExists(target, snapshot) {
421
+ if (snapshot)
422
+ return await snapshot(target) !== null;
407
423
  try {
408
424
  return (await fs.stat(target)).isFile();
409
425
  }
@@ -574,6 +590,7 @@ function parseLaunch(value, adapter, name) {
574
590
  throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch.args must contain at most 128 strings.`);
575
591
  }
576
592
  const parsedArgs = args.map((argument, index) => boundedString(argument, 4096, `runtime.ports.${name}.launch.args[${index}]`));
593
+ rejectProjectAuthoredForgeArguments(parsedArgs, adapter, name);
577
594
  const portableDirectory = launch.workingDirectory === undefined || launch.workingDirectory === null
578
595
  ? "."
579
596
  : containedPortablePath(launch.workingDirectory, `runtime.ports.${name}.launch.workingDirectory`);
@@ -596,9 +613,46 @@ function parseLaunchEnvironment(value, portName) {
596
613
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name)) {
597
614
  throw new Error(`runtime_contract_invalid: launch environment name '${name}' is invalid.`);
598
615
  }
616
+ if (name.toLowerCase().startsWith("forge_") || isUnsafeDotnetEnvironmentName(name)) {
617
+ throw new Error(`runtime_contract_invalid: launch environment name '${name}' is host-owned or enables pre-startup code injection and cannot be project-authored.`);
618
+ }
599
619
  return [name, boundedString(raw, 8192, `runtime.ports.${portName}.launch.environment.${name}`)];
600
620
  }));
601
621
  }
622
+ function isUnsafeDotnetEnvironmentName(name) {
623
+ const normalized = name.toUpperCase();
624
+ return normalized === "DOTNET_STARTUP_HOOKS"
625
+ || normalized === "DOTNET_ADDITIONAL_DEPS"
626
+ || normalized === "DOTNET_SHARED_STORE"
627
+ || normalized === "DOTNET_HOST_PATH"
628
+ || normalized === "MSBUILD_EXE_PATH"
629
+ || normalized === "MSBUILDSDKSPATH"
630
+ || normalized === "NUGET_PLUGIN_PATHS"
631
+ || normalized === "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES"
632
+ || normalized === "ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES"
633
+ || normalized.startsWith("CUSTOMBEFOREMICROSOFTCOMMON")
634
+ || normalized.startsWith("CUSTOMAFTERMICROSOFTCOMMON")
635
+ || normalized.startsWith("DOTNET_ROOT")
636
+ || normalized.startsWith("CORECLR_")
637
+ || normalized.startsWith("COR_")
638
+ || normalized.startsWith("COMPLUS_")
639
+ || normalized.startsWith("LD_")
640
+ || normalized.startsWith("DYLD_");
641
+ }
642
+ function rejectProjectAuthoredForgeArguments(args, adapter, portName) {
643
+ if (adapter !== "aspnetcore")
644
+ return;
645
+ const applicationArgumentIndex = args.indexOf("--");
646
+ if (applicationArgumentIndex < 0)
647
+ return;
648
+ const forbidden = args.slice(applicationArgumentIndex + 1).find(argument => {
649
+ const key = argument.split("=", 1)[0].replace(/^(?:--?|\/)/u, "");
650
+ return /^forge(?:[_:.-]|$)/iu.test(key);
651
+ });
652
+ if (forbidden) {
653
+ throw new Error(`runtime_contract_invalid: runtime.ports.${portName}.launch.args cannot override the host-owned FORGE_* namespace.`);
654
+ }
655
+ }
602
656
  function containedPortablePath(value, label) {
603
657
  const original = boundedString(value, 1024, label);
604
658
  const portable = original.replaceAll("\\", "/");
@@ -4,8 +4,10 @@ import { type RuntimeIsolationLease } from "./runtime-isolation-lease.js";
4
4
  import { type LoadedHostRuntimeContract } from "./host-runtime-contract.js";
5
5
  import { HostPortBroker } from "./host-port-broker.js";
6
6
  import { NodeHostRuntimeAdapter, type NodeRuntimeAdapterLaunch, type NodeRuntimeCommand, type NodeRuntimeCommandLauncher } from "./node-host-runtime-adapter.js";
7
+ import type { AttemptProcessTreeMemoryTelemetry } from "./attempt-process-supervisor.js";
7
8
  import { AttemptProcessSupervisorRegistry } from "./attempt-process-supervisor-registry.js";
8
9
  import { HostListenerInspector } from "./host-listener-inspector.js";
10
+ import { type MemoryAdmissionDiagnostics } from "./memory-capacity.js";
9
11
  import { PlaywrightHostRuntimeAdapter } from "./playwright-host-runtime-adapter.js";
10
12
  import { HostParallelPromotionStore } from "./host-runtime-promotion.js";
11
13
  import { type HostProcessRecovery } from "../shared/process/host-process-recovery.js";
@@ -15,6 +17,18 @@ export interface HostRuntimeLeaseStart {
15
17
  readonly runtimeContract: LoadedHostRuntimeContract;
16
18
  readonly executionClass: HostRuntimeExecutionClass;
17
19
  }
20
+ export interface HostRuntimeAdmissionCheckpoint {
21
+ readonly code: "host_runtime_serial_admission_waiting" | "host_runtime_prepared_probe_started" | "host_runtime_prepared_probe_progress" | "host_runtime_prepared_probe_completed" | "host_runtime_prepared_admission_waiting" | "host_runtime_prepared_admission_acquired";
22
+ readonly message: string;
23
+ readonly expiresAt: string;
24
+ }
25
+ export type HostRuntimeAdmissionCheckpointHandler = (checkpoint: HostRuntimeAdmissionCheckpoint) => void | Promise<void>;
26
+ export interface HostRuntimeProjectCheckpoint {
27
+ readonly code: "host_runtime_build_started" | "host_runtime_build_cached" | "host_runtime_build_completed";
28
+ readonly message: string;
29
+ readonly data?: Readonly<Record<string, unknown>>;
30
+ }
31
+ export type HostRuntimeProjectCheckpointHandler = (checkpoint: HostRuntimeProjectCheckpoint) => void | Promise<void>;
18
32
  export type HostRuntimeExecutionClass = "analysis_only" | "managed_runtime";
19
33
  export type HostRuntimeSafetyClass = "host_serial_only" | "host_parallel_candidate" | "host_parallel_safe";
20
34
  export interface HostRuntimeProjectCapacitySnapshot {
@@ -29,12 +43,25 @@ export interface HostRuntimeProjectCapacitySnapshot {
29
43
  readonly promotedAt?: string | null;
30
44
  }
31
45
  export interface HostRuntimeCapacitySnapshot {
46
+ readonly preparedWorktreeReadiness?: boolean;
32
47
  readonly activeAttempts: number;
33
48
  readonly reservedAttempts: number;
34
49
  readonly waitingAttempts: number;
35
50
  readonly degradedAttempts: number;
36
51
  readonly exclusiveReservations: number;
37
52
  readonly sharedReservations: number;
53
+ readonly hardMaxAttempts: number;
54
+ readonly effectiveMaxAttempts: number;
55
+ readonly availableAttempts: number;
56
+ readonly totalMemoryBytes: number;
57
+ readonly availableMemoryBytes: number;
58
+ readonly hostReserveBytes: number;
59
+ readonly availableCpuMillis: number;
60
+ readonly availableWorkspaceBytes: number;
61
+ readonly activeReservationMemoryBytes: number;
62
+ readonly memoryAdmission: MemoryAdmissionDiagnostics;
63
+ readonly unavailableCode?: string | null;
64
+ readonly unavailableReason?: string | null;
38
65
  readonly projects: readonly HostRuntimeProjectCapacitySnapshot[];
39
66
  }
40
67
  export interface HostRuntimeReapOptions {
@@ -48,21 +75,41 @@ export interface HostRuntimeReapReport {
48
75
  readonly processesTerminated: number;
49
76
  readonly reusedPidsProtected: number;
50
77
  readonly degraded: number;
78
+ /**
79
+ * Active attempts of a previous runner instance whose every supervised
80
+ * process tree was proved absent, terminated or PID-reused. Forge may settle
81
+ * these exact attempts instead of leaving their launch outcome unknown.
82
+ */
83
+ readonly recoveredAttempts?: readonly HostRuntimeRecoveredAttempt[];
84
+ }
85
+ export interface HostRuntimeRecoveredAttempt {
86
+ readonly dispatchAttemptId: string;
87
+ readonly fencingToken: string;
88
+ readonly runnerInstanceId: string;
51
89
  }
52
90
  export interface HostNodeRuntimeLaunch<T> extends NodeRuntimeAdapterLaunch<T> {
53
91
  readonly runtimeLease: RuntimeIsolationLease;
54
92
  }
55
93
  export interface HostRuntimeLeaseRuntime {
94
+ acquirePreparation?(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
56
95
  acquire(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
57
- start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
58
- startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
96
+ start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal, onAdmissionCheckpoint?: HostRuntimeAdmissionCheckpointHandler): Promise<HostRuntimeLeaseStart>;
97
+ startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal, onProjectCheckpoint?: HostRuntimeProjectCheckpointHandler): Promise<HostRuntimeLeaseStart>;
59
98
  updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
60
99
  providerProcessRunner?(dispatchAttemptId: string, fencingToken: string): ProviderProcessRunner | null;
100
+ providerMemoryTelemetry?(dispatchAttemptId: string, fencingToken: string): AttemptProcessTreeMemoryTelemetry | null;
61
101
  release(dispatchAttemptId: string, fencingToken: string): Promise<RuntimeIsolationLease | null>;
62
- invalidatePromotion?(repoId: string): Promise<void>;
102
+ invalidatePromotion?(repoId: string, scope?: "all" | "discovery"): Promise<void>;
103
+ hasForegroundDemand?(): boolean;
63
104
  reap?(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport>;
64
105
  snapshot?(): HostRuntimeCapacitySnapshot;
65
106
  }
107
+ export interface HostCapacityResourceSample {
108
+ readonly totalMemoryBytes: number;
109
+ readonly availableMemoryBytes: number;
110
+ readonly totalCpuMillis: number;
111
+ readonly availableWorkspaceBytes: number;
112
+ }
66
113
  export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime {
67
114
  private readonly stateRoot;
68
115
  private readonly processSupervisors;
@@ -71,30 +118,57 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
71
118
  private readonly promotionStore;
72
119
  private readonly playwrightAdapter;
73
120
  private readonly processRecovery;
121
+ private readonly capacityResourceSample?;
122
+ private readonly preparedReadiness;
74
123
  private readonly active;
75
124
  private readonly pendingRenewals;
125
+ private readonly acquiringAdmissions;
76
126
  private readonly admissions;
77
127
  private readonly singletonOwners;
78
128
  private readonly singletonWaiters;
79
129
  private readonly parallelProjectOwners;
80
130
  private readonly parallelProjectWaiters;
131
+ private readonly capacityReservations;
132
+ private readonly capacityWaiters;
133
+ private capacityWaiterScanCursor;
134
+ private readonly backgroundAdmissionWaiters;
81
135
  private readonly degraded;
82
136
  private readonly knownPromotions;
137
+ private ledgerWriteTail;
138
+ private workspaceAvailableBytes;
83
139
  private readonly portBroker;
84
140
  private readonly nodeAdapter;
85
- constructor(stateRoot: string, portBroker?: HostPortBroker, nodeAdapter?: NodeHostRuntimeAdapter, processSupervisors?: AttemptProcessSupervisorRegistry, processRunner?: ProviderProcessRunner, listenerInspector?: HostListenerInspector, promotionStore?: HostParallelPromotionStore, playwrightAdapter?: PlaywrightHostRuntimeAdapter, processRecovery?: HostProcessRecovery);
141
+ private readonly capacityConfiguration;
142
+ private readonly memoryAvailability;
143
+ private readonly executionRoot;
144
+ constructor(stateRoot: string, portBroker?: HostPortBroker, nodeAdapter?: NodeHostRuntimeAdapter, processSupervisors?: AttemptProcessSupervisorRegistry, processRunner?: ProviderProcessRunner, listenerInspector?: HostListenerInspector, promotionStore?: HostParallelPromotionStore, playwrightAdapter?: PlaywrightHostRuntimeAdapter, processRecovery?: HostProcessRecovery, capacityResourceSample?: HostCapacityResourceSample | undefined, executionRoot?: string);
86
145
  reap(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport>;
87
146
  snapshot(): HostRuntimeCapacitySnapshot;
147
+ hasForegroundDemand(): boolean;
148
+ acquirePreparation(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
88
149
  acquire(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
150
+ private reserveHostCapacity;
151
+ private reserveBackgroundAdmission;
152
+ private hostCapacityAvailable;
153
+ private hostCapacityImpossibleReason;
154
+ private hostResourceSnapshot;
155
+ private refreshWorkspaceCapacity;
156
+ private releaseHostCapacity;
157
+ private drainCapacityWaiters;
158
+ private capacityWaitersForScan;
159
+ private detachCapacityWaiter;
89
160
  private reserveSingletonKeys;
90
161
  private reserveParallelProject;
91
- start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
162
+ private upgradeParallelProjectToSingleton;
163
+ start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal, onAdmissionCheckpoint?: HostRuntimeAdmissionCheckpointHandler): Promise<HostRuntimeLeaseStart>;
92
164
  updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
93
- startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
165
+ startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal, onProjectCheckpoint?: HostRuntimeProjectCheckpointHandler): Promise<HostRuntimeLeaseStart>;
94
166
  providerProcessRunner(dispatchAttemptId: string, fencingToken: string): ProviderProcessRunner | null;
167
+ private markCapacityReservationRunning;
168
+ providerMemoryTelemetry(dispatchAttemptId: string, fencingToken: string): AttemptProcessTreeMemoryTelemetry | null;
95
169
  private promoteVerifiedRuntime;
96
170
  launchNodeRuntime<T>(dispatchAttemptId: string, fencingToken: string, portName: string, command: NodeRuntimeCommand, launcher: NodeRuntimeCommandLauncher<T>, signal?: AbortSignal): Promise<HostNodeRuntimeLaunch<T>>;
97
- release(dispatchAttemptId: string, fencingToken: string): Promise<RuntimeIsolationLease | null>;
171
+ release(dispatchAttemptId: string, fencingToken: string, preservePreparation?: boolean): Promise<RuntimeIsolationLease | null>;
98
172
  private readLedgers;
99
173
  private recoverPersistedProcesses;
100
174
  private markLedgerDegraded;
@@ -102,6 +176,7 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
102
176
  private forgetDegraded;
103
177
  private attemptRoot;
104
178
  private writeLedger;
179
+ private writeLedgerAtomic;
105
180
  private ledgerPath;
106
181
  private singletonKeysAvailable;
107
182
  private assignSingletonKeys;
@@ -118,9 +193,13 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
118
193
  private cancelAdmissionWaiters;
119
194
  private drainSingletonWaiters;
120
195
  private drainParallelProjectWaiters;
196
+ private drainBackgroundAdmissionWaiters;
121
197
  private refreshKnownPromotions;
122
- invalidatePromotion(repoId: string): Promise<void>;
198
+ invalidatePromotion(repoId: string, scope?: "all" | "discovery"): Promise<void>;
123
199
  private drainAdmissionWaiters;
200
+ private reconcileReservationOwners;
201
+ private reservationOwnerIsLive;
202
+ private hasAttemptRepresentation;
124
203
  }
125
204
  export declare class HostRuntimeLeaseCleanupError extends Error {
126
205
  readonly lease: RuntimeIsolationLease;
@@ -129,5 +208,8 @@ export declare class HostRuntimeLeaseCleanupError extends Error {
129
208
  export declare class HostRuntimeAdmissionExpiredError extends Error {
130
209
  constructor();
131
210
  }
211
+ export declare class HostRuntimeCapacityRequestImpossibleError extends Error {
212
+ constructor(reason: string);
213
+ }
132
214
  export declare function hostRuntimeEventResources(lease: RuntimeIsolationLease, runtimeContract?: LoadedHostRuntimeContract, executionClass?: HostRuntimeExecutionClass): Record<string, unknown>;
133
215
  export declare function hostRuntimeSingletonKeys(dispatch: RunOnceDispatchPacket): readonly string[];