@loucompanion/forge-bridge 0.1.3-dev.f66395c3ac21 → 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 (221) hide show
  1. package/README.md +165 -1
  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 +9 -1
  5. package/dist/bridge/bridge.service.d.ts +129 -3
  6. package/dist/bridge/bridge.service.js +2427 -70
  7. package/dist/bridge/bridge.types.d.ts +218 -2
  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 +14 -2
  21. package/dist/cleanup/cleanup.service.js +496 -3
  22. package/dist/cleanup/cleanup.types.d.ts +10 -0
  23. package/dist/cleanup/index.d.ts +1 -1
  24. package/dist/cleanup/index.js +1 -1
  25. package/dist/cleanup/internals.d.ts +2 -2
  26. package/dist/cleanup/internals.js +1 -1
  27. package/dist/cli/cli.help.d.ts +2 -0
  28. package/dist/cli/cli.help.js +217 -0
  29. package/dist/cli/cli.service.d.ts +55 -7
  30. package/dist/cli/cli.service.js +429 -219
  31. package/dist/cli/local-config.d.ts +7 -0
  32. package/dist/cli/local-config.js +51 -3
  33. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  34. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  35. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  36. package/dist/runtime/attempt-database-broker.js +390 -0
  37. package/dist/runtime/attempt-node-runtime-launcher.d.ts +18 -0
  38. package/dist/runtime/attempt-node-runtime-launcher.js +140 -0
  39. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  40. package/dist/runtime/attempt-path-operation.js +94 -0
  41. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  42. package/dist/runtime/attempt-private-directory.js +54 -0
  43. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  44. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  45. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  46. package/dist/runtime/attempt-process-supervisor.js +397 -0
  47. package/dist/runtime/canonical-source-file.d.ts +3 -0
  48. package/dist/runtime/canonical-source-file.js +54 -0
  49. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  50. package/dist/runtime/docker-engine-probe.js +333 -0
  51. package/dist/runtime/generated-output-reset.d.ts +20 -0
  52. package/dist/runtime/generated-output-reset.js +157 -0
  53. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  54. package/dist/runtime/host-database-capability-boundary.js +62 -0
  55. package/dist/runtime/host-diagnostics.d.ts +11 -0
  56. package/dist/runtime/host-diagnostics.js +191 -0
  57. package/dist/runtime/host-execution-root.d.ts +4 -0
  58. package/dist/runtime/host-execution-root.js +39 -0
  59. package/dist/runtime/host-listener-inspector.d.ts +51 -0
  60. package/dist/runtime/host-listener-inspector.js +385 -0
  61. package/dist/runtime/host-port-broker.d.ts +76 -0
  62. package/dist/runtime/host-port-broker.js +333 -0
  63. package/dist/runtime/host-runtime-contract.d.ts +58 -0
  64. package/dist/runtime/host-runtime-contract.js +813 -0
  65. package/dist/runtime/host-runtime-lease.d.ts +217 -0
  66. package/dist/runtime/host-runtime-lease.js +2487 -0
  67. package/dist/runtime/host-runtime-promotion.d.ts +32 -0
  68. package/dist/runtime/host-runtime-promotion.js +206 -0
  69. package/dist/runtime/memory-capacity.d.ts +70 -0
  70. package/dist/runtime/memory-capacity.js +123 -0
  71. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  72. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  73. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  74. package/dist/runtime/node-host-runtime-adapter.js +285 -0
  75. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  76. package/dist/runtime/pinned-directory-process.js +61 -0
  77. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  78. package/dist/runtime/pinned-source-directory.js +94 -0
  79. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  80. package/dist/runtime/playwright-host-runtime-adapter.js +268 -0
  81. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  82. package/dist/runtime/prepared-runtime-identity.js +218 -0
  83. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  84. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  85. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  86. package/dist/runtime/runtime-isolation-lease.js +161 -0
  87. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  88. package/dist/runtime/workspace-resource-estimator.js +174 -0
  89. package/dist/sandbox/command-runner.d.ts +4 -0
  90. package/dist/sandbox/command-runner.js +47 -0
  91. package/dist/sandbox/contract.d.ts +5 -0
  92. package/dist/sandbox/contract.js +276 -0
  93. package/dist/sandbox/event-budget.d.ts +11 -0
  94. package/dist/sandbox/event-budget.js +27 -0
  95. package/dist/sandbox/image-policy.d.ts +35 -0
  96. package/dist/sandbox/image-policy.js +247 -0
  97. package/dist/sandbox/index.d.ts +5 -0
  98. package/dist/sandbox/index.js +5 -0
  99. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  100. package/dist/sandbox/lease-watchdog.js +67 -0
  101. package/dist/sandbox/preflight.d.ts +84 -0
  102. package/dist/sandbox/preflight.js +521 -0
  103. package/dist/sandbox/process.d.ts +20 -0
  104. package/dist/sandbox/process.js +209 -0
  105. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  106. package/dist/sandbox/provider-auth/codex.js +212 -0
  107. package/dist/sandbox/repository-lock.d.ts +6 -0
  108. package/dist/sandbox/repository-lock.js +128 -0
  109. package/dist/sandbox/runtime.d.ts +302 -0
  110. package/dist/sandbox/runtime.js +2367 -0
  111. package/dist/sandbox/sandbox.types.d.ts +93 -0
  112. package/dist/sandbox/sandbox.types.js +1 -0
  113. package/dist/sandbox/smoke.d.ts +76 -0
  114. package/dist/sandbox/smoke.js +380 -0
  115. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  116. package/dist/sandbox/worker-supervisor.js +36 -0
  117. package/dist/sandbox/worker.d.ts +1 -0
  118. package/dist/sandbox/worker.js +225 -0
  119. package/dist/sandbox/workspace.service.d.ts +13 -0
  120. package/dist/sandbox/workspace.service.js +74 -0
  121. package/dist/session/managed-attempt-tools.d.ts +28 -0
  122. package/dist/session/managed-attempt-tools.js +1015 -0
  123. package/dist/session/managed-build-coordinator.d.ts +5 -0
  124. package/dist/session/managed-build-coordinator.js +105 -0
  125. package/dist/session/managed-build-receipt.d.ts +21 -0
  126. package/dist/session/managed-build-receipt.js +337 -0
  127. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  128. package/dist/session/managed-verification-timeouts.js +17 -0
  129. package/dist/session/phase-completion.artifacts.d.ts +27 -0
  130. package/dist/session/phase-completion.artifacts.js +829 -0
  131. package/dist/session/phase-completion.d.ts +53 -42
  132. package/dist/session/phase-completion.helpers.d.ts +117 -0
  133. package/dist/session/phase-completion.helpers.js +707 -0
  134. package/dist/session/phase-completion.js +125 -3007
  135. package/dist/session/phase-completion.prompts.d.ts +14 -0
  136. package/dist/session/phase-completion.prompts.js +233 -0
  137. package/dist/session/phase-completion.runtime.d.ts +49 -0
  138. package/dist/session/phase-completion.runtime.js +840 -0
  139. package/dist/session/phase-completion.schemas.d.ts +3 -0
  140. package/dist/session/phase-completion.schemas.js +859 -0
  141. package/dist/session/phase-completion.validation.d.ts +38 -0
  142. package/dist/session/phase-completion.validation.js +918 -0
  143. package/dist/session/pre-publication-validation.d.ts +75 -0
  144. package/dist/session/pre-publication-validation.js +661 -0
  145. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  146. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  147. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  148. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  149. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +95 -0
  150. package/dist/session/provider-cli/provider-cli.artifacts.js +812 -0
  151. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  152. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  153. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  154. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  155. package/dist/session/provider-cli/provider-cli.deterministic.js +407 -0
  156. package/dist/session/provider-cli/provider-cli.process.d.ts +99 -0
  157. package/dist/session/provider-cli/provider-cli.process.js +874 -0
  158. package/dist/session/provider-cli/provider-cli.prompts.d.ts +52 -0
  159. package/dist/session/provider-cli/provider-cli.prompts.js +715 -0
  160. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  161. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +66 -0
  162. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  163. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  164. package/dist/session/provider-cli/provider-cli.runtime.d.ts +16 -0
  165. package/dist/session/provider-cli/provider-cli.runtime.js +81 -0
  166. package/dist/session/provider-cli/provider-cli.service.d.ts +47 -53
  167. package/dist/session/provider-cli/provider-cli.service.js +1257 -2497
  168. package/dist/session/provider-cli/provider-cli.types.d.ts +8 -0
  169. package/dist/session/provider-cli/provider-output-state.d.ts +32 -0
  170. package/dist/session/provider-cli/provider-output-state.js +817 -0
  171. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  172. package/dist/session/provider-cli/provider-usage.js +28 -5
  173. package/dist/session/session-redaction.d.ts +3 -0
  174. package/dist/session/session-redaction.js +448 -0
  175. package/dist/session/session.base.d.ts +6 -1
  176. package/dist/session/session.service.d.ts +8 -2
  177. package/dist/session/session.service.js +586 -59
  178. package/dist/session/session.types.d.ts +324 -3
  179. package/dist/session/session.types.js +939 -10
  180. package/dist/session/unreferenced-exports.d.ts +12 -0
  181. package/dist/session/unreferenced-exports.js +59 -0
  182. package/dist/shared/format/format-bytes.d.ts +1 -0
  183. package/dist/shared/format/format-bytes.js +9 -0
  184. package/dist/shared/git/git.js +2 -1
  185. package/dist/shared/path/path.d.ts +1 -0
  186. package/dist/shared/path/path.js +21 -1
  187. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  188. package/dist/shared/process/dotnet-toolchain.js +307 -0
  189. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  190. package/dist/shared/process/host-process-recovery.js +78 -0
  191. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  192. package/dist/shared/process/host-process-supervisor.js +330 -0
  193. package/dist/shared/process/process.d.ts +3 -0
  194. package/dist/shared/process/process.js +13 -7
  195. package/dist/shared/process/windows-launch.d.ts +8 -0
  196. package/dist/shared/process/windows-launch.js +41 -0
  197. package/dist/shared/provider-capabilities.d.ts +4 -0
  198. package/dist/shared/provider-capabilities.js +88 -0
  199. package/dist/shared/redaction/redaction.d.ts +1 -0
  200. package/dist/shared/redaction/redaction.js +47 -4
  201. package/dist/version.d.ts +1 -1
  202. package/dist/version.js +1 -1
  203. package/dist/worktree/artifact-injector.d.ts +3 -2
  204. package/dist/worktree/artifact-injector.js +82 -10
  205. package/dist/worktree/dependency-warmer.d.ts +53 -1
  206. package/dist/worktree/dependency-warmer.js +619 -11
  207. package/dist/worktree/index.d.ts +2 -1
  208. package/dist/worktree/index.js +1 -0
  209. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  210. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  211. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  212. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  213. package/dist/worktree/worktree-paths.d.ts +4 -0
  214. package/dist/worktree/worktree-paths.js +30 -0
  215. package/dist/worktree/worktree.base.d.ts +3 -0
  216. package/dist/worktree/worktree.service.d.ts +16 -3
  217. package/dist/worktree/worktree.service.js +334 -54
  218. package/dist/worktree/worktree.types.d.ts +9 -1
  219. package/package.json +12 -7
  220. package/runtime/windows-job-supervisor.ps1 +722 -0
  221. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,5 @@
1
+ export declare class ManagedBuildCoordinator {
2
+ private readonly stateRoot;
3
+ constructor(stateRoot?: string);
4
+ withBuildSlot<T>(worktreePath: string, action: (queueWaitMs: number) => Promise<T>, onWaiting?: () => Promise<void>): Promise<T>;
5
+ }
@@ -0,0 +1,105 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { defaultWorktreeCoordinatorStateRoot } from "../worktree/worktree-paths.js";
5
+ const heartbeatIntervalMs = 30_000;
6
+ const staleAfterMs = 2 * 60_000;
7
+ const acquisitionTimeoutMs = 2 * 60 * 60_000;
8
+ const pollIntervalMs = 100;
9
+ export class ManagedBuildCoordinator {
10
+ stateRoot;
11
+ constructor(stateRoot = defaultWorktreeCoordinatorStateRoot()) {
12
+ this.stateRoot = stateRoot;
13
+ }
14
+ async withBuildSlot(worktreePath, action, onWaiting) {
15
+ const resolved = path.resolve(worktreePath);
16
+ const key = createHash("sha256").update(resolved.toLowerCase()).digest("hex");
17
+ const root = path.join(this.stateRoot, "managed-build-locks");
18
+ const target = path.join(root, `${key}.lock`);
19
+ const token = randomUUID();
20
+ const startedAt = Date.now();
21
+ let waitingReported = false;
22
+ await fs.mkdir(root, { recursive: true, mode: 0o700 });
23
+ while (true) {
24
+ try {
25
+ const handle = await fs.open(target, "wx", 0o600);
26
+ try {
27
+ await handle.writeFile(`${JSON.stringify({
28
+ schemaVersion: 1,
29
+ token,
30
+ pid: process.pid,
31
+ worktreePath: resolved,
32
+ acquiredAt: new Date().toISOString(),
33
+ })}\n`);
34
+ await handle.sync();
35
+ }
36
+ finally {
37
+ await handle.close();
38
+ }
39
+ break;
40
+ }
41
+ catch (error) {
42
+ if (error.code !== "EEXIST")
43
+ throw error;
44
+ if (await staleLock(target)) {
45
+ await fs.rm(target, { force: true });
46
+ continue;
47
+ }
48
+ if (!waitingReported) {
49
+ waitingReported = true;
50
+ await onWaiting?.();
51
+ }
52
+ if (Date.now() - startedAt >= acquisitionTimeoutMs) {
53
+ throw new Error("managed_build_slot_timeout: another build held this worktree for more than two hours.", { cause: error });
54
+ }
55
+ await delay(pollIntervalMs);
56
+ }
57
+ }
58
+ const heartbeat = setInterval(() => void refreshHeartbeat(target, token), heartbeatIntervalMs);
59
+ heartbeat.unref?.();
60
+ try {
61
+ return await action(Date.now() - startedAt);
62
+ }
63
+ finally {
64
+ clearInterval(heartbeat);
65
+ const owner = await readOwner(target);
66
+ if (owner?.token === token)
67
+ await fs.rm(target, { force: true });
68
+ }
69
+ }
70
+ }
71
+ async function staleLock(target) {
72
+ const stat = await fs.lstat(target).catch(() => null);
73
+ if (!stat || stat.isSymbolicLink() || !stat.isFile() || stat.size > 16 * 1024)
74
+ return true;
75
+ if (Date.now() - stat.mtimeMs >= staleAfterMs)
76
+ return true;
77
+ const owner = await readOwner(target);
78
+ // A competing process can observe the exclusive lock file between open("wx")
79
+ // and the owner JSON being flushed. Keep a fresh unreadable lock rather than
80
+ // deleting a lock whose writer is still publishing its ownership record.
81
+ if (!owner || !Number.isInteger(owner.pid) || (owner.pid ?? 0) <= 0)
82
+ return false;
83
+ try {
84
+ process.kill(owner.pid, 0);
85
+ return false;
86
+ }
87
+ catch {
88
+ return true;
89
+ }
90
+ }
91
+ async function refreshHeartbeat(target, token) {
92
+ const owner = await readOwner(target);
93
+ if (owner?.token !== token)
94
+ return;
95
+ const now = new Date();
96
+ await fs.utimes(target, now, now).catch(() => undefined);
97
+ }
98
+ async function readOwner(target) {
99
+ return await fs.readFile(target, "utf8")
100
+ .then(value => JSON.parse(value))
101
+ .catch(() => null);
102
+ }
103
+ async function delay(ms) {
104
+ await new Promise(resolve => setTimeout(resolve, ms));
105
+ }
@@ -0,0 +1,21 @@
1
+ export interface ManagedBuildReceiptEvidence {
2
+ readonly fingerprint: string;
3
+ readonly receiptPath: string;
4
+ readonly producerCommand: readonly string[];
5
+ readonly configuration: string;
6
+ readonly coverage: string;
7
+ readonly outputCount: number;
8
+ }
9
+ export declare function publishManagedBuildReceipt(options: {
10
+ readonly worktreePath: string;
11
+ readonly command: string;
12
+ readonly args: readonly string[];
13
+ readonly configuration: string;
14
+ readonly coverage: string;
15
+ }): Promise<ManagedBuildReceiptEvidence | null>;
16
+ export declare function verifyManagedBuildReceipt(options: {
17
+ readonly worktreePath: string;
18
+ readonly configuration: string;
19
+ readonly requiredCoverage: string;
20
+ }): Promise<ManagedBuildReceiptEvidence>;
21
+ export declare function managedBuildConfiguration(command: string, args: readonly string[]): string;
@@ -0,0 +1,337 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { defaultWorktreeCoordinatorStateRoot } from "../worktree/worktree-paths.js";
6
+ const schemaVersion = 1;
7
+ const maximumRetainedReceipts = 8;
8
+ const maximumOutputManifestEntries = 20_000;
9
+ const skippedTraversalDirectories = new Set([
10
+ ".git",
11
+ ".forge-worktrees",
12
+ "node_modules",
13
+ ".venv",
14
+ "bin",
15
+ "obj",
16
+ "TestResults",
17
+ "artifacts",
18
+ ]);
19
+ export async function publishManagedBuildReceipt(options) {
20
+ const worktreePath = path.resolve(options.worktreePath);
21
+ const inputs = await currentBuildInputs(worktreePath);
22
+ const outputs = await collectBuildOutputs(worktreePath);
23
+ if (outputs.length === 0)
24
+ return null;
25
+ const producerCommand = [options.command, ...options.args];
26
+ const producerCommandFingerprint = sha256Json(producerCommand);
27
+ const fingerprint = `sha256:${sha256Json({
28
+ ...inputs,
29
+ configuration: options.configuration,
30
+ coverage: options.coverage,
31
+ producerCommandFingerprint,
32
+ outputs,
33
+ })}`;
34
+ const directory = managedBuildReceiptDirectory(worktreePath);
35
+ const receiptPath = path.join(directory, `${fingerprint.slice("sha256:".length)}.json`);
36
+ const receipt = {
37
+ schemaVersion,
38
+ fingerprint,
39
+ ...inputs,
40
+ configuration: options.configuration,
41
+ coverage: options.coverage,
42
+ producerCommand,
43
+ producerCommandFingerprint,
44
+ outputs,
45
+ completedAt: new Date().toISOString(),
46
+ };
47
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
48
+ await persistImmutableReceipt(receiptPath, receipt);
49
+ await persistJsonAtomically(path.join(directory, "current.json"), {
50
+ schemaVersion,
51
+ fingerprint,
52
+ receiptPath,
53
+ });
54
+ await pruneReceipts(directory, receiptPath);
55
+ return evidence(receipt, receiptPath);
56
+ }
57
+ export async function verifyManagedBuildReceipt(options) {
58
+ const worktreePath = path.resolve(options.worktreePath);
59
+ const directory = managedBuildReceiptDirectory(worktreePath);
60
+ const pointer = await readJson(path.join(directory, "current.json"));
61
+ const receiptPath = typeof pointer?.receiptPath === "string" ? pointer.receiptPath : "";
62
+ if (!receiptPath || path.dirname(receiptPath) !== directory) {
63
+ throw new Error("managed_verification_build_receipt_required: no verified local build receipt is available.");
64
+ }
65
+ const receipt = await readBuildReceipt(receiptPath);
66
+ if (!receipt || receipt.fingerprint !== pointer?.fingerprint) {
67
+ throw new Error("managed_verification_build_receipt_invalid: the local build receipt is missing or corrupt.");
68
+ }
69
+ if (receipt.configuration.toLowerCase() !== options.configuration.toLowerCase()) {
70
+ throw new Error(`managed_verification_build_receipt_mismatch: expected ${options.configuration} outputs but receipt proves ${receipt.configuration}.`);
71
+ }
72
+ if (receipt.coverage !== options.requiredCoverage) {
73
+ throw new Error(`managed_verification_build_receipt_mismatch: expected ${options.requiredCoverage} coverage but receipt proves ${receipt.coverage}.`);
74
+ }
75
+ const current = await currentBuildInputs(worktreePath);
76
+ if (receipt.sourceDigest !== current.sourceDigest
77
+ || receipt.dependencyDigest !== current.dependencyDigest
78
+ || receipt.toolchain !== current.toolchain
79
+ || receipt.platform !== current.platform
80
+ || receipt.architecture !== current.architecture
81
+ || receipt.policyFingerprint !== current.policyFingerprint) {
82
+ throw new Error("managed_verification_build_receipt_mismatch: source, dependencies, toolchain, platform, or policy changed after the verified build.");
83
+ }
84
+ if (!(await outputsMatch(worktreePath, receipt.outputs))) {
85
+ throw new Error("managed_verification_build_receipt_output_missing: one or more verified build outputs are missing or changed.");
86
+ }
87
+ return evidence(receipt, receiptPath);
88
+ }
89
+ export function managedBuildConfiguration(command, args) {
90
+ const optionIndex = args.findIndex(argument => ["-c", "--configuration", "-Configuration"].includes(argument));
91
+ if (optionIndex >= 0 && args[optionIndex + 1])
92
+ return args[optionIndex + 1];
93
+ const inline = args.find(argument => /^--configuration=/iu.test(argument));
94
+ if (inline)
95
+ return inline.slice(inline.indexOf("=") + 1);
96
+ const executable = path.basename(command).toLowerCase();
97
+ const scriptIndex = args.findIndex(argument => argument.toLowerCase() === "-file");
98
+ const script = scriptIndex >= 0 ? path.basename(args[scriptIndex + 1] ?? "").toLowerCase() : "";
99
+ // The affected-tests runner builds Release before running the Release tier scripts.
100
+ const runsAffectedTests = args.some(argument => path.basename(argument).toLowerCase() === "run-affected-dotnet-tests.mjs");
101
+ return executable.includes("powershell") || executable === "pwsh" || executable === "pwsh.exe"
102
+ || script.startsWith("run-dotnet-test-") || runsAffectedTests
103
+ ? "Release"
104
+ : "Debug";
105
+ }
106
+ function managedBuildReceiptDirectory(worktreePath) {
107
+ const key = createHash("sha256").update(path.resolve(worktreePath).toLowerCase()).digest("hex");
108
+ return path.join(defaultWorktreeCoordinatorStateRoot(), "managed-build-receipts", key);
109
+ }
110
+ async function currentBuildInputs(worktreePath) {
111
+ const [sourceDigest, dependencyDigest, toolchain] = await Promise.all([
112
+ worktreeSourceDigest(worktreePath),
113
+ dependencyReceiptsDigest(worktreePath),
114
+ dotnetVersion(worktreePath),
115
+ ]);
116
+ return {
117
+ sourceDigest,
118
+ dependencyDigest,
119
+ toolchain,
120
+ platform: process.platform,
121
+ architecture: process.arch,
122
+ policyFingerprint: `sha256:${sha256Json({
123
+ contract: "managed-build-reuse-v1",
124
+ runtimeMode: process.env.FORGE_RUNTIME_MODE ?? "host",
125
+ trustDomain: sha256Text(process.env.FORGE_TRUST_DOMAIN_ID ?? "default"),
126
+ invalidationEpoch: sha256Text(process.env.FORGE_BUILD_CACHE_EPOCH ?? "default"),
127
+ dotnetRoot: process.env.DOTNET_ROOT ?? null,
128
+ msbuildSdksPath: process.env.MSBuildSDKsPath ?? null,
129
+ nugetPackages: process.env.NUGET_PACKAGES ?? null,
130
+ })}`,
131
+ };
132
+ }
133
+ async function worktreeSourceDigest(worktreePath) {
134
+ const names = await gitFileNames(worktreePath);
135
+ const hash = createHash("sha256");
136
+ for (const relative of names) {
137
+ const absolute = path.resolve(worktreePath, relative);
138
+ const containment = path.relative(worktreePath, absolute);
139
+ if (containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment))
140
+ continue;
141
+ const stat = await fs.stat(absolute).catch(() => null);
142
+ if (!stat?.isFile())
143
+ continue;
144
+ hash.update(relative.replaceAll("\\", "/"));
145
+ hash.update("\0");
146
+ hash.update(await fs.readFile(absolute));
147
+ hash.update("\0");
148
+ }
149
+ return `sha256:${hash.digest("hex")}`;
150
+ }
151
+ async function gitFileNames(worktreePath) {
152
+ const output = await runProcess(worktreePath, "git", [
153
+ "ls-files",
154
+ "-z",
155
+ "--cached",
156
+ "--others",
157
+ "--exclude-standard",
158
+ ]);
159
+ if (output.exitCode !== 0) {
160
+ throw new Error(`managed_build_source_digest_failed: git ls-files exited ${output.exitCode}.`);
161
+ }
162
+ return output.stdout.split("\0").filter(Boolean).sort((left, right) => left.localeCompare(right));
163
+ }
164
+ async function dependencyReceiptsDigest(worktreePath) {
165
+ const receipts = [];
166
+ await walkDirectories(worktreePath, 0, async (directory, name) => {
167
+ if (!["node_modules", ".venv", "obj"].includes(name))
168
+ return;
169
+ const receiptPath = path.join(directory, name, ".forge-dependency-preparation.json");
170
+ const content = await fs.readFile(receiptPath).catch(() => null);
171
+ if (content)
172
+ receipts.push({
173
+ path: path.relative(worktreePath, receiptPath).replaceAll("\\", "/"),
174
+ digest: createHash("sha256").update(content).digest("hex"),
175
+ });
176
+ });
177
+ receipts.sort((left, right) => left.path.localeCompare(right.path));
178
+ return `sha256:${sha256Json(receipts)}`;
179
+ }
180
+ async function collectBuildOutputs(worktreePath) {
181
+ const outputs = [];
182
+ await walkDirectories(worktreePath, 0, async (directory, name) => {
183
+ if (name !== "bin")
184
+ return;
185
+ await collectFiles(path.join(directory, name), worktreePath, outputs);
186
+ });
187
+ outputs.sort((left, right) => left.path.localeCompare(right.path));
188
+ if (outputs.length > maximumOutputManifestEntries) {
189
+ throw new Error(`managed_build_receipt_too_large: build produced more than ${maximumOutputManifestEntries} output files.`);
190
+ }
191
+ return outputs;
192
+ }
193
+ async function collectFiles(directory, worktreePath, outputs) {
194
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
195
+ for (const entry of entries) {
196
+ const absolute = path.join(directory, entry.name);
197
+ if (entry.isDirectory())
198
+ await collectFiles(absolute, worktreePath, outputs);
199
+ else if (entry.isFile()) {
200
+ const stat = await fs.stat(absolute);
201
+ outputs.push({
202
+ path: path.relative(worktreePath, absolute).replaceAll("\\", "/"),
203
+ size: stat.size,
204
+ digest: createHash("sha256").update(await fs.readFile(absolute)).digest("hex"),
205
+ });
206
+ }
207
+ }
208
+ }
209
+ async function walkDirectories(directory, depth, observe) {
210
+ if (depth > 8)
211
+ return;
212
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
213
+ for (const entry of entries) {
214
+ if (!entry.isDirectory())
215
+ continue;
216
+ await observe(directory, entry.name);
217
+ if (!skippedTraversalDirectories.has(entry.name) && !entry.name.startsWith(".")) {
218
+ await walkDirectories(path.join(directory, entry.name), depth + 1, observe);
219
+ }
220
+ }
221
+ }
222
+ async function outputsMatch(worktreePath, outputs) {
223
+ if (outputs.length === 0)
224
+ return false;
225
+ for (const output of outputs) {
226
+ const absolute = path.resolve(worktreePath, output.path);
227
+ const containment = path.relative(worktreePath, absolute);
228
+ if (containment.startsWith(`..${path.sep}`) || path.isAbsolute(containment))
229
+ return false;
230
+ const stat = await fs.stat(absolute).catch(() => null);
231
+ if (!stat?.isFile() || stat.size !== output.size)
232
+ return false;
233
+ const digest = createHash("sha256").update(await fs.readFile(absolute)).digest("hex");
234
+ if (digest !== output.digest)
235
+ return false;
236
+ }
237
+ return true;
238
+ }
239
+ async function dotnetVersion(cwd) {
240
+ const result = await runProcess(cwd, "dotnet", ["--version"]);
241
+ return result.exitCode === 0
242
+ ? `dotnet@${result.stdout.trim() || result.stderr.trim() || "unknown"}`
243
+ : `dotnet@unavailable:${result.exitCode}`;
244
+ }
245
+ async function runProcess(cwd, command, args) {
246
+ return await new Promise((resolve, reject) => {
247
+ const child = spawn(command, args, { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
248
+ let stdout = "";
249
+ let stderr = "";
250
+ child.stdout.setEncoding("utf8");
251
+ child.stderr.setEncoding("utf8");
252
+ child.stdout.on("data", chunk => { stdout += String(chunk); });
253
+ child.stderr.on("data", chunk => { stderr += String(chunk); });
254
+ child.once("error", reject);
255
+ child.once("close", exitCode => resolve({ exitCode, stdout, stderr }));
256
+ });
257
+ }
258
+ async function persistImmutableReceipt(receiptPath, receipt) {
259
+ const existing = await readBuildReceipt(receiptPath);
260
+ if (existing)
261
+ return;
262
+ const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
263
+ await fs.writeFile(temporaryPath, `${JSON.stringify(receipt, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
264
+ try {
265
+ await fs.rename(temporaryPath, receiptPath).catch(async (error) => {
266
+ if (error.code !== "EEXIST")
267
+ throw error;
268
+ });
269
+ }
270
+ finally {
271
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
272
+ }
273
+ }
274
+ async function persistJsonAtomically(receiptPath, value) {
275
+ const temporaryPath = `${receiptPath}.${randomUUID()}.tmp`;
276
+ await fs.writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
277
+ try {
278
+ await fs.rename(temporaryPath, receiptPath);
279
+ }
280
+ finally {
281
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
282
+ }
283
+ }
284
+ async function readBuildReceipt(receiptPath) {
285
+ const value = await readJson(receiptPath);
286
+ return value?.schemaVersion === schemaVersion
287
+ && typeof value.fingerprint === "string"
288
+ && typeof value.sourceDigest === "string"
289
+ && typeof value.dependencyDigest === "string"
290
+ && typeof value.toolchain === "string"
291
+ && typeof value.policyFingerprint === "string"
292
+ && typeof value.configuration === "string"
293
+ && typeof value.coverage === "string"
294
+ && Array.isArray(value.producerCommand)
295
+ && Array.isArray(value.outputs)
296
+ ? value
297
+ : null;
298
+ }
299
+ async function readJson(receiptPath) {
300
+ try {
301
+ return JSON.parse(await fs.readFile(receiptPath, "utf8"));
302
+ }
303
+ catch (error) {
304
+ if (error.code === "ENOENT" || error instanceof SyntaxError)
305
+ return null;
306
+ throw error;
307
+ }
308
+ }
309
+ async function pruneReceipts(directory, preservedPath) {
310
+ const entries = await fs.readdir(directory, { withFileTypes: true });
311
+ const receipts = await Promise.all(entries
312
+ .filter(entry => entry.isFile() && /^[0-9a-f]{64}\.json$/u.test(entry.name))
313
+ .map(async (entry) => {
314
+ const receiptPath = path.join(directory, entry.name);
315
+ return { receiptPath, stat: await fs.stat(receiptPath) };
316
+ }));
317
+ receipts.sort((left, right) => right.stat.mtimeMs - left.stat.mtimeMs);
318
+ await Promise.all(receipts.slice(maximumRetainedReceipts)
319
+ .filter(entry => entry.receiptPath !== preservedPath)
320
+ .map(entry => fs.rm(entry.receiptPath, { force: true })));
321
+ }
322
+ function evidence(receipt, receiptPath) {
323
+ return {
324
+ fingerprint: receipt.fingerprint,
325
+ receiptPath,
326
+ producerCommand: receipt.producerCommand,
327
+ configuration: receipt.configuration,
328
+ coverage: receipt.coverage,
329
+ outputCount: receipt.outputs.length,
330
+ };
331
+ }
332
+ function sha256Json(value) {
333
+ return sha256Text(JSON.stringify(value));
334
+ }
335
+ function sha256Text(value) {
336
+ return createHash("sha256").update(value).digest("hex");
337
+ }
@@ -0,0 +1,8 @@
1
+ export declare const managedQuickTierTimeoutMs = 900000;
2
+ export declare const managedLongTierTimeoutMs = 2700000;
3
+ export declare const managedShardWaveTimeoutMs = 900000;
4
+ export declare const managedMaximumCommandTimeoutMs = 7200000;
5
+ export declare function managedVerificationTierTimeoutMs(tier: string, allShards?: {
6
+ readonly shardCount: number;
7
+ readonly maxParallelism: number;
8
+ }): number;
@@ -0,0 +1,17 @@
1
+ // Runner-owned limits for managed .NET verification. The comment-repair prompt quotes
2
+ // them, so an agent knows a tier that is still running has not failed: Forge terminates
3
+ // a hung command at its limit and the receipt then reports timed_out.
4
+ export const managedQuickTierTimeoutMs = 900_000;
5
+ export const managedLongTierTimeoutMs = 2_700_000;
6
+ export const managedShardWaveTimeoutMs = 900_000;
7
+ export const managedMaximumCommandTimeoutMs = 7_200_000;
8
+ export function managedVerificationTierTimeoutMs(tier, allShards) {
9
+ if (tier === "Unit" || tier === "Contract")
10
+ return managedQuickTierTimeoutMs;
11
+ if (!allShards)
12
+ return managedLongTierTimeoutMs;
13
+ // One window covers the build and every wave of shards, so a single-shard limit would
14
+ // end a healthy all-shard run of a large tier.
15
+ const waves = Math.ceil(allShards.shardCount / Math.max(1, allShards.maxParallelism));
16
+ return Math.min(managedMaximumCommandTimeoutMs, Math.max(managedLongTierTimeoutMs, managedShardWaveTimeoutMs * (waves + 1)));
17
+ }
@@ -0,0 +1,27 @@
1
+ import { type CompletionArtifactReference, type CompletionPhase, type CompletionToolServerConfig, type DecomposeCompletionGate, type DraftCompletionGate, type QaCompletionGate } from "./phase-completion.js";
2
+ import type { RunnerDispatchArtifact } from "./session.types.js";
3
+ export declare function writeDecomposeGateCompletionArtifact(config: CompletionToolServerConfig, gate: DecomposeCompletionGate, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
4
+ export declare function writeDraftGateCompletionArtifact(config: CompletionToolServerConfig, gate: DraftCompletionGate, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
5
+ export declare function writeCompletionArtifact(config: CompletionToolServerConfig, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
6
+ export declare function writeArchitectCompletionArtifact(config: CompletionToolServerConfig, payload: Record<string, unknown>, status: string, producedBy: string): Promise<CompletionArtifactReference>;
7
+ export declare function writeQaGateCompletionArtifact(config: CompletionToolServerConfig, gate: QaCompletionGate, input: unknown): Promise<CompletionArtifactReference>;
8
+ export declare function writeQaCompletionArtifact(config: CompletionToolServerConfig, payload: Record<string, unknown>, status: string, producedBy: string): Promise<CompletionArtifactReference>;
9
+ export declare function readQaGateArtifact(config: CompletionToolServerConfig, gate: QaCompletionGate): Promise<Record<string, unknown>>;
10
+ export declare function readCompletionArtifact(worktreePath: string, workItemId: string, phase: CompletionPhase, ownerType?: string | null, ownerId?: string | null): Promise<RunnerDispatchArtifact | null>;
11
+ export interface ArtifactBundleFile {
12
+ readonly fileName: string;
13
+ readonly content: string;
14
+ }
15
+ export interface ArtifactBundlePublicationOptions {
16
+ readonly signal?: AbortSignal;
17
+ /** Fault-injection hook used by publication tests. */
18
+ readonly beforePublishFile?: (fileName: string, index: number) => void | Promise<void>;
19
+ }
20
+ /**
21
+ * Stages a complete phase bundle outside the live artifact directory, then
22
+ * publishes its canonical verdict/marker last. Consumers treat the marker as
23
+ * the bundle's commit point, so they can observe either the old complete bundle
24
+ * or the new complete bundle, never a partially written set. A failed or
25
+ * cancelled publication rolls every touched file back before rejecting.
26
+ */
27
+ export declare function publishArtifactBundleAtomic(artifactRoot: string, files: readonly ArtifactBundleFile[], commitFileName: string, options?: ArtifactBundlePublicationOptions): Promise<void>;