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

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 (165) 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 +70 -22
  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 +192 -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 +29 -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 +19 -9
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +137 -107
  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 +489 -34
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +106 -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 +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  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-recovery.js +3 -2
  144. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  145. package/dist/shared/process/host-process-supervisor.js +60 -11
  146. package/dist/shared/process/process.d.ts +2 -0
  147. package/dist/shared/process/process.js +12 -7
  148. package/dist/shared/provider-capabilities.d.ts +3 -0
  149. package/dist/shared/provider-capabilities.js +85 -0
  150. package/dist/shared/redaction/redaction.d.ts +1 -0
  151. package/dist/shared/redaction/redaction.js +39 -3
  152. package/dist/shared/time/monotonic.d.ts +1 -0
  153. package/dist/shared/time/monotonic.js +8 -0
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/worktree/dependency-warmer.d.ts +41 -9
  157. package/dist/worktree/dependency-warmer.js +497 -22
  158. package/dist/worktree/index.d.ts +1 -1
  159. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  160. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  161. package/dist/worktree/worktree.service.d.ts +7 -0
  162. package/dist/worktree/worktree.service.js +253 -21
  163. package/dist/worktree/worktree.types.d.ts +7 -1
  164. package/package.json +7 -5
  165. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -0,0 +1,285 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { GitClient } from "../shared/git/git.js";
3
+ import { loadHostRuntimeContract } from "./host-runtime-contract.js";
4
+ import { configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible } from "./host-runtime-promotion.js";
5
+ import { preparedRuntimeIdentity } from "./prepared-runtime-identity.js";
6
+ /** Foreground barrier: prepare a wave, verify its exact runtimes, then release agents. */
7
+ export class PreparedRuntimeReadiness {
8
+ runtime;
9
+ store;
10
+ releasePreparation;
11
+ admissionExpired;
12
+ preparations = new Map();
13
+ draining = new Set();
14
+ constructor(runtime, store, releasePreparation, admissionExpired) {
15
+ this.runtime = runtime;
16
+ this.store = store;
17
+ this.releasePreparation = releasePreparation;
18
+ this.admissionExpired = admissionExpired;
19
+ }
20
+ async acquire(dispatch, signal) {
21
+ const id = dispatch.sandbox.dispatchAttemptId;
22
+ if (this.preparations.has(id))
23
+ throw new Error("host_runtime_preparation_conflict: duplicate preparation.");
24
+ const controller = new AbortController();
25
+ const abort = () => this.cancel(id, dispatch.lease.fencingToken);
26
+ const entry = { dispatch, controller, phase: "preparing", expiresAt: dispatch.lease.expiresAt,
27
+ removeAbort: () => signal?.removeEventListener("abort", abort) };
28
+ this.preparations.set(id, entry);
29
+ this.scheduleDeadline(entry);
30
+ signal?.addEventListener("abort", abort, { once: true });
31
+ if (signal?.aborted)
32
+ abort();
33
+ try {
34
+ // Preparation consumes normal capacity, but no project-runtime claim.
35
+ return await this.runtime.acquire({ ...dispatch,
36
+ runtimeIntent: { ...dispatch.runtimeIntent, executionClass: "analysis_only", applicationRuntimeRequired: false },
37
+ }, controller.signal);
38
+ }
39
+ catch (error) {
40
+ const failure = controller.signal.aborted ? controller.signal.reason : error;
41
+ this.cancel(id, dispatch.lease.fencingToken);
42
+ throw failure;
43
+ }
44
+ }
45
+ has(id) { return this.preparations.has(id); }
46
+ hasPending() { return this.preparations.size > 0; }
47
+ pendingSnapshots() {
48
+ return [...this.preparations.values()].map(entry => ({
49
+ repoId: entry.dispatch.repo.id,
50
+ dispatchAttemptId: entry.dispatch.sandbox.dispatchAttemptId,
51
+ fencingToken: entry.dispatch.lease.fencingToken,
52
+ ...(entry.probe
53
+ ? {
54
+ probeDispatchAttemptId: entry.probe.sandbox.dispatchAttemptId,
55
+ probeFencingToken: entry.probe.lease.fencingToken,
56
+ }
57
+ : {}),
58
+ }));
59
+ }
60
+ async verify(dispatch, worktree, checkpoint) {
61
+ const id = dispatch.sandbox.dispatchAttemptId;
62
+ const entry = this.preparations.get(id);
63
+ if (!entry || entry.dispatch.lease.fencingToken !== dispatch.lease.fencingToken) {
64
+ throw new Error("host_runtime_preparation_missing: preparation ownership changed.");
65
+ }
66
+ // Release preparation headroom before waiting for peers; otherwise a one-slot
67
+ // host would deadlock while the rest of the wave tries to prepare.
68
+ await this.releasePreparation(id, dispatch.lease.fencingToken);
69
+ entry.controller.signal.throwIfAborted();
70
+ const completion = new Promise((resolve, reject) => {
71
+ entry.resolve = resolve;
72
+ entry.reject = reject;
73
+ });
74
+ entry.check = () => this.check(entry, worktree, checkpoint);
75
+ entry.phase = "queued";
76
+ void this.drain(dispatch.repo.id);
77
+ return await completion;
78
+ }
79
+ async renew(id, fence, expiresAt) {
80
+ const entry = this.preparations.get(id);
81
+ if (!entry || entry.dispatch.lease.fencingToken !== fence || Date.parse(expiresAt) <= Date.parse(entry.expiresAt))
82
+ return;
83
+ entry.expiresAt = expiresAt;
84
+ this.scheduleDeadline(entry);
85
+ if (entry.probe)
86
+ await this.runtime.updateLease(entry.probe.sandbox.dispatchAttemptId, entry.probe.lease.fencingToken, expiresAt);
87
+ }
88
+ cancel(id, fence, reason = new Error("host_runtime_preparation_cancelled: prepared readiness was cancelled.")) {
89
+ const entry = this.preparations.get(id);
90
+ if (!entry || entry.dispatch.lease.fencingToken !== fence)
91
+ return;
92
+ if (entry.deadline)
93
+ clearTimeout(entry.deadline);
94
+ entry.controller.abort(reason);
95
+ // Do not return to session teardown while a supervised probe still owns the worktree.
96
+ if (entry.phase === "checking")
97
+ return;
98
+ entry.reject?.(entry.controller.signal.reason);
99
+ entry.removeAbort();
100
+ this.preparations.delete(id);
101
+ void this.drain(entry.dispatch.repo.id);
102
+ }
103
+ async drain(repoId) {
104
+ if (this.draining.has(repoId))
105
+ return;
106
+ const entries = () => [...this.preparations.values()].filter(entry => entry.dispatch.repo.id === repoId);
107
+ if (entries().some(entry => entry.phase === "preparing"))
108
+ return;
109
+ this.draining.add(repoId);
110
+ const completed = [];
111
+ try {
112
+ for (const entry of entries()) {
113
+ if (entry.phase !== "queued")
114
+ continue;
115
+ entry.phase = "checking";
116
+ try {
117
+ completed.push({ entry, identity: await entry.check() });
118
+ }
119
+ catch (error) {
120
+ completed.push({ entry, error });
121
+ }
122
+ }
123
+ // No provider from this wave can start while a sibling proof is running.
124
+ for (const result of completed) {
125
+ const { entry } = result;
126
+ this.preparations.delete(entry.dispatch.sandbox.dispatchAttemptId);
127
+ if (entry.deadline)
128
+ clearTimeout(entry.deadline);
129
+ entry.removeAbort();
130
+ if (entry.controller.signal.aborted)
131
+ entry.reject?.(entry.controller.signal.reason);
132
+ else if (result.error)
133
+ entry.reject?.(result.error);
134
+ else
135
+ entry.resolve?.(result.identity);
136
+ }
137
+ }
138
+ finally {
139
+ this.draining.delete(repoId);
140
+ if (entries().some(entry => entry.phase === "queued"))
141
+ void this.drain(repoId);
142
+ }
143
+ }
144
+ scheduleDeadline(entry) {
145
+ if (entry.deadline)
146
+ clearTimeout(entry.deadline);
147
+ const delay = Date.parse(entry.expiresAt) - Date.now();
148
+ entry.deadline = setTimeout(() => this.cancel(entry.dispatch.sandbox.dispatchAttemptId, entry.dispatch.lease.fencingToken, this.admissionExpired()), Number.isFinite(delay) ? Math.max(0, Math.min(delay, 2_147_483_647)) : 0);
149
+ entry.deadline.unref();
150
+ }
151
+ async check(entry, worktree, checkpoint) {
152
+ const { dispatch, controller } = entry;
153
+ if (!dispatch.runtimeIntent.applicationRuntimeRequired)
154
+ return undefined;
155
+ const contract = await loadHostRuntimeContract(worktree);
156
+ if (!hostRuntimeParallelEligible(contract))
157
+ return undefined;
158
+ const preparedDiagnostics = { entries: new Map() };
159
+ const identity = await preparedRuntimeIdentity(worktree, controller.signal, preparedDiagnostics);
160
+ if (hostRuntimeContractFingerprint(contract) !== identity.runtimeContractFingerprint) {
161
+ throw new Error("host_runtime_prepared_source_changed: runtime contract changed while capturing its source identity.");
162
+ }
163
+ if (await this.store.load(dispatch.repo.id, identity))
164
+ return identity;
165
+ await checkpoint?.({ code: "host_runtime_prepared_probe_started", expiresAt: entry.expiresAt,
166
+ message: `Verifying prepared runtime ${identity.inputCommitSha.slice(0, 12)} before agent admission; project-default readiness is not a substitute.` });
167
+ const probeId = randomUUID();
168
+ const probe = { ...dispatch,
169
+ cycleRun: { ...dispatch.cycleRun, phase: "prepared_readiness" },
170
+ lease: { ...dispatch.lease, id: probeId, fencingToken: randomUUID(), expiresAt: entry.expiresAt },
171
+ sandbox: { ...dispatch.sandbox, dispatchAttemptId: probeId, executionLeaseId: probeId,
172
+ inputCommitSha: identity.inputCommitSha },
173
+ };
174
+ entry.probe = probe;
175
+ const stopPreparedHeadMonitor = new AbortController();
176
+ const preparedHeadMonitor = monitorPreparedHead(worktree, identity.inputCommitSha, controller, stopPreparedHeadMonitor.signal);
177
+ let cleanupConfirmed;
178
+ let verifiedRuntimeContractFingerprint;
179
+ let verifiedPorts;
180
+ try {
181
+ controller.signal.throwIfAborted();
182
+ await this.runtime.acquire(probe, controller.signal);
183
+ const prepared = await this.runtime.start(probe, worktree, controller.signal);
184
+ if (hostRuntimeContractFingerprint(prepared.runtimeContract) !== identity.runtimeContractFingerprint) {
185
+ throw new Error("host_runtime_prepared_source_changed: probe loaded a different runtime contract; application was not started.");
186
+ }
187
+ const started = await this.runtime.startConfiguredProject(probeId, probe.lease.fencingToken, controller.signal, progress => checkpoint?.({ code: "host_runtime_prepared_probe_progress", expiresAt: entry.expiresAt,
188
+ message: progress.message }));
189
+ verifiedRuntimeContractFingerprint = hostRuntimeContractFingerprint(started.runtimeContract);
190
+ verifiedPorts = started.lease.resources.ports.map(port => port.name).sort();
191
+ }
192
+ finally {
193
+ stopPreparedHeadMonitor.abort();
194
+ await preparedHeadMonitor;
195
+ // A failed teardown must not publish reusable evidence.
196
+ cleanupConfirmed = Boolean(await this.runtime.release(probeId, probe.lease.fencingToken));
197
+ entry.probe = undefined;
198
+ }
199
+ if (!cleanupConfirmed) {
200
+ throw new Error("host_runtime_prepared_probe_cleanup_unconfirmed: matching probe teardown was not confirmed.");
201
+ }
202
+ // Probe-owned processes may create transient, untracked files while they
203
+ // are alive. Verify the prepared source only after matching teardown is
204
+ // confirmed: persistent source edits still fail closed, while files that
205
+ // belong to the probe lifecycle cannot create a false source-change loop.
206
+ const currentDiagnostics = { entries: new Map() };
207
+ const currentIdentity = await preparedRuntimeIdentity(worktree, controller.signal, currentDiagnostics);
208
+ const changed = [];
209
+ if (verifiedRuntimeContractFingerprint !== identity.runtimeContractFingerprint
210
+ || currentIdentity.runtimeContractFingerprint !== identity.runtimeContractFingerprint) {
211
+ changed.push("runtime contract");
212
+ }
213
+ if (currentIdentity.inputCommitSha !== identity.inputCommitSha)
214
+ changed.push("HEAD");
215
+ if (currentIdentity.sourceFingerprint !== identity.sourceFingerprint) {
216
+ const paths = changedIdentityPaths(preparedDiagnostics.entries, currentDiagnostics.entries);
217
+ changed.push(paths.length > 0
218
+ ? `source fingerprint (${paths.join(", ")})`
219
+ : "source fingerprint");
220
+ }
221
+ if (changed.length > 0) {
222
+ throw new Error(`host_runtime_prepared_source_changed: ${changed.join(", ")} changed during isolation verification.`);
223
+ }
224
+ if (JSON.stringify(verifiedPorts) !== JSON.stringify(configuredLaunchPortNames(contract))) {
225
+ throw new Error("host_runtime_prepared_probe_incomplete: assigned-port verification did not confirm every configured launch port.");
226
+ }
227
+ controller.signal.throwIfAborted();
228
+ await this.store.observe(dispatch.repo.id, contract, probeId, configuredLaunchPortNames(contract), identity.inputCommitSha, identity.sourceFingerprint);
229
+ await checkpoint?.({ code: "host_runtime_prepared_probe_completed", expiresAt: entry.expiresAt,
230
+ message: `Prepared runtime ${identity.inputCommitSha.slice(0, 12)} passed isolation verification; its source-bound proof is retained independently.` });
231
+ return identity;
232
+ }
233
+ }
234
+ const preparedHeadPollIntervalMs = 1_000;
235
+ async function monitorPreparedHead(worktree, expectedCommitSha, preparationController, stopSignal) {
236
+ const git = new GitClient();
237
+ while (!stopSignal.aborted && !preparationController.signal.aborted) {
238
+ try {
239
+ const currentCommitSha = (await git.run(worktree, ["rev-parse", "--verify", "HEAD^{commit}"]))
240
+ .trim()
241
+ .toLowerCase();
242
+ if (currentCommitSha !== expectedCommitSha.toLowerCase()) {
243
+ throw new Error("host_runtime_prepared_source_changed: HEAD changed during isolation verification; the obsolete probe was cancelled.");
244
+ }
245
+ }
246
+ catch (error) {
247
+ if (!stopSignal.aborted && !preparationController.signal.aborted) {
248
+ const failure = error instanceof Error && error.message.startsWith("host_runtime_prepared_source_changed:")
249
+ ? error
250
+ : new Error(`host_runtime_prepared_source_changed: HEAD could not be revalidated during isolation verification: ${errorMessage(error)}`);
251
+ preparationController.abort(failure);
252
+ }
253
+ return;
254
+ }
255
+ await waitForPreparedHeadPoll(stopSignal);
256
+ }
257
+ }
258
+ async function waitForPreparedHeadPoll(signal) {
259
+ if (signal.aborted)
260
+ return;
261
+ await new Promise(resolve => {
262
+ const complete = () => {
263
+ clearTimeout(timer);
264
+ signal.removeEventListener("abort", complete);
265
+ resolve();
266
+ };
267
+ const timer = setTimeout(complete, preparedHeadPollIntervalMs);
268
+ timer.unref();
269
+ signal.addEventListener("abort", complete, { once: true });
270
+ if (signal.aborted)
271
+ complete();
272
+ });
273
+ }
274
+ function errorMessage(error) {
275
+ return error instanceof Error ? error.message : String(error);
276
+ }
277
+ function changedIdentityPaths(before, after) {
278
+ const paths = new Set([...before.keys(), ...after.keys()]);
279
+ const changed = [...paths]
280
+ .filter(path => before.get(path) !== after.get(path))
281
+ .sort((left, right) => left.localeCompare(right));
282
+ if (changed.length <= 8)
283
+ return changed;
284
+ return [...changed.slice(0, 8), `+${changed.length - 8} more`];
285
+ }
@@ -57,7 +57,10 @@ export function transitionRuntimeIsolationLease(lease, nextState, occurredAt = n
57
57
  return transitioned;
58
58
  }
59
59
  export function renewRuntimeIsolationLease(lease, fencingToken, expiresAt) {
60
- if (lease.fencingToken !== fencingToken || lease.state === "released" || !validTimestamp(expiresAt))
60
+ if (lease.fencingToken !== fencingToken
61
+ || lease.state === "releasing"
62
+ || lease.state === "released"
63
+ || !validTimestamp(expiresAt))
61
64
  return null;
62
65
  const renewed = { ...lease, expiresAt };
63
66
  return validRuntimeIsolationLease(renewed) ? renewed : null;
@@ -0,0 +1,17 @@
1
+ export declare const minimumWorkspaceEstimateBytes: number;
2
+ export declare const architectMinimumWorkspaceEstimateBytes: number;
3
+ export interface RepositoryWorkspaceFootprint {
4
+ readonly sourceBytes: number;
5
+ readonly hasNode: boolean;
6
+ readonly hasDotNet: boolean;
7
+ readonly hasBrowser: boolean;
8
+ readonly serviceCount: number;
9
+ }
10
+ export interface WorkspaceEstimateHints {
11
+ readonly role?: string | null;
12
+ readonly serviceCount?: number;
13
+ readonly browser?: boolean;
14
+ }
15
+ export declare function estimateWorkspaceBytesFromRepo(repoPath: string, hints?: WorkspaceEstimateHints): Promise<number>;
16
+ export declare function calculateWorkspaceBytes(footprint: RepositoryWorkspaceFootprint, role?: string | null): number;
17
+ export declare function measureRepositoryWorkspace(repoPath: string): Promise<RepositoryWorkspaceFootprint>;
@@ -0,0 +1,174 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ const GiB = 1024 ** 3;
6
+ const alignmentBytes = 512 * 1024 ** 2;
7
+ export const minimumWorkspaceEstimateBytes = 2 * GiB;
8
+ export const architectMinimumWorkspaceEstimateBytes = GiB;
9
+ const maximumBytes = 100 * GiB;
10
+ const footprintCacheLifetimeMs = 5 * 60_000;
11
+ const excludedDirectories = new Set([
12
+ ".git",
13
+ ".artifacts",
14
+ ".forge-worktrees",
15
+ ".local",
16
+ ".next",
17
+ ".nuget",
18
+ ".pnpm-store",
19
+ ".tmp",
20
+ ".turbo",
21
+ "artifacts",
22
+ "bin",
23
+ "build",
24
+ "coverage",
25
+ "dist",
26
+ "node_modules",
27
+ "obj",
28
+ "testresults",
29
+ ]);
30
+ const footprintCache = new Map();
31
+ export async function estimateWorkspaceBytesFromRepo(repoPath, hints = {}) {
32
+ const cacheKey = path.resolve(repoPath);
33
+ const cached = footprintCache.get(cacheKey);
34
+ const footprint = cached && Date.now() - cached.measuredAt < footprintCacheLifetimeMs
35
+ ? cached.footprint
36
+ : await measureRepositoryWorkspace(cacheKey);
37
+ if (!cached || cached.footprint !== footprint) {
38
+ footprintCache.set(cacheKey, { measuredAt: Date.now(), footprint });
39
+ }
40
+ return calculateWorkspaceBytes({
41
+ ...footprint,
42
+ hasBrowser: footprint.hasBrowser || hints.browser === true,
43
+ serviceCount: Math.max(footprint.serviceCount, Math.max(0, hints.serviceCount ?? 0)),
44
+ }, hints.role);
45
+ }
46
+ export function calculateWorkspaceBytes(footprint, role) {
47
+ const normalizedRole = role?.trim().toLowerCase() ?? "";
48
+ if (normalizedRole === "architect") {
49
+ // Architect owns a checkout and planning artifacts, not dependencies,
50
+ // build outputs, browser state, services, or application runtime output.
51
+ const estimate = architectMinimumWorkspaceEstimateBytes
52
+ + Math.max(0, footprint.sourceBytes) * 2;
53
+ return Math.min(maximumBytes, Math.max(architectMinimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
54
+ }
55
+ let estimate = 2 * GiB + Math.max(0, footprint.sourceBytes) * 3;
56
+ if (footprint.hasNode)
57
+ estimate += 3 * GiB;
58
+ if (footprint.hasDotNet)
59
+ estimate += 2 * GiB;
60
+ if (footprint.hasBrowser)
61
+ estimate += 1536 * 1024 ** 2;
62
+ estimate += Math.max(0, footprint.serviceCount) * GiB;
63
+ if (["qa", "ci_repair", "pr_fix"].includes(normalizedRole)) {
64
+ estimate += GiB;
65
+ }
66
+ return Math.min(maximumBytes, Math.max(minimumWorkspaceEstimateBytes, alignUp(estimate, alignmentBytes)));
67
+ }
68
+ export async function measureRepositoryWorkspace(repoPath) {
69
+ return await measureGitWorkspace(repoPath) ?? await measureFilesystemFallback(repoPath);
70
+ }
71
+ async function measureGitWorkspace(repoPath) {
72
+ let sourceBytes = 0;
73
+ let hasNode = false;
74
+ let hasDotNet = false;
75
+ let hasBrowser = false;
76
+ let serviceCount = 0;
77
+ const child = spawn("git", ["-c", "core.quotepath=false", "ls-files", "--cached", "--others", "--exclude-standard"], {
78
+ cwd: repoPath,
79
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
80
+ stdio: ["ignore", "pipe", "ignore"],
81
+ windowsHide: true,
82
+ });
83
+ const closed = new Promise((resolve) => {
84
+ child.once("close", resolve);
85
+ child.once("error", () => resolve(null));
86
+ });
87
+ try {
88
+ const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
89
+ for await (const relativePath of lines) {
90
+ if (!relativePath.trim())
91
+ continue;
92
+ const file = await inspectWorkspaceFile(path.join(repoPath, relativePath));
93
+ if (!file)
94
+ continue;
95
+ sourceBytes += file.bytes;
96
+ hasNode ||= file.hasNode;
97
+ hasDotNet ||= file.hasDotNet;
98
+ hasBrowser ||= file.hasBrowser;
99
+ serviceCount = Math.max(serviceCount, file.serviceCount);
100
+ }
101
+ return await closed === 0 ? { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount } : null;
102
+ }
103
+ catch {
104
+ child.kill();
105
+ await closed;
106
+ return null;
107
+ }
108
+ }
109
+ async function measureFilesystemFallback(repoPath) {
110
+ let sourceBytes = 0;
111
+ let hasNode = false;
112
+ let hasDotNet = false;
113
+ let hasBrowser = false;
114
+ let serviceCount = 0;
115
+ const pending = [path.resolve(repoPath)];
116
+ while (pending.length > 0) {
117
+ const current = pending.pop();
118
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
119
+ for (const entry of entries) {
120
+ const entryPath = path.join(current, entry.name);
121
+ if (entry.isDirectory()) {
122
+ if (!excludedDirectories.has(entry.name.toLowerCase()))
123
+ pending.push(entryPath);
124
+ continue;
125
+ }
126
+ if (!entry.isFile())
127
+ continue;
128
+ const file = await inspectWorkspaceFile(entryPath);
129
+ if (!file)
130
+ continue;
131
+ sourceBytes += file.bytes;
132
+ hasNode ||= file.hasNode;
133
+ hasDotNet ||= file.hasDotNet;
134
+ hasBrowser ||= file.hasBrowser;
135
+ serviceCount = Math.max(serviceCount, file.serviceCount);
136
+ }
137
+ }
138
+ return { sourceBytes, hasNode, hasDotNet, hasBrowser, serviceCount };
139
+ }
140
+ async function inspectWorkspaceFile(filePath) {
141
+ const stat = await fs.stat(filePath).catch(() => null);
142
+ if (!stat?.isFile())
143
+ return null;
144
+ const name = path.basename(filePath).toLowerCase();
145
+ const sandbox = name === "forge.sandbox.yml" || name === "forge.sandbox.yaml";
146
+ return {
147
+ bytes: stat.size,
148
+ hasNode: ["package.json", "pnpm-lock.yaml", "package-lock.json", "yarn.lock"].includes(name),
149
+ hasDotNet: name.endsWith(".sln") || name.endsWith(".csproj") || name.endsWith(".fsproj"),
150
+ hasBrowser: name.startsWith("playwright.config"),
151
+ serviceCount: sandbox
152
+ ? countSandboxServices(await fs.readFile(filePath, "utf8").catch(() => ""))
153
+ : 0,
154
+ };
155
+ }
156
+ function countSandboxServices(yaml) {
157
+ let inServices = false;
158
+ let count = 0;
159
+ for (const rawLine of yaml.split(/\r?\n/gu)) {
160
+ const line = rawLine.trim();
161
+ if (!line || line.startsWith("#"))
162
+ continue;
163
+ if (!/^\s/u.test(rawLine)) {
164
+ inServices = line.toLowerCase() === "services:";
165
+ continue;
166
+ }
167
+ if (inServices && line.startsWith("-"))
168
+ count += 1;
169
+ }
170
+ return count;
171
+ }
172
+ function alignUp(value, alignment) {
173
+ return Math.ceil(value / alignment) * alignment;
174
+ }
@@ -19,23 +19,53 @@ export declare class DockerPreflight {
19
19
  private readonly cacheMs;
20
20
  private generation;
21
21
  private cached;
22
- private cacheAt;
22
+ private fullPreflightAt;
23
+ private staticPreflight;
24
+ private inspectionInFlight;
25
+ private lastCapacitySample;
23
26
  private readonly options;
24
27
  private readonly docker;
25
28
  private readonly ownership;
26
29
  private diskPressureLatched;
30
+ private readonly memoryAvailability;
27
31
  constructor(options?: DockerPreflightOptions | string, commands?: CommandRunner, cacheMs?: number);
32
+ private unavailable;
33
+ private memoryReserveBytes;
34
+ private defaultAdmissionMemoryBytes;
28
35
  current(): SandboxPreflightResult | null;
29
36
  refresh(): Promise<SandboxPreflightResult>;
30
37
  release(): Promise<void>;
31
38
  run(force?: boolean): Promise<SandboxPreflightResult>;
39
+ /**
40
+ * Refreshes only the admission inputs that change while the daemon's static
41
+ * capability snapshot remains valid. Heartbeats use this path so periodic
42
+ * liveness reporting does not wake Docker/WSL to rerun feature probes.
43
+ */
44
+ refreshCapacity(): Promise<SandboxPreflightResult>;
45
+ /**
46
+ * When the cached static preflight remains usable, produces a capacity sample
47
+ * that began after this admission request. An older inspection is awaited,
48
+ * not preempted or reused. Later admission callers may share the next sample
49
+ * only when it began after they entered this method. Without a usable static
50
+ * snapshot, the existing fail-closed result is returned without probing.
51
+ */
52
+ refreshCapacityForAdmission(): Promise<SandboxPreflightResult>;
53
+ private startInspection;
32
54
  private inspect;
55
+ private inspectCapacity;
33
56
  }
57
+ /**
58
+ * Uses the same reviewed reserve vector as host-runtime admission. The input
59
+ * is always the physical host total; Docker Desktop's VM total is tracked as
60
+ * a separate daemon commitment domain.
61
+ */
62
+ export declare function resolveSandboxHostMemoryReserveBytes(totalMemoryBytes: number, explicitBytes?: number, environment?: NodeJS.ProcessEnv): number;
34
63
  export declare function canRunDockerMaintenance(preflight: {
35
64
  readonly healthy: boolean;
36
65
  readonly unavailableCode?: string | null;
37
66
  }): boolean;
38
67
  export declare function evaluateDiskPressure(availableBytes: number, totalBytes: number, minimumAvailableBytes: number, highWatermarkPercent: number, lowWatermarkPercent: number, pressureLatched: boolean): boolean;
68
+ export declare function requiredAvailableBytesToClearDiskPressure(totalBytes: number, minimumAvailableBytes: number, lowWatermarkPercent: number): number;
39
69
  export declare function storagePolicyAvailable(storageQuota: boolean, environment?: NodeJS.ProcessEnv): boolean;
40
70
  export declare class DockerContextOwnershipLock {
41
71
  private lockPath;