@loucompanion/forge-bridge 0.1.3-dev.9bc5014a2270 → 0.1.3-dev.a58d7cd1092f

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 (168) hide show
  1. package/README.md +127 -0
  2. package/dist/bridge/bridge.base.d.ts +8 -1
  3. package/dist/bridge/bridge.service.d.ts +93 -3
  4. package/dist/bridge/bridge.service.js +1358 -47
  5. package/dist/bridge/bridge.types.d.ts +202 -1
  6. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  7. package/dist/bridge/runner-host-capabilities.js +93 -0
  8. package/dist/cleanup/cleanup.service.d.ts +14 -2
  9. package/dist/cleanup/cleanup.service.js +496 -3
  10. package/dist/cleanup/cleanup.types.d.ts +10 -0
  11. package/dist/cleanup/index.d.ts +1 -1
  12. package/dist/cleanup/index.js +1 -1
  13. package/dist/cleanup/internals.d.ts +2 -2
  14. package/dist/cleanup/internals.js +1 -1
  15. package/dist/cli/cli.help.js +47 -1
  16. package/dist/cli/cli.service.d.ts +45 -2
  17. package/dist/cli/cli.service.js +378 -26
  18. package/dist/cli/local-config.d.ts +7 -0
  19. package/dist/cli/local-config.js +51 -3
  20. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  21. package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
  22. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  23. package/dist/runtime/attempt-node-runtime-launcher.js +105 -0
  24. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  25. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  26. package/dist/runtime/attempt-process-supervisor.d.ts +97 -0
  27. package/dist/runtime/attempt-process-supervisor.js +386 -0
  28. package/dist/runtime/docker-engine-probe.d.ts +83 -0
  29. package/dist/runtime/docker-engine-probe.js +300 -0
  30. package/dist/runtime/generated-output-reset.d.ts +10 -0
  31. package/dist/runtime/generated-output-reset.js +103 -0
  32. package/dist/runtime/host-listener-inspector.d.ts +46 -0
  33. package/dist/runtime/host-listener-inspector.js +349 -0
  34. package/dist/runtime/host-port-broker.d.ts +76 -0
  35. package/dist/runtime/host-port-broker.js +333 -0
  36. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  37. package/dist/runtime/host-runtime-contract.js +700 -0
  38. package/dist/runtime/host-runtime-lease.d.ts +197 -0
  39. package/dist/runtime/host-runtime-lease.js +2203 -0
  40. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  41. package/dist/runtime/host-runtime-promotion.js +159 -0
  42. package/dist/runtime/memory-capacity.d.ts +68 -0
  43. package/dist/runtime/memory-capacity.js +121 -0
  44. package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
  45. package/dist/runtime/node-host-runtime-adapter.js +284 -0
  46. package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
  47. package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
  48. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  49. package/dist/runtime/runtime-isolation-lease.js +158 -0
  50. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  51. package/dist/runtime/workspace-resource-estimator.js +174 -0
  52. package/dist/sandbox/command-runner.d.ts +4 -0
  53. package/dist/sandbox/command-runner.js +47 -0
  54. package/dist/sandbox/contract.d.ts +5 -0
  55. package/dist/sandbox/contract.js +276 -0
  56. package/dist/sandbox/event-budget.d.ts +11 -0
  57. package/dist/sandbox/event-budget.js +27 -0
  58. package/dist/sandbox/image-policy.d.ts +35 -0
  59. package/dist/sandbox/image-policy.js +247 -0
  60. package/dist/sandbox/index.d.ts +5 -0
  61. package/dist/sandbox/index.js +5 -0
  62. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  63. package/dist/sandbox/lease-watchdog.js +67 -0
  64. package/dist/sandbox/preflight.d.ts +84 -0
  65. package/dist/sandbox/preflight.js +521 -0
  66. package/dist/sandbox/process.d.ts +20 -0
  67. package/dist/sandbox/process.js +209 -0
  68. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  69. package/dist/sandbox/provider-auth/codex.js +212 -0
  70. package/dist/sandbox/repository-lock.d.ts +5 -0
  71. package/dist/sandbox/repository-lock.js +121 -0
  72. package/dist/sandbox/runtime.d.ts +297 -0
  73. package/dist/sandbox/runtime.js +2338 -0
  74. package/dist/sandbox/sandbox.types.d.ts +93 -0
  75. package/dist/sandbox/sandbox.types.js +1 -0
  76. package/dist/sandbox/smoke.d.ts +76 -0
  77. package/dist/sandbox/smoke.js +380 -0
  78. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  79. package/dist/sandbox/worker-supervisor.js +36 -0
  80. package/dist/sandbox/worker.d.ts +1 -0
  81. package/dist/sandbox/worker.js +225 -0
  82. package/dist/sandbox/workspace.service.d.ts +13 -0
  83. package/dist/sandbox/workspace.service.js +72 -0
  84. package/dist/session/managed-attempt-tools.d.ts +21 -0
  85. package/dist/session/managed-attempt-tools.js +912 -0
  86. package/dist/session/managed-build-coordinator.d.ts +5 -0
  87. package/dist/session/managed-build-coordinator.js +105 -0
  88. package/dist/session/managed-build-receipt.d.ts +21 -0
  89. package/dist/session/managed-build-receipt.js +335 -0
  90. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  91. package/dist/session/phase-completion.artifacts.js +256 -26
  92. package/dist/session/phase-completion.d.ts +12 -2
  93. package/dist/session/phase-completion.helpers.d.ts +5 -0
  94. package/dist/session/phase-completion.helpers.js +51 -10
  95. package/dist/session/phase-completion.js +14 -3
  96. package/dist/session/phase-completion.prompts.d.ts +3 -3
  97. package/dist/session/phase-completion.prompts.js +24 -8
  98. package/dist/session/phase-completion.runtime.d.ts +13 -1
  99. package/dist/session/phase-completion.runtime.js +189 -8
  100. package/dist/session/phase-completion.schemas.js +117 -4
  101. package/dist/session/phase-completion.validation.d.ts +4 -1
  102. package/dist/session/phase-completion.validation.js +153 -22
  103. package/dist/session/pre-publication-validation.d.ts +46 -0
  104. package/dist/session/pre-publication-validation.js +208 -0
  105. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  106. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  107. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  108. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  109. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  110. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  111. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  112. package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
  113. package/dist/session/provider-cli/provider-cli.process.js +474 -23
  114. package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
  115. package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
  116. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  117. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  118. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  119. package/dist/session/provider-cli/provider-cli.service.d.ts +26 -3
  120. package/dist/session/provider-cli/provider-cli.service.js +646 -90
  121. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  122. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  123. package/dist/session/provider-cli/provider-output-state.js +91 -16
  124. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  125. package/dist/session/provider-cli/provider-usage.js +28 -5
  126. package/dist/session/session-redaction.d.ts +3 -0
  127. package/dist/session/session-redaction.js +38 -0
  128. package/dist/session/session.base.d.ts +6 -1
  129. package/dist/session/session.service.d.ts +8 -2
  130. package/dist/session/session.service.js +530 -42
  131. package/dist/session/session.types.d.ts +124 -1
  132. package/dist/session/session.types.js +231 -4
  133. package/dist/shared/format/format-bytes.d.ts +1 -0
  134. package/dist/shared/format/format-bytes.js +9 -0
  135. package/dist/shared/path/path.d.ts +1 -0
  136. package/dist/shared/path/path.js +21 -1
  137. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  138. package/dist/shared/process/dotnet-toolchain.js +179 -0
  139. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  140. package/dist/shared/process/host-process-recovery.js +78 -0
  141. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  142. package/dist/shared/process/host-process-supervisor.js +330 -0
  143. package/dist/shared/process/process.d.ts +1 -0
  144. package/dist/shared/process/process.js +1 -0
  145. package/dist/shared/process/windows-launch.d.ts +1 -1
  146. package/dist/shared/process/windows-launch.js +6 -3
  147. package/dist/shared/provider-capabilities.d.ts +1 -0
  148. package/dist/shared/provider-capabilities.js +3 -0
  149. package/dist/shared/redaction/redaction.js +16 -4
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/dist/worktree/artifact-injector.d.ts +3 -2
  153. package/dist/worktree/artifact-injector.js +43 -4
  154. package/dist/worktree/dependency-warmer.d.ts +36 -2
  155. package/dist/worktree/dependency-warmer.js +347 -11
  156. package/dist/worktree/index.d.ts +2 -1
  157. package/dist/worktree/index.js +1 -0
  158. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  159. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  160. package/dist/worktree/worktree-paths.d.ts +4 -0
  161. package/dist/worktree/worktree-paths.js +30 -0
  162. package/dist/worktree/worktree.base.d.ts +3 -0
  163. package/dist/worktree/worktree.service.d.ts +13 -2
  164. package/dist/worktree/worktree.service.js +238 -47
  165. package/dist/worktree/worktree.types.d.ts +5 -1
  166. package/package.json +8 -4
  167. package/runtime/windows-job-supervisor.ps1 +444 -0
  168. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,12 +1,129 @@
1
- import { redactSecrets } from "../shared/redaction/redaction.js";
1
+ import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "./session-redaction.js";
2
2
  import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.js";
3
3
  import { GitWorktreeService } from "../worktree/worktree.service.js";
4
+ import { SandboxWorkspaceService } from "../sandbox/workspace.service.js";
5
+ import { HostRuntimeAdmissionExpiredError, hostRuntimeEventResources, } from "../runtime/host-runtime-lease.js";
6
+ import { configuredLaunchPortNames, hostRuntimeContractFingerprint, hostRuntimeParallelEligible, } from "../runtime/host-runtime-promotion.js";
7
+ const minimumReadinessProbeStartupBudgetMs = 150_000;
4
8
  export class BridgeSessionService {
5
9
  worktrees;
6
10
  providerCli;
7
- constructor(worktrees = new GitWorktreeService(), providerCli = new RoutingProviderCliService()) {
11
+ hostRuntime;
12
+ constructor(worktrees = process.env.FORGE_SANDBOX_ID
13
+ ? new SandboxWorkspaceService()
14
+ : new GitWorktreeService(), providerCli = new RoutingProviderCliService(), hostRuntime) {
8
15
  this.worktrees = worktrees;
9
16
  this.providerCli = providerCli;
17
+ this.hostRuntime = hostRuntime;
18
+ }
19
+ async updateLease(dispatchAttemptId, fencingToken, expiresAt) {
20
+ await this.hostRuntime?.updateLease(dispatchAttemptId, fencingToken, expiresAt);
21
+ }
22
+ async reapHostRuntime(options) {
23
+ return await this.hostRuntime?.reap?.(options) ?? null;
24
+ }
25
+ runtimeCapacity() {
26
+ return this.hostRuntime?.snapshot?.() ?? null;
27
+ }
28
+ async probeHostRuntime(request, signal) {
29
+ if (!this.hostRuntime) {
30
+ return failedProbe("host_runtime_unavailable", "This bridge does not provide managed host runtime isolation.");
31
+ }
32
+ if (this.hostRuntime.hasForegroundDemand?.()) {
33
+ return failedProbe("host_runtime_probe_deferred", "Forge deferred the background readiness probe because a user lane is active, reserved, or waiting on this runner.");
34
+ }
35
+ const dispatch = readinessProbeDispatch(request);
36
+ let worktree = null;
37
+ let admissionAcquired = false;
38
+ let runtimeStarted = false;
39
+ let result;
40
+ let cleanupFailure = null;
41
+ try {
42
+ throwIfAborted(signal);
43
+ await this.hostRuntime.acquire(dispatch, signal);
44
+ admissionAcquired = true;
45
+ // Readiness probes acquire background-exclusive project admission. Only
46
+ // invalidate the prior proof after that fence is held so a user lane can
47
+ // never observe the proof disappear underneath shared admission.
48
+ await this.hostRuntime.invalidatePromotion?.(request.repoId);
49
+ assertReadinessProbeStartupBudget(request.expiresAt);
50
+ worktree = await this.worktrees.prepare({
51
+ repo: dispatch.repo,
52
+ workItem: dispatch.workItem,
53
+ baseBranch: request.revision,
54
+ worktreeKey: dispatch.lease.worktreeKey,
55
+ ownerType: "project",
56
+ ownerId: request.projectId,
57
+ lane: "runtime_readiness",
58
+ phase: "readiness_probe",
59
+ allowedRepoRoot: request.repoRoot,
60
+ // Readiness probes are disposable infrastructure checks, not agent lanes.
61
+ // Always remove their worktree even when the bridge is configured to retain
62
+ // failed agent worktrees for inspection.
63
+ cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
64
+ artifacts: [],
65
+ });
66
+ assertReadinessProbeStartupBudget(request.expiresAt);
67
+ let started = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal);
68
+ runtimeStarted = true;
69
+ const discovery = started.runtimeContract.discovery;
70
+ if (!hostRuntimeParallelEligible(started.runtimeContract)) {
71
+ result = {
72
+ status: "serialized",
73
+ runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
74
+ runtimeContractSource: started.runtimeContract.source,
75
+ reasonCode: discovery?.reasonCode ?? "host_runtime_contract_serialized",
76
+ reasonMessage: discovery?.summary ?? "The detected runtime requires serialized host execution.",
77
+ checkedAt: new Date().toISOString(),
78
+ };
79
+ }
80
+ else {
81
+ assertReadinessProbeStartupBudget(request.expiresAt);
82
+ started = await this.hostRuntime.startConfiguredProject(request.requestId, request.fencingToken, signal);
83
+ const requiredPorts = configuredLaunchPortNames(started.runtimeContract);
84
+ const verifiedPorts = started.lease.resources.ports.map(port => port.name);
85
+ const verified = requiredPorts.length > 0 && requiredPorts.every(name => verifiedPorts.includes(name));
86
+ result = verified
87
+ ? {
88
+ status: "parallel_ready",
89
+ runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
90
+ runtimeContractSource: started.runtimeContract.source,
91
+ reasonCode: null,
92
+ reasonMessage: null,
93
+ qaAppLaunchTarget: managedQaLaunchTarget(started.runtimeContract),
94
+ checkedAt: new Date().toISOString(),
95
+ }
96
+ : failedProbe("host_runtime_probe_unverified", "The application did not prove ownership and readiness on every Forge-assigned port.");
97
+ }
98
+ }
99
+ catch (error) {
100
+ result = failedProbe(probeFailureCode(error), probeFailureMessage(error));
101
+ }
102
+ finally {
103
+ if (runtimeStarted || admissionAcquired) {
104
+ try {
105
+ const released = await this.hostRuntime.release(request.requestId, request.fencingToken);
106
+ if (runtimeStarted && !released)
107
+ cleanupFailure = new Error("The runtime lease was no longer current during cleanup.");
108
+ }
109
+ catch (error) {
110
+ cleanupFailure = error instanceof Error ? error : new Error(String(error));
111
+ }
112
+ }
113
+ if (worktree) {
114
+ try {
115
+ await this.worktrees.release(worktree);
116
+ }
117
+ catch (error) {
118
+ cleanupFailure ??= error instanceof Error ? error : new Error(String(error));
119
+ }
120
+ }
121
+ }
122
+ if (cleanupFailure) {
123
+ await this.hostRuntime.invalidatePromotion?.(request.repoId).catch(() => undefined);
124
+ return failedProbe("host_runtime_cleanup_degraded", `Forge could not prove exact readiness cleanup: ${cleanupFailure.message}`);
125
+ }
126
+ return result;
10
127
  }
11
128
  async runOnce(dispatch, sink, signal) {
12
129
  validateApiKeySecretGrant(dispatch);
@@ -16,7 +133,14 @@ export class BridgeSessionService {
16
133
  if (signal?.aborted) {
17
134
  return;
18
135
  }
19
- const redacted = redactSecrets(event, redactionSecrets);
136
+ const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
137
+ if (redacted.eventType !== "provider_tool_heartbeat") {
138
+ events.push(redacted);
139
+ }
140
+ await sink.emit(redacted);
141
+ };
142
+ const emitLifecycle = async (event) => {
143
+ const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
20
144
  events.push(redacted);
21
145
  await sink.emit(redacted);
22
146
  };
@@ -27,44 +151,165 @@ export class BridgeSessionService {
27
151
  content: "forge-bridge accepted run-once dispatch.",
28
152
  eventType: "dispatch_accepted"
29
153
  });
30
- const worktree = await this.worktrees.prepare({
31
- repo: dispatch.repo,
32
- workItem: dispatch.workItem,
33
- baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
34
- worktreeKey: dispatch.lease.worktreeKey,
35
- ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
36
- ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
37
- lane: dispatch.execution.lane,
38
- phase: dispatch.cycleRun.phase,
39
- allowedRepoRoot: dispatch.serverLocal.repoRoot,
40
- cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
41
- artifacts: dispatch.artifacts,
42
- qaRework: dispatch.qaRework,
43
- warmPathOverrides: dispatch.warmPathOverrides
44
- });
45
- const providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
46
- throwIfAborted(signal);
47
- await emit({
48
- role: "runner",
49
- kind: "checkpoint",
50
- content: `Prepared worktree ${worktree.branchName}.`,
51
- eventType: "worktree_prepared"
52
- });
53
- if (worktree.dependencyWarm) {
54
- await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
55
- }
154
+ let worktree = null;
155
+ let hostSingletonKeys = null;
156
+ let hostLease = null;
56
157
  try {
158
+ if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
159
+ const capacity = this.hostRuntime.snapshot?.() ?? null;
160
+ const projectCapacity = capacity?.projects.find(project => project.repoId === dispatch.repo.id) ?? null;
161
+ await emitLifecycle({
162
+ role: "runner",
163
+ kind: "checkpoint",
164
+ content: capacity
165
+ ? `Waiting for runner capacity before preparing the worktree (queue position ${capacity.waitingAttempts + 1}; runner ${capacity.activeAttempts} active, ${capacity.reservedAttempts} reserved; project ${projectCapacity?.activeAttempts ?? 0} active, ${projectCapacity?.waitingAttempts ?? 0} already waiting). Forge will continue automatically when admission is available.`
166
+ : "Waiting for runner capacity before preparing the worktree. Forge will continue automatically when admission is available.",
167
+ eventType: "host_runtime_admission_waiting",
168
+ data: capacity ? JSON.stringify({
169
+ queue_position: capacity.waitingAttempts + 1,
170
+ runner_active_attempts: capacity.activeAttempts,
171
+ runner_reserved_attempts: capacity.reservedAttempts,
172
+ runner_waiting_attempts: capacity.waitingAttempts,
173
+ project_active_attempts: projectCapacity?.activeAttempts ?? 0,
174
+ project_reserved_attempts: projectCapacity?.reservedAttempts ?? 0,
175
+ project_waiting_attempts: projectCapacity?.waitingAttempts ?? 0,
176
+ project_safety_class: projectCapacity?.safetyClass ?? "unknown",
177
+ }) : undefined,
178
+ });
179
+ try {
180
+ hostSingletonKeys = await this.hostRuntime.acquire(dispatch, signal);
181
+ }
182
+ catch (error) {
183
+ if (error instanceof HostRuntimeAdmissionExpiredError) {
184
+ await emitLifecycle({
185
+ role: "runner",
186
+ kind: "error",
187
+ content: "Managed host admission expired while the project singleton remained busy; no worktree or runtime resources were created.",
188
+ eventType: "host_runtime_admission_expired",
189
+ }).catch(() => undefined);
190
+ }
191
+ throw error;
192
+ }
193
+ throwIfAborted(signal);
194
+ await emitLifecycle({
195
+ role: "runner",
196
+ kind: "checkpoint",
197
+ content: hostSingletonKeys.length > 0
198
+ ? `Acquired managed host singleton ${hostSingletonKeys.join(", ")}.`
199
+ : "Observed assigned-port evidence permits provisional parallel host admission; the prepared runtime contract must still match before launch.",
200
+ eventType: "host_runtime_admission_acquired",
201
+ });
202
+ }
203
+ try {
204
+ worktree = await this.worktrees.prepare({
205
+ repo: dispatch.repo,
206
+ workItem: dispatch.workItem,
207
+ baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
208
+ worktreeKey: dispatch.lease.worktreeKey,
209
+ ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
210
+ ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
211
+ lane: dispatch.execution.lane,
212
+ phase: dispatch.cycleRun.phase,
213
+ allowedRepoRoot: dispatch.serverLocal.repoRoot,
214
+ cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
215
+ artifacts: dispatch.artifacts,
216
+ priorPhaseArtifacts: dispatch.priorPhaseArtifacts,
217
+ qaRework: dispatch.qaRework,
218
+ warmPathOverrides: dispatch.warmPathOverrides,
219
+ applicationRuntimeRequired: dispatch.runtimeIntent.applicationRuntimeRequired,
220
+ preserveExistingState: Boolean(dispatch.session.resumedFromSessionId)
221
+ });
222
+ }
223
+ catch (error) {
224
+ throw new Error(`worktree_prepare_failed: ${failureMessage(error)}`, { cause: error });
225
+ }
226
+ let providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
227
+ throwIfAborted(signal);
228
+ await emit({
229
+ role: "runner",
230
+ kind: "checkpoint",
231
+ content: dispatch.session.resumedFromSessionId
232
+ ? `Reattached to worktree ${worktree.branchName} without resetting source or clearing attempt artifacts.`
233
+ : `Prepared worktree ${worktree.branchName}.`,
234
+ eventType: dispatch.session.resumedFromSessionId ? "worktree_reattached" : "worktree_prepared"
235
+ });
236
+ if ((worktree.hydratedPriorPhaseArtifactCount ?? 0) > 0) {
237
+ await emit({
238
+ role: "runner",
239
+ kind: "checkpoint",
240
+ content: `Hydrated ${worktree.hydratedPriorPhaseArtifactCount} persisted prior-phase artifact(s) into the prepared worktree.`,
241
+ eventType: "prior_phase_artifacts_hydrated"
242
+ });
243
+ }
244
+ if (worktree.dependencyWarm) {
245
+ await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
246
+ }
247
+ if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
248
+ hostLease = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal, async (checkpoint) => {
249
+ await emitLifecycle({
250
+ role: "runner",
251
+ kind: "checkpoint",
252
+ content: checkpoint.message,
253
+ eventType: checkpoint.code,
254
+ data: JSON.stringify({ expires_at: checkpoint.expiresAt }),
255
+ });
256
+ });
257
+ await emitLifecycle({
258
+ role: "runner",
259
+ kind: "checkpoint",
260
+ content: `Host runtime lease ${hostLease.lease.leaseId} is active for this worktree.`,
261
+ eventType: "sandbox_started",
262
+ occurredAt: hostLease.lease.stateChangedAt,
263
+ data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
264
+ });
265
+ await emitLifecycle({
266
+ role: "runner",
267
+ kind: "checkpoint",
268
+ content: "Managed host fallback isolates the worktree, attempt paths, assigned loopback ports, and supervised process trees, but not the host filesystem, network, credentials, or kernel. Serialized projects also hold a runner-local singleton. Enable Run in Docker in Project Settings when stronger isolation is required.",
269
+ eventType: "host_runtime_isolation_notice",
270
+ data: JSON.stringify({
271
+ isolationStrength: "managed_host_process",
272
+ remediation: "Enable Run in Docker in Project Settings.",
273
+ }),
274
+ });
275
+ const unconfiguredPortCount = hostLease.lease.resources.ports.length;
276
+ hostLease = await this.hostRuntime.startConfiguredProject(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken, signal, async (checkpoint) => {
277
+ await emitLifecycle({
278
+ role: "runner",
279
+ kind: "checkpoint",
280
+ content: checkpoint.message,
281
+ eventType: checkpoint.code,
282
+ data: checkpoint.data ? JSON.stringify(checkpoint.data) : undefined,
283
+ });
284
+ });
285
+ if (hostLease.lease.resources.ports.length > unconfiguredPortCount) {
286
+ const assigned = hostLease.lease.resources.ports.at(-1);
287
+ await emitLifecycle({
288
+ role: "runner",
289
+ kind: "checkpoint",
290
+ content: `Verified supervised project runtime ${assigned.name} on ${assigned.hostIp}:${assigned.hostPort}.`,
291
+ eventType: "host_runtime_project_ready",
292
+ data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
293
+ });
294
+ }
295
+ }
296
+ providerDispatch = withMaterializedManagedQaTarget(providerDispatch, hostLease);
57
297
  if (dispatch.draft) {
58
298
  throwIfAborted(signal);
299
+ const processRunner = hostLease && dispatch.sandbox
300
+ ? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
301
+ : undefined;
59
302
  const run = await this.providerCli.runDraft({
60
303
  dispatch: providerDispatch,
61
304
  worktree,
305
+ runtimeEnvironment: hostLease?.environment,
306
+ processRunner,
62
307
  emit,
63
308
  signal
64
309
  });
65
310
  throwIfAborted(signal);
66
311
  const draftArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
67
- return redactSecrets({
312
+ return redactRunOnceResultPacket({
68
313
  schemaVersion: 1,
69
314
  exitCode: 0,
70
315
  summary: run.summary,
@@ -75,27 +320,46 @@ export class BridgeSessionService {
75
320
  }, redactionSecrets);
76
321
  }
77
322
  throwIfAborted(signal);
78
- const lane = dispatch.execution.lane.toLowerCase();
79
- const role = dispatch.execution.role.toLowerCase();
323
+ const lane = dispatch.execution.lane.trim().toLowerCase();
324
+ const role = dispatch.execution.role.trim().toLowerCase();
325
+ const cycleKind = dispatch.cycleRun.kind.trim().toLowerCase();
80
326
  const isQa = role === "qa" || lane === "qa";
81
- const isArchitect = role === "architect" || lane === "architect" || dispatch.cycleRun.kind.toLowerCase() === "architect";
327
+ const isArchitect = role === "architect" || lane === "architect" || cycleKind === "architect";
328
+ const isCiRepair = role === "ci_repair" || lane === "ci_repair" || cycleKind === "pr_checks";
82
329
  const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
83
330
  const context = {
84
331
  dispatch: providerDispatch,
85
332
  worktree,
333
+ runtimeEnvironment: hostLease?.environment,
334
+ processRunner: hostLease && dispatch.sandbox
335
+ ? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
336
+ : undefined,
86
337
  emit,
87
338
  signal
88
339
  };
89
- const run = isQa
90
- ? await this.providerCli.runQa(context)
91
- : isArchitect
92
- ? await this.providerCli.runArchitect(context)
93
- : isConflictResolve
94
- ? await this.providerCli.runConflictResolve(context)
95
- : await this.providerCli.runImplementation(context);
340
+ let run;
341
+ try {
342
+ run = isQa
343
+ ? await this.providerCli.runQa(context)
344
+ : isArchitect
345
+ ? await this.providerCli.runArchitect(context)
346
+ : isCiRepair
347
+ ? await this.providerCli.runCiRepair(context)
348
+ : isConflictResolve
349
+ ? await this.providerCli.runConflictResolve(context)
350
+ : await this.providerCli.runImplementation(context);
351
+ }
352
+ catch (error) {
353
+ const message = failureMessage(error);
354
+ if (message.includes("attempt_process_supervisor_start_")
355
+ || message.includes("host_process_supervisor_start_")) {
356
+ throw new Error(`provider_start_unavailable: ${message}`, { cause: error });
357
+ }
358
+ throw error;
359
+ }
96
360
  throwIfAborted(signal);
97
361
  const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
98
- return redactSecrets({
362
+ return redactRunOnceResultPacket({
99
363
  schemaVersion: 1,
100
364
  exitCode: 0,
101
365
  summary: run.summary,
@@ -106,10 +370,217 @@ export class BridgeSessionService {
106
370
  }, redactionSecrets);
107
371
  }
108
372
  finally {
109
- await this.worktrees.release(worktree);
373
+ if (hostLease && dispatch.sandbox) {
374
+ const providerMemory = this.hostRuntime?.providerMemoryTelemetry?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
375
+ if (providerMemory) {
376
+ await emitLifecycle({
377
+ role: "runner",
378
+ kind: "checkpoint",
379
+ content: `Observed managed-host provider process-tree peak memory ${formatMemoryBytes(providerMemory.peakMemoryBytes)} from ${providerMemory.sampleCount} sample${providerMemory.sampleCount === 1 ? "" : "s"}.`,
380
+ eventType: "sandbox_peak_memory_observed",
381
+ occurredAt: providerMemory.observedAt,
382
+ data: JSON.stringify({
383
+ peakMemoryBytes: providerMemory.peakMemoryBytes,
384
+ sampleCount: providerMemory.sampleCount,
385
+ source: providerMemory.source,
386
+ containers: [{
387
+ role: "agent",
388
+ peakMemoryBytes: providerMemory.peakMemoryBytes,
389
+ }],
390
+ }),
391
+ }).catch(() => undefined);
392
+ }
393
+ const teardownAt = new Date().toISOString();
394
+ await emitLifecycle({
395
+ role: "runner",
396
+ kind: "checkpoint",
397
+ content: `Releasing host runtime lease ${hostLease.lease.leaseId}.`,
398
+ eventType: "sandbox_teardown_started",
399
+ occurredAt: teardownAt,
400
+ }).catch(() => undefined);
401
+ try {
402
+ const released = await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
403
+ if (!released) {
404
+ await emitLifecycle({
405
+ role: "runner",
406
+ kind: "error",
407
+ content: "Host runtime cleanup requires reconciliation: the runtime lease was not current during release.",
408
+ eventType: "teardown_failed",
409
+ occurredAt: new Date().toISOString(),
410
+ }).catch(() => undefined);
411
+ }
412
+ else {
413
+ await emitLifecycle({
414
+ role: "runner",
415
+ kind: "checkpoint",
416
+ content: `Released host runtime lease ${released.leaseId}.`,
417
+ eventType: "sandbox_teardown_completed",
418
+ occurredAt: released.releasedAt,
419
+ }).catch(() => undefined);
420
+ }
421
+ }
422
+ catch (error) {
423
+ await emitLifecycle({
424
+ role: "runner",
425
+ kind: "error",
426
+ content: `Host runtime cleanup requires reconciliation: ${error instanceof Error ? error.message : String(error)}`,
427
+ eventType: "teardown_failed",
428
+ occurredAt: new Date().toISOString(),
429
+ }).catch(() => undefined);
430
+ }
431
+ }
432
+ if (!hostLease && hostSingletonKeys && dispatch.sandbox) {
433
+ await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken).catch(() => undefined);
434
+ }
435
+ if (worktree && !isPauseCancellation(signal))
436
+ await this.worktrees.release(worktree);
110
437
  }
111
438
  }
112
439
  }
440
+ function failureMessage(error) {
441
+ return error instanceof Error ? error.message : String(error);
442
+ }
443
+ function formatMemoryBytes(bytes) {
444
+ return `${(bytes / (1024 ** 2)).toFixed(bytes >= 1024 ** 3 ? 0 : 1)} MiB`;
445
+ }
446
+ function isPauseCancellation(signal) {
447
+ return signal?.aborted === true
448
+ && String(signal.reason ?? "").trim().toLowerCase() === "session pause requested";
449
+ }
450
+ function readinessProbeDispatch(request) {
451
+ return {
452
+ schemaVersion: 2,
453
+ userId: request.userId,
454
+ workItem: {
455
+ id: request.projectId,
456
+ kind: "runtime_readiness",
457
+ title: "Automatic host-isolation readiness check",
458
+ description: "Internal readiness proof without a provider agent.",
459
+ acceptanceCriteria: "",
460
+ reproSteps: "",
461
+ systemInfo: "",
462
+ },
463
+ repo: {
464
+ id: request.repoId,
465
+ name: request.repoName,
466
+ remoteUrl: request.remoteUrl,
467
+ defaultBranch: request.defaultBranch,
468
+ cloneOnDemand: request.cloneOnDemand,
469
+ serverLocalPath: request.runnerRepoLocalPath,
470
+ },
471
+ cycleRun: {
472
+ id: request.requestId,
473
+ ownerType: "project",
474
+ ownerId: request.projectId,
475
+ kind: "runtime_readiness",
476
+ phase: "readiness_probe",
477
+ requestedBase: request.revision,
478
+ baseBranch: request.revision,
479
+ attempt: request.attempt,
480
+ model: "none",
481
+ },
482
+ session: { id: request.requestId, model: "none", lane: "runtime_readiness" },
483
+ lease: {
484
+ id: request.requestId,
485
+ ownerType: "project",
486
+ ownerId: request.projectId,
487
+ worktreeKey: `runtime-readiness-${request.requestId}`,
488
+ lane: "runtime_readiness",
489
+ fencingToken: request.fencingToken,
490
+ expiresAt: request.expiresAt,
491
+ },
492
+ execution: {
493
+ provider: "none",
494
+ model: "none",
495
+ authKind: "none",
496
+ location: "bridge",
497
+ runnerId: request.runnerId,
498
+ repoId: request.repoId,
499
+ role: "runtime_readiness",
500
+ lane: "runtime_readiness",
501
+ requiresApiKeySecretGrant: false,
502
+ runnerRepoLocalPath: request.runnerRepoLocalPath,
503
+ },
504
+ runtimeIntent: { executionClass: "managed_runtime", applicationRuntimeRequired: true },
505
+ serverLocal: { repoRoot: request.repoRoot, cleanupWorktrees: true },
506
+ sandbox: {
507
+ dispatchAttemptId: request.requestId,
508
+ executionLeaseId: request.requestId,
509
+ runnerInstanceId: request.runnerInstanceId,
510
+ executionMode: "host",
511
+ executionPolicy: "host_allowed",
512
+ networkPolicy: { mode: "offline", allowedHosts: [] },
513
+ requestedResources: {
514
+ sandboxSlots: 1,
515
+ cpuMillis: 2000,
516
+ workerMemoryLimitBytes: 1024 * 1024 * 1024,
517
+ serviceMemoryLimitBytes: 1024 * 1024 * 1024,
518
+ runtimeOverheadBytes: 256 * 1024 * 1024,
519
+ memoryBytes: 2 * 1024 * 1024 * 1024,
520
+ workspaceBytes: 10 * 1024 * 1024 * 1024,
521
+ pids: 512,
522
+ buildSlots: 1,
523
+ },
524
+ publicationMode: "shared_filesystem",
525
+ },
526
+ artifacts: [],
527
+ };
528
+ }
529
+ function failedProbe(reasonCode, reasonMessage) {
530
+ return { status: "failed", reasonCode, reasonMessage, checkedAt: new Date().toISOString() };
531
+ }
532
+ function assertReadinessProbeStartupBudget(expiresAt) {
533
+ const remainingMs = Date.parse(expiresAt) - Date.now();
534
+ if (!Number.isFinite(remainingMs) || remainingMs < minimumReadinessProbeStartupBudgetMs) {
535
+ throw new Error("host_runtime_probe_deadline_insufficient: readiness admission completed too close to the probe deadline; Forge released the reservation before launching the application.");
536
+ }
537
+ }
538
+ function managedQaLaunchTarget(runtimeContract) {
539
+ const playwright = runtimeContract.contract.runtime.playwright;
540
+ if (!playwright)
541
+ return null;
542
+ const port = runtimeContract.contract.runtime.ports.find(candidate => candidate.name === playwright.baseUrlPort);
543
+ return port?.launch && port.baseUrlEnv && port.format
544
+ ? { managedRuntimePort: port.name, routes: ["/"] }
545
+ : null;
546
+ }
547
+ function withMaterializedManagedQaTarget(dispatch, hostLease) {
548
+ const target = dispatch.qaBrowser?.launchTarget;
549
+ const portName = target?.managed_runtime_port?.trim().toLowerCase();
550
+ if (!target || !portName)
551
+ return dispatch;
552
+ if (!hostLease) {
553
+ throw new Error("qa_managed_runtime_unavailable: Browser QA requires the verified managed host runtime selected during readiness.");
554
+ }
555
+ const normalizedName = portName.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_");
556
+ const url = hostLease.environment[`FORGE_${normalizedName}_URL`];
557
+ if (!url) {
558
+ throw new Error(`qa_managed_runtime_unavailable: verified runtime port '${portName}' was not launched for this attempt.`);
559
+ }
560
+ return {
561
+ ...dispatch,
562
+ qaBrowser: dispatch.qaBrowser ? {
563
+ ...dispatch.qaBrowser,
564
+ launchTarget: {
565
+ ...target,
566
+ url: target.url ?? url,
567
+ ready_url: target.ready_url ?? url,
568
+ notes: [target.notes, "Forge launched and verified this URL inside the current fenced attempt."]
569
+ .filter(Boolean)
570
+ .join(" "),
571
+ },
572
+ } : dispatch.qaBrowser,
573
+ };
574
+ }
575
+ function probeFailureCode(error) {
576
+ const message = error instanceof Error ? error.message : String(error);
577
+ const candidate = message.split(":", 1)[0]?.trim().toLowerCase() ?? "";
578
+ return /^[a-z][a-z0-9_.-]{2,159}$/u.test(candidate) ? candidate : "host_runtime_probe_failed";
579
+ }
580
+ function probeFailureMessage(error) {
581
+ const message = error instanceof Error ? error.message : String(error);
582
+ return message.length > 1000 ? `${message.slice(0, 997)}...` : message;
583
+ }
113
584
  function throwIfAborted(signal) {
114
585
  if (signal?.aborted) {
115
586
  throw new Error(String(signal.reason ?? "Dispatch cancelled."));
@@ -150,6 +621,23 @@ function withPreparedQaReworkPacket(dispatch, packetPath) {
150
621
  };
151
622
  }
152
623
  async function emitDependencyWarmEvents(warm, emit) {
624
+ for (const receipt of warm.receipts) {
625
+ await emit({
626
+ role: "runner",
627
+ kind: "checkpoint",
628
+ content: receipt.cacheHit
629
+ ? `Reused verified ${receipt.ecosystem} dependency preparation receipt ${receipt.fingerprint} in ${receipt.packageRoot}.`
630
+ : `Published ${receipt.ecosystem} dependency preparation receipt ${receipt.fingerprint} in ${receipt.packageRoot}.`,
631
+ eventType: receipt.cacheHit ? "dependencies_preparation_cached" : "dependencies_preparation_receipt_published",
632
+ data: JSON.stringify({
633
+ ecosystem: receipt.ecosystem,
634
+ package_root: receipt.packageRoot,
635
+ cache_hit: receipt.cacheHit,
636
+ fingerprint: receipt.fingerprint,
637
+ receipt_path: receipt.receiptPath,
638
+ }),
639
+ });
640
+ }
153
641
  if (warm.linked.length > 0) {
154
642
  await emit({
155
643
  role: "runner",