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

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 (162) 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 +1563 -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-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +67 -20
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +191 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +28 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +17 -8
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +132 -103
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +485 -31
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +105 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +8 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +43 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +771 -68
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  144. package/dist/shared/process/host-process-supervisor.js +57 -9
  145. package/dist/shared/process/process.d.ts +2 -0
  146. package/dist/shared/process/process.js +12 -7
  147. package/dist/shared/provider-capabilities.d.ts +3 -0
  148. package/dist/shared/provider-capabilities.js +85 -0
  149. package/dist/shared/redaction/redaction.d.ts +1 -0
  150. package/dist/shared/redaction/redaction.js +39 -3
  151. package/dist/version.d.ts +1 -1
  152. package/dist/version.js +1 -1
  153. package/dist/worktree/dependency-warmer.d.ts +41 -9
  154. package/dist/worktree/dependency-warmer.js +497 -22
  155. package/dist/worktree/index.d.ts +1 -1
  156. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  157. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  158. package/dist/worktree/worktree.service.d.ts +7 -0
  159. package/dist/worktree/worktree.service.js +253 -21
  160. package/dist/worktree/worktree.types.d.ts +7 -1
  161. package/package.json +7 -5
  162. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -2,55 +2,89 @@ 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;
12
+ // Every discovery scan stops after this many directory entries. A monorepo needs a few
13
+ // thousand; the bound only keeps a pathological tree from stalling attempt preparation.
14
+ const maximumDiscoveryEntries = 20_000;
15
+ /**
16
+ * A discovery scan reached its entry budget before seeing the whole tree, so what it did not
17
+ * find may still exist. Discovery must not treat that as absence: a partial runtime (for
18
+ * example a web app without its API) would send the attempt's traffic to the wrong server.
19
+ */
20
+ class IncompleteRuntimeDiscoveryError extends Error {
21
+ }
22
+ // Dependency, build and editor folders never hold an application entrypoint, and counting
23
+ // them would exhaust the budget and serialize a repository for no reason.
8
24
  const ignoredDiscoveryDirectories = new Set([
9
25
  ".forge",
10
26
  ".forge-designer",
11
27
  ".git",
28
+ ".gradle",
29
+ ".idea",
12
30
  ".local",
31
+ ".next",
32
+ ".pnpm",
33
+ ".turbo",
34
+ ".venv",
35
+ ".vs",
36
+ ".vscode",
37
+ ".yarn",
13
38
  "bin",
39
+ "build",
14
40
  "coverage",
15
41
  "dist",
16
42
  "node_modules",
17
43
  "obj",
44
+ "target",
45
+ "vendor",
46
+ "venv",
18
47
  ]);
19
48
  const topLevelKeys = new Set(["schemaVersion", "runtime"]);
20
49
  const runtimeKeys = new Set(["hostConcurrency", "ports", "playwright", "browserProfile", "singletonResources"]);
21
50
  const portKeys = new Set(["adapter", "launch", "env", "baseUrlEnv", "format"]);
22
51
  const launchKeys = new Set(["command", "args", "workingDirectory", "environment"]);
23
52
  const playwrightKeys = new Set(["config", "baseUrlPort", "workers"]);
24
- export async function loadHostRuntimeContract(worktreePath) {
53
+ export async function loadHostRuntimeContract(worktreePath, snapshot, directorySnapshot) {
54
+ worktreePath = await fs.realpath(worktreePath);
25
55
  const target = path.join(path.resolve(worktreePath), hostRuntimeContractPath);
26
- let targetStat;
56
+ let source;
27
57
  try {
28
- targetStat = await fs.stat(target);
58
+ const captured = snapshot ? await snapshot(target) : await readCanonicalSourceFile(target, maximumContractBytes);
59
+ if (captured === null)
60
+ return await discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot);
61
+ source = captured;
29
62
  }
30
63
  catch (error) {
31
64
  if (isFileNotFound(error))
32
- return await discoverHostRuntimeContract(worktreePath);
65
+ return await discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot);
33
66
  throw error;
34
67
  }
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
68
  return {
44
- contract: validateHostRuntimeContract(await fs.readFile(realTarget, "utf8")),
69
+ contract: validateHostRuntimeContract(source.toString("utf8")),
45
70
  source: "project",
46
71
  };
47
72
  }
48
- async function discoverHostRuntimeContract(worktreePath) {
73
+ async function discoverHostRuntimeContract(worktreePath, snapshot, directorySnapshot) {
49
74
  const root = path.resolve(worktreePath);
50
- const [nodeDiscovery, aspNetDiscovery] = await Promise.all([
51
- discoverNodeRuntimes(root),
52
- discoverAspNetRuntimes(root),
53
- ]);
75
+ let nodeDiscovery;
76
+ let aspNetDiscovery;
77
+ try {
78
+ [nodeDiscovery, aspNetDiscovery] = await Promise.all([
79
+ discoverNodeRuntimes(root, snapshot, directorySnapshot),
80
+ discoverAspNetRuntimes(root, snapshot, directorySnapshot),
81
+ ]);
82
+ }
83
+ catch (error) {
84
+ if (error instanceof IncompleteRuntimeDiscoveryError)
85
+ return incompleteRuntimeDiscovery();
86
+ throw error;
87
+ }
54
88
  if (nodeDiscovery.ambiguous || aspNetDiscovery.ambiguous) {
55
89
  return ambiguousRuntimeDiscovery();
56
90
  }
@@ -99,16 +133,16 @@ async function discoverHostRuntimeContract(worktreePath) {
99
133
  },
100
134
  };
101
135
  }
102
- async function discoverNodeRuntimes(root) {
136
+ async function discoverNodeRuntimes(root, snapshot, directorySnapshot) {
103
137
  const manifests = [...new Set([
104
138
  path.join(root, "package.json"),
105
- ...await findFiles(root, file => path.basename(file).toLowerCase() === "package.json", 4, 500),
139
+ ...await findFiles(root, file => path.basename(file).toLowerCase() === "package.json", 4, maximumDiscoveryEntries, directorySnapshot),
106
140
  ])];
107
- const candidates = (await Promise.all(manifests.map(async (manifestPath) => await discoverNodeRuntime(root, manifestPath)))).filter((candidate) => candidate !== null);
141
+ const candidates = (await Promise.all(manifests.map(async (manifestPath) => await discoverNodeRuntime(root, manifestPath, snapshot, directorySnapshot)))).filter((candidate) => candidate !== null);
108
142
  return { candidates: candidates.length === 1 ? candidates : [], ambiguous: candidates.length > 1 };
109
143
  }
110
- async function discoverNodeRuntime(root, manifestPath) {
111
- const source = await readBoundedOptionalFile(manifestPath, 256 * 1024);
144
+ async function discoverNodeRuntime(root, manifestPath, snapshot, directorySnapshot) {
145
+ const source = await readBoundedOptionalFile(manifestPath, 256 * 1024, snapshot);
112
146
  if (source === null)
113
147
  return null;
114
148
  let manifest;
@@ -130,15 +164,15 @@ async function discoverNodeRuntime(root, manifestPath) {
130
164
  const next = Object.hasOwn(dependencies, "next") || /(^|\s)next(?:\s|$)/u.test(script);
131
165
  if (vite === next)
132
166
  return null;
133
- const packageManager = await nearestPackageManager(root, path.dirname(manifestPath));
167
+ const packageManager = await nearestPackageManager(root, path.dirname(manifestPath), snapshot);
134
168
  if (!packageManager)
135
169
  return null;
136
170
  const adapter = vite ? "vite" : "next";
137
171
  const playwrightConfigs = [...new Set((await Promise.all([
138
172
  ...["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"]
139
173
  .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),
174
+ .map(async (candidate) => await optionalFileExists(path.join(root, candidate), snapshot) ? path.join(root, candidate) : null),
175
+ findFiles(root, file => /^playwright\.config\.(?:ts|js|mjs|cjs)$/iu.test(path.basename(file)), 4, maximumDiscoveryEntries, directorySnapshot),
142
176
  ])).flat().filter((value) => value !== null))]
143
177
  .map(config => portableRelativePath(root, config));
144
178
  const playwright = playwrightConfigs.length === 1
@@ -182,11 +216,13 @@ async function discoverNodeRuntime(root, manifestPath) {
182
216
  }),
183
217
  };
184
218
  }
185
- async function discoverAspNetRuntimes(root) {
186
- const projects = await findFiles(root, file => file.toLowerCase().endsWith(".csproj"), 3, 200);
219
+ async function discoverAspNetRuntimes(root, snapshot, directorySnapshot) {
220
+ // Deep enough for nested solutions (src/<area>/<Project>/<Project>.csproj); the entry budget,
221
+ // not the depth, bounds the scan, and a scan that exceeds it is reported as incomplete.
222
+ const projects = await findFiles(root, file => file.toLowerCase().endsWith(".csproj"), 6, maximumDiscoveryEntries, directorySnapshot);
187
223
  const webProjects = [];
188
224
  for (const projectPath of projects) {
189
- const source = await readBoundedOptionalFile(projectPath, 512 * 1024);
225
+ const source = await readBoundedOptionalFile(projectPath, 512 * 1024, snapshot);
190
226
  if (source && /<Project\s+Sdk=["']Microsoft\.NET\.Sdk\.Web["']/iu.test(source)) {
191
227
  webProjects.push({ relativePath: path.relative(root, projectPath).replaceAll("\\", "/"), source });
192
228
  }
@@ -194,7 +230,7 @@ async function discoverAspNetRuntimes(root) {
194
230
  if (webProjects.length !== 1)
195
231
  return { candidates: [], ambiguous: webProjects.length > 1 };
196
232
  const project = webProjects[0];
197
- const launchProfile = await discoverAspNetLaunchProfile(root, project.relativePath);
233
+ const launchProfile = await discoverAspNetLaunchProfile(root, project.relativePath, snapshot);
198
234
  const contract = {
199
235
  schemaVersion: 1,
200
236
  runtime: {
@@ -277,6 +313,17 @@ function combineBrowserAndApiRuntime(browser, api) {
277
313
  }),
278
314
  };
279
315
  }
316
+ function incompleteRuntimeDiscovery() {
317
+ return {
318
+ contract: defaultHostRuntimeContract(),
319
+ source: "default",
320
+ discovery: {
321
+ status: "ambiguous",
322
+ reasonCode: "runtime_discovery_incomplete",
323
+ summary: "Forge could not scan the whole repository within its discovery budget, so host application runs remain serialized.",
324
+ },
325
+ };
326
+ }
280
327
  function ambiguousRuntimeDiscovery() {
281
328
  return {
282
329
  contract: defaultHostRuntimeContract(),
@@ -288,15 +335,15 @@ function ambiguousRuntimeDiscovery() {
288
335
  },
289
336
  };
290
337
  }
291
- async function nearestPackageManager(root, start) {
338
+ async function nearestPackageManager(root, start, snapshot) {
292
339
  let directory = path.resolve(start);
293
340
  const resolvedRoot = path.resolve(root);
294
341
  while (directory === resolvedRoot || directory.startsWith(`${resolvedRoot}${path.sep}`)) {
295
342
  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),
343
+ optionalFileExists(path.join(directory, "pnpm-lock.yaml"), snapshot).then(exists => exists ? "pnpm" : null),
344
+ optionalFileExists(path.join(directory, "yarn.lock"), snapshot).then(exists => exists ? "yarn" : null),
345
+ optionalFileExists(path.join(directory, "package-lock.json"), snapshot).then(exists => exists ? "npm" : null),
346
+ optionalFileExists(path.join(directory, "npm-shrinkwrap.json"), snapshot).then(exists => exists ? "npm" : null),
300
347
  ])).filter((value) => value !== null);
301
348
  const packageManagers = [...new Set(lockfiles)];
302
349
  if (packageManagers.length > 1)
@@ -312,9 +359,9 @@ async function nearestPackageManager(root, start) {
312
359
  function portableRelativePath(root, target) {
313
360
  return path.relative(root, target).replaceAll("\\", "/");
314
361
  }
315
- async function discoverAspNetLaunchProfile(root, projectRelativePath) {
362
+ async function discoverAspNetLaunchProfile(root, projectRelativePath, snapshot) {
316
363
  const target = path.join(root, path.dirname(projectRelativePath), "Properties", "launchSettings.json");
317
- const source = await readBoundedOptionalFile(target, 512 * 1024);
364
+ const source = await readBoundedOptionalFile(target, 512 * 1024, snapshot);
318
365
  if (!source)
319
366
  return { name: null, source: null, environment: {} };
320
367
  try {
@@ -339,7 +386,13 @@ async function discoverAspNetLaunchProfile(root, projectRelativePath) {
339
386
  return { name: null, source, environment: {} };
340
387
  }
341
388
  }
389
+ // A project's "forge-attempt" profile configures its API for managed attempts. The
390
+ // attempt's pre-publication checks then run against the same settings the project's own
391
+ // e2e suite uses, instead of the developer's "http" profile.
392
+ const attemptLaunchProfileName = "forge-attempt";
342
393
  function aspNetProfilePriority(name) {
394
+ if (name.toLowerCase() === attemptLaunchProfileName)
395
+ return -1;
343
396
  if (name.toLowerCase() === "http")
344
397
  return 0;
345
398
  if (name.toLowerCase() === "https")
@@ -354,6 +407,7 @@ function safeAspNetLaunchEnvironment(name, value) {
354
407
  return false;
355
408
  const normalized = name.toUpperCase();
356
409
  return !normalized.startsWith("FORGE_")
410
+ && !isUnsafeDotnetEnvironmentName(name)
357
411
  && !new Set([
358
412
  "ASPNETCORE_URLS",
359
413
  "ASPNETCORE_HTTP_PORTS",
@@ -368,34 +422,53 @@ function safeAspNetLaunchEnvironment(name, value) {
368
422
  "TMPDIR",
369
423
  ]).has(normalized);
370
424
  }
371
- async function findFiles(root, accepts, maximumDepth, maximumEntries) {
425
+ async function findFiles(root, accepts, maximumDepth, maximumEntries, snapshot) {
372
426
  const results = [];
373
427
  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()))
428
+ const directories = [{ directory: root, depth: 0 }];
429
+ while (directories.length > 0) {
430
+ const current = directories.shift();
431
+ if (current.depth > maximumDepth)
432
+ continue;
433
+ const entries = snapshot
434
+ ? await snapshot(current.directory)
435
+ : await fs.readdir(current.directory, { withFileTypes: true }).then(found => found.map(entry => ({
436
+ name: entry.name,
437
+ isDirectory: entry.isDirectory(),
438
+ isFile: entry.isFile(),
439
+ }))).catch((error) => {
440
+ // A directory removed since it was listed holds nothing; one that cannot be read
441
+ // (permissions, open-file limits) may hold the entrypoint discovery is looking for.
442
+ if (isFileNotFound(error))
443
+ return [];
444
+ throw new IncompleteRuntimeDiscoveryError();
445
+ });
446
+ const orderedEntries = [...entries].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
447
+ for (const entry of orderedEntries) {
448
+ if (entry.isDirectory
449
+ && (ignoredDiscoveryDirectories.has(entry.name.toLowerCase()) || isGeneratedOutputDirectoryName(entry.name)))
382
450
  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))
451
+ if (examined++ >= maximumEntries)
452
+ throw new IncompleteRuntimeDiscoveryError();
453
+ const target = path.join(current.directory, entry.name);
454
+ if (entry.isDirectory)
455
+ directories.push({ directory: target, depth: current.depth + 1 });
456
+ else if (entry.isFile && accepts(target))
387
457
  results.push(target);
388
458
  }
389
- };
390
- await visitDirectory(root, 0);
459
+ }
391
460
  return results.sort((left, right) => left.localeCompare(right));
392
461
  }
393
- async function readBoundedOptionalFile(target, maximumBytes) {
462
+ async function readBoundedOptionalFile(target, maximumBytes, snapshot) {
463
+ if (snapshot) {
464
+ const source = await snapshot(target);
465
+ return source && source.length > 0 && source.length <= maximumBytes ? source.toString("utf8") : null;
466
+ }
394
467
  try {
395
468
  const stat = await fs.stat(target);
396
469
  if (!stat.isFile() || stat.size < 1 || stat.size > maximumBytes)
397
470
  return null;
398
- return await fs.readFile(target, "utf8");
471
+ return (await readCanonicalSourceFile(target, maximumBytes)).toString("utf8");
399
472
  }
400
473
  catch (error) {
401
474
  if (isFileNotFound(error))
@@ -403,7 +476,9 @@ async function readBoundedOptionalFile(target, maximumBytes) {
403
476
  throw error;
404
477
  }
405
478
  }
406
- async function optionalFileExists(target) {
479
+ async function optionalFileExists(target, snapshot) {
480
+ if (snapshot)
481
+ return await snapshot(target) !== null;
407
482
  try {
408
483
  return (await fs.stat(target)).isFile();
409
484
  }
@@ -574,6 +649,7 @@ function parseLaunch(value, adapter, name) {
574
649
  throw new Error(`runtime_contract_invalid: runtime.ports.${name}.launch.args must contain at most 128 strings.`);
575
650
  }
576
651
  const parsedArgs = args.map((argument, index) => boundedString(argument, 4096, `runtime.ports.${name}.launch.args[${index}]`));
652
+ rejectProjectAuthoredForgeArguments(parsedArgs, adapter, name);
577
653
  const portableDirectory = launch.workingDirectory === undefined || launch.workingDirectory === null
578
654
  ? "."
579
655
  : containedPortablePath(launch.workingDirectory, `runtime.ports.${name}.launch.workingDirectory`);
@@ -596,9 +672,46 @@ function parseLaunchEnvironment(value, portName) {
596
672
  if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(name)) {
597
673
  throw new Error(`runtime_contract_invalid: launch environment name '${name}' is invalid.`);
598
674
  }
675
+ if (name.toLowerCase().startsWith("forge_") || isUnsafeDotnetEnvironmentName(name)) {
676
+ throw new Error(`runtime_contract_invalid: launch environment name '${name}' is host-owned or enables pre-startup code injection and cannot be project-authored.`);
677
+ }
599
678
  return [name, boundedString(raw, 8192, `runtime.ports.${portName}.launch.environment.${name}`)];
600
679
  }));
601
680
  }
681
+ function isUnsafeDotnetEnvironmentName(name) {
682
+ const normalized = name.toUpperCase();
683
+ return normalized === "DOTNET_STARTUP_HOOKS"
684
+ || normalized === "DOTNET_ADDITIONAL_DEPS"
685
+ || normalized === "DOTNET_SHARED_STORE"
686
+ || normalized === "DOTNET_HOST_PATH"
687
+ || normalized === "MSBUILD_EXE_PATH"
688
+ || normalized === "MSBUILDSDKSPATH"
689
+ || normalized === "NUGET_PLUGIN_PATHS"
690
+ || normalized === "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES"
691
+ || normalized === "ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES"
692
+ || normalized.startsWith("CUSTOMBEFOREMICROSOFTCOMMON")
693
+ || normalized.startsWith("CUSTOMAFTERMICROSOFTCOMMON")
694
+ || normalized.startsWith("DOTNET_ROOT")
695
+ || normalized.startsWith("CORECLR_")
696
+ || normalized.startsWith("COR_")
697
+ || normalized.startsWith("COMPLUS_")
698
+ || normalized.startsWith("LD_")
699
+ || normalized.startsWith("DYLD_");
700
+ }
701
+ function rejectProjectAuthoredForgeArguments(args, adapter, portName) {
702
+ if (adapter !== "aspnetcore")
703
+ return;
704
+ const applicationArgumentIndex = args.indexOf("--");
705
+ if (applicationArgumentIndex < 0)
706
+ return;
707
+ const forbidden = args.slice(applicationArgumentIndex + 1).find(argument => {
708
+ const key = argument.split("=", 1)[0].replace(/^(?:--?|\/)/u, "");
709
+ return /^forge(?:[_:.-]|$)/iu.test(key);
710
+ });
711
+ if (forbidden) {
712
+ throw new Error(`runtime_contract_invalid: runtime.ports.${portName}.launch.args cannot override the host-owned FORGE_* namespace.`);
713
+ }
714
+ }
602
715
  function containedPortablePath(value, label) {
603
716
  const original = boundedString(value, 1024, label);
604
717
  const portable = original.replaceAll("\\", "/");
@@ -4,8 +4,11 @@ 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";
9
+ import { type AttemptDatabaseBroker } from "./attempt-database-broker.js";
8
10
  import { HostListenerInspector } from "./host-listener-inspector.js";
11
+ import { type MemoryAdmissionDiagnostics } from "./memory-capacity.js";
9
12
  import { PlaywrightHostRuntimeAdapter } from "./playwright-host-runtime-adapter.js";
10
13
  import { HostParallelPromotionStore } from "./host-runtime-promotion.js";
11
14
  import { type HostProcessRecovery } from "../shared/process/host-process-recovery.js";
@@ -15,6 +18,18 @@ export interface HostRuntimeLeaseStart {
15
18
  readonly runtimeContract: LoadedHostRuntimeContract;
16
19
  readonly executionClass: HostRuntimeExecutionClass;
17
20
  }
21
+ export interface HostRuntimeAdmissionCheckpoint {
22
+ 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";
23
+ readonly message: string;
24
+ readonly expiresAt: string;
25
+ }
26
+ export type HostRuntimeAdmissionCheckpointHandler = (checkpoint: HostRuntimeAdmissionCheckpoint) => void | Promise<void>;
27
+ export interface HostRuntimeProjectCheckpoint {
28
+ readonly code: "host_runtime_build_started" | "host_runtime_build_cached" | "host_runtime_build_completed" | "host_runtime_database_provisioned";
29
+ readonly message: string;
30
+ readonly data?: Readonly<Record<string, unknown>>;
31
+ }
32
+ export type HostRuntimeProjectCheckpointHandler = (checkpoint: HostRuntimeProjectCheckpoint) => void | Promise<void>;
18
33
  export type HostRuntimeExecutionClass = "analysis_only" | "managed_runtime";
19
34
  export type HostRuntimeSafetyClass = "host_serial_only" | "host_parallel_candidate" | "host_parallel_safe";
20
35
  export interface HostRuntimeProjectCapacitySnapshot {
@@ -29,12 +44,25 @@ export interface HostRuntimeProjectCapacitySnapshot {
29
44
  readonly promotedAt?: string | null;
30
45
  }
31
46
  export interface HostRuntimeCapacitySnapshot {
47
+ readonly preparedWorktreeReadiness?: boolean;
32
48
  readonly activeAttempts: number;
33
49
  readonly reservedAttempts: number;
34
50
  readonly waitingAttempts: number;
35
51
  readonly degradedAttempts: number;
36
52
  readonly exclusiveReservations: number;
37
53
  readonly sharedReservations: number;
54
+ readonly hardMaxAttempts: number;
55
+ readonly effectiveMaxAttempts: number;
56
+ readonly availableAttempts: number;
57
+ readonly totalMemoryBytes: number;
58
+ readonly availableMemoryBytes: number;
59
+ readonly hostReserveBytes: number;
60
+ readonly availableCpuMillis: number;
61
+ readonly availableWorkspaceBytes: number;
62
+ readonly activeReservationMemoryBytes: number;
63
+ readonly memoryAdmission: MemoryAdmissionDiagnostics;
64
+ readonly unavailableCode?: string | null;
65
+ readonly unavailableReason?: string | null;
38
66
  readonly projects: readonly HostRuntimeProjectCapacitySnapshot[];
39
67
  }
40
68
  export interface HostRuntimeReapOptions {
@@ -48,21 +76,41 @@ export interface HostRuntimeReapReport {
48
76
  readonly processesTerminated: number;
49
77
  readonly reusedPidsProtected: number;
50
78
  readonly degraded: number;
79
+ /**
80
+ * Active attempts of a previous runner instance whose every supervised
81
+ * process tree was proved absent, terminated or PID-reused. Forge may settle
82
+ * these exact attempts instead of leaving their launch outcome unknown.
83
+ */
84
+ readonly recoveredAttempts?: readonly HostRuntimeRecoveredAttempt[];
85
+ }
86
+ export interface HostRuntimeRecoveredAttempt {
87
+ readonly dispatchAttemptId: string;
88
+ readonly fencingToken: string;
89
+ readonly runnerInstanceId: string;
51
90
  }
52
91
  export interface HostNodeRuntimeLaunch<T> extends NodeRuntimeAdapterLaunch<T> {
53
92
  readonly runtimeLease: RuntimeIsolationLease;
54
93
  }
55
94
  export interface HostRuntimeLeaseRuntime {
95
+ acquirePreparation?(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
56
96
  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>;
97
+ start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal, onAdmissionCheckpoint?: HostRuntimeAdmissionCheckpointHandler): Promise<HostRuntimeLeaseStart>;
98
+ startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal, onProjectCheckpoint?: HostRuntimeProjectCheckpointHandler): Promise<HostRuntimeLeaseStart>;
59
99
  updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
60
100
  providerProcessRunner?(dispatchAttemptId: string, fencingToken: string): ProviderProcessRunner | null;
101
+ providerMemoryTelemetry?(dispatchAttemptId: string, fencingToken: string): AttemptProcessTreeMemoryTelemetry | null;
61
102
  release(dispatchAttemptId: string, fencingToken: string): Promise<RuntimeIsolationLease | null>;
62
- invalidatePromotion?(repoId: string): Promise<void>;
103
+ invalidatePromotion?(repoId: string, scope?: "all" | "discovery"): Promise<void>;
104
+ hasForegroundDemand?(): boolean;
63
105
  reap?(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport>;
64
106
  snapshot?(): HostRuntimeCapacitySnapshot;
65
107
  }
108
+ export interface HostCapacityResourceSample {
109
+ readonly totalMemoryBytes: number;
110
+ readonly availableMemoryBytes: number;
111
+ readonly totalCpuMillis: number;
112
+ readonly availableWorkspaceBytes: number;
113
+ }
66
114
  export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime {
67
115
  private readonly stateRoot;
68
116
  private readonly processSupervisors;
@@ -71,30 +119,58 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
71
119
  private readonly promotionStore;
72
120
  private readonly playwrightAdapter;
73
121
  private readonly processRecovery;
122
+ private readonly capacityResourceSample?;
123
+ private readonly databaseBroker;
124
+ private readonly preparedReadiness;
74
125
  private readonly active;
75
126
  private readonly pendingRenewals;
127
+ private readonly acquiringAdmissions;
76
128
  private readonly admissions;
77
129
  private readonly singletonOwners;
78
130
  private readonly singletonWaiters;
79
131
  private readonly parallelProjectOwners;
80
132
  private readonly parallelProjectWaiters;
133
+ private readonly capacityReservations;
134
+ private readonly capacityWaiters;
135
+ private capacityWaiterScanCursor;
136
+ private readonly backgroundAdmissionWaiters;
81
137
  private readonly degraded;
82
138
  private readonly knownPromotions;
139
+ private ledgerWriteTail;
140
+ private workspaceAvailableBytes;
83
141
  private readonly portBroker;
84
142
  private readonly nodeAdapter;
85
- constructor(stateRoot: string, portBroker?: HostPortBroker, nodeAdapter?: NodeHostRuntimeAdapter, processSupervisors?: AttemptProcessSupervisorRegistry, processRunner?: ProviderProcessRunner, listenerInspector?: HostListenerInspector, promotionStore?: HostParallelPromotionStore, playwrightAdapter?: PlaywrightHostRuntimeAdapter, processRecovery?: HostProcessRecovery);
143
+ private readonly capacityConfiguration;
144
+ private readonly memoryAvailability;
145
+ private readonly executionRoot;
146
+ constructor(stateRoot: string, portBroker?: HostPortBroker, nodeAdapter?: NodeHostRuntimeAdapter, processSupervisors?: AttemptProcessSupervisorRegistry, processRunner?: ProviderProcessRunner, listenerInspector?: HostListenerInspector, promotionStore?: HostParallelPromotionStore, playwrightAdapter?: PlaywrightHostRuntimeAdapter, processRecovery?: HostProcessRecovery, capacityResourceSample?: HostCapacityResourceSample | undefined, executionRoot?: string, databaseBroker?: AttemptDatabaseBroker | null);
86
147
  reap(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport>;
87
148
  snapshot(): HostRuntimeCapacitySnapshot;
149
+ hasForegroundDemand(): boolean;
150
+ acquirePreparation(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
88
151
  acquire(dispatch: RunOnceDispatchPacket, signal?: AbortSignal): Promise<readonly string[]>;
152
+ private reserveHostCapacity;
153
+ private reserveBackgroundAdmission;
154
+ private hostCapacityAvailable;
155
+ private hostCapacityImpossibleReason;
156
+ private hostResourceSnapshot;
157
+ private refreshWorkspaceCapacity;
158
+ private releaseHostCapacity;
159
+ private drainCapacityWaiters;
160
+ private capacityWaitersForScan;
161
+ private detachCapacityWaiter;
89
162
  private reserveSingletonKeys;
90
163
  private reserveParallelProject;
91
- start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
164
+ private upgradeParallelProjectToSingleton;
165
+ start(dispatch: RunOnceDispatchPacket, worktreePath: string, signal?: AbortSignal, onAdmissionCheckpoint?: HostRuntimeAdmissionCheckpointHandler): Promise<HostRuntimeLeaseStart>;
92
166
  updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
93
- startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal): Promise<HostRuntimeLeaseStart>;
167
+ startConfiguredProject(dispatchAttemptId: string, fencingToken: string, signal?: AbortSignal, onProjectCheckpoint?: HostRuntimeProjectCheckpointHandler): Promise<HostRuntimeLeaseStart>;
94
168
  providerProcessRunner(dispatchAttemptId: string, fencingToken: string): ProviderProcessRunner | null;
169
+ private markCapacityReservationRunning;
170
+ providerMemoryTelemetry(dispatchAttemptId: string, fencingToken: string): AttemptProcessTreeMemoryTelemetry | null;
95
171
  private promoteVerifiedRuntime;
96
172
  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>;
173
+ release(dispatchAttemptId: string, fencingToken: string, preservePreparation?: boolean): Promise<RuntimeIsolationLease | null>;
98
174
  private readLedgers;
99
175
  private recoverPersistedProcesses;
100
176
  private markLedgerDegraded;
@@ -102,6 +178,7 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
102
178
  private forgetDegraded;
103
179
  private attemptRoot;
104
180
  private writeLedger;
181
+ private writeLedgerAtomic;
105
182
  private ledgerPath;
106
183
  private singletonKeysAvailable;
107
184
  private assignSingletonKeys;
@@ -118,9 +195,13 @@ export declare class HostRuntimeLeaseManager implements HostRuntimeLeaseRuntime
118
195
  private cancelAdmissionWaiters;
119
196
  private drainSingletonWaiters;
120
197
  private drainParallelProjectWaiters;
198
+ private drainBackgroundAdmissionWaiters;
121
199
  private refreshKnownPromotions;
122
- invalidatePromotion(repoId: string): Promise<void>;
200
+ invalidatePromotion(repoId: string, scope?: "all" | "discovery"): Promise<void>;
123
201
  private drainAdmissionWaiters;
202
+ private reconcileReservationOwners;
203
+ private reservationOwnerIsLive;
204
+ private hasAttemptRepresentation;
124
205
  }
125
206
  export declare class HostRuntimeLeaseCleanupError extends Error {
126
207
  readonly lease: RuntimeIsolationLease;
@@ -129,5 +210,8 @@ export declare class HostRuntimeLeaseCleanupError extends Error {
129
210
  export declare class HostRuntimeAdmissionExpiredError extends Error {
130
211
  constructor();
131
212
  }
213
+ export declare class HostRuntimeCapacityRequestImpossibleError extends Error {
214
+ constructor(reason: string);
215
+ }
132
216
  export declare function hostRuntimeEventResources(lease: RuntimeIsolationLease, runtimeContract?: LoadedHostRuntimeContract, executionClass?: HostRuntimeExecutionClass): Record<string, unknown>;
133
217
  export declare function hostRuntimeSingletonKeys(dispatch: RunOnceDispatchPacket): readonly string[];