@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.eeefdb0d19c1

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 (118) hide show
  1. package/README.md +34 -0
  2. package/dist/bridge/bridge.base.d.ts +2 -1
  3. package/dist/bridge/bridge.service.d.ts +22 -3
  4. package/dist/bridge/bridge.service.js +416 -40
  5. package/dist/bridge/bridge.types.d.ts +59 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +37 -3
  14. package/dist/cli/cli.service.d.ts +30 -2
  15. package/dist/cli/cli.service.js +182 -23
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +41 -1
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  23. package/dist/runtime/attempt-process-supervisor.js +343 -0
  24. package/dist/runtime/generated-output-reset.d.ts +10 -0
  25. package/dist/runtime/generated-output-reset.js +103 -0
  26. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  27. package/dist/runtime/host-listener-inspector.js +324 -0
  28. package/dist/runtime/host-port-broker.d.ts +76 -0
  29. package/dist/runtime/host-port-broker.js +333 -0
  30. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  31. package/dist/runtime/host-runtime-contract.js +700 -0
  32. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  33. package/dist/runtime/host-runtime-lease.js +1460 -0
  34. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  35. package/dist/runtime/host-runtime-promotion.js +159 -0
  36. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  37. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  38. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  40. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  41. package/dist/runtime/runtime-isolation-lease.js +158 -0
  42. package/dist/sandbox/preflight.d.ts +14 -0
  43. package/dist/sandbox/preflight.js +91 -12
  44. package/dist/sandbox/process.js +125 -40
  45. package/dist/sandbox/repository-lock.js +37 -1
  46. package/dist/sandbox/runtime.d.ts +66 -3
  47. package/dist/sandbox/runtime.js +670 -105
  48. package/dist/sandbox/sandbox.types.d.ts +3 -0
  49. package/dist/sandbox/smoke.d.ts +76 -0
  50. package/dist/sandbox/smoke.js +380 -0
  51. package/dist/sandbox/worker-supervisor.js +1 -1
  52. package/dist/sandbox/worker.js +2 -5
  53. package/dist/sandbox/workspace.service.d.ts +1 -0
  54. package/dist/sandbox/workspace.service.js +28 -5
  55. package/dist/session/managed-attempt-tools.d.ts +12 -0
  56. package/dist/session/managed-attempt-tools.js +350 -0
  57. package/dist/session/phase-completion.artifacts.js +9 -2
  58. package/dist/session/phase-completion.d.ts +4 -2
  59. package/dist/session/phase-completion.helpers.js +27 -3
  60. package/dist/session/phase-completion.js +12 -3
  61. package/dist/session/phase-completion.prompts.d.ts +3 -3
  62. package/dist/session/phase-completion.prompts.js +19 -5
  63. package/dist/session/phase-completion.runtime.d.ts +12 -1
  64. package/dist/session/phase-completion.runtime.js +127 -7
  65. package/dist/session/phase-completion.schemas.js +108 -3
  66. package/dist/session/phase-completion.validation.d.ts +4 -1
  67. package/dist/session/phase-completion.validation.js +106 -2
  68. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  69. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  70. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  71. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  72. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  73. package/dist/session/provider-cli/provider-cli.process.d.ts +16 -0
  74. package/dist/session/provider-cli/provider-cli.process.js +212 -25
  75. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  76. package/dist/session/provider-cli/provider-cli.prompts.js +84 -17
  77. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  78. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  79. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  80. package/dist/session/provider-cli/provider-cli.service.d.ts +3 -2
  81. package/dist/session/provider-cli/provider-cli.service.js +254 -71
  82. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  83. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  84. package/dist/session/provider-cli/provider-output-state.js +30 -2
  85. package/dist/session/session-redaction.d.ts +3 -0
  86. package/dist/session/session-redaction.js +23 -0
  87. package/dist/session/session.base.d.ts +6 -1
  88. package/dist/session/session.service.d.ts +8 -2
  89. package/dist/session/session.service.js +436 -41
  90. package/dist/session/session.types.d.ts +55 -0
  91. package/dist/session/session.types.js +69 -2
  92. package/dist/shared/path/path.js +3 -1
  93. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  94. package/dist/shared/process/host-process-recovery.js +78 -0
  95. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  96. package/dist/shared/process/host-process-supervisor.js +282 -0
  97. package/dist/shared/process/process.d.ts +1 -0
  98. package/dist/shared/process/process.js +1 -0
  99. package/dist/shared/redaction/redaction.js +6 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/worktree/artifact-injector.d.ts +3 -2
  103. package/dist/worktree/artifact-injector.js +43 -4
  104. package/dist/worktree/dependency-warmer.d.ts +10 -1
  105. package/dist/worktree/dependency-warmer.js +94 -5
  106. package/dist/worktree/index.d.ts +1 -0
  107. package/dist/worktree/index.js +1 -0
  108. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  109. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  110. package/dist/worktree/worktree-paths.d.ts +4 -0
  111. package/dist/worktree/worktree-paths.js +30 -0
  112. package/dist/worktree/worktree.base.d.ts +3 -0
  113. package/dist/worktree/worktree.service.d.ts +7 -2
  114. package/dist/worktree/worktree.service.js +72 -44
  115. package/dist/worktree/worktree.types.d.ts +3 -1
  116. package/package.json +3 -2
  117. package/runtime/windows-job-supervisor.ps1 +356 -0
  118. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,15 +1,120 @@
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
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;
5
8
  export class BridgeSessionService {
6
9
  worktrees;
7
10
  providerCli;
11
+ hostRuntime;
8
12
  constructor(worktrees = process.env.FORGE_SANDBOX_ID
9
13
  ? new SandboxWorkspaceService()
10
- : new GitWorktreeService(), providerCli = new RoutingProviderCliService()) {
14
+ : new GitWorktreeService(), providerCli = new RoutingProviderCliService(), hostRuntime) {
11
15
  this.worktrees = worktrees;
12
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
+ const dispatch = readinessProbeDispatch(request);
33
+ let worktree = null;
34
+ let admissionAcquired = false;
35
+ let runtimeStarted = false;
36
+ let result;
37
+ let cleanupFailure = null;
38
+ try {
39
+ throwIfAborted(signal);
40
+ await this.hostRuntime.invalidatePromotion?.(request.repoId);
41
+ await this.hostRuntime.acquire(dispatch, signal);
42
+ admissionAcquired = true;
43
+ assertReadinessProbeStartupBudget(request.expiresAt);
44
+ worktree = await this.worktrees.prepare({
45
+ repo: dispatch.repo,
46
+ workItem: dispatch.workItem,
47
+ baseBranch: request.revision,
48
+ worktreeKey: dispatch.lease.worktreeKey,
49
+ ownerType: "project",
50
+ ownerId: request.projectId,
51
+ lane: "runtime_readiness",
52
+ phase: "readiness_probe",
53
+ allowedRepoRoot: request.repoRoot,
54
+ cleanupWorktrees: request.cleanupWorktrees,
55
+ artifacts: [],
56
+ });
57
+ assertReadinessProbeStartupBudget(request.expiresAt);
58
+ let started = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal);
59
+ runtimeStarted = true;
60
+ const discovery = started.runtimeContract.discovery;
61
+ if (!hostRuntimeParallelEligible(started.runtimeContract)) {
62
+ result = {
63
+ status: "serialized",
64
+ runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
65
+ runtimeContractSource: started.runtimeContract.source,
66
+ reasonCode: discovery?.reasonCode ?? "host_runtime_contract_serialized",
67
+ reasonMessage: discovery?.summary ?? "The detected runtime requires serialized host execution.",
68
+ checkedAt: new Date().toISOString(),
69
+ };
70
+ }
71
+ else {
72
+ assertReadinessProbeStartupBudget(request.expiresAt);
73
+ started = await this.hostRuntime.startConfiguredProject(request.requestId, request.fencingToken, signal);
74
+ const requiredPorts = configuredLaunchPortNames(started.runtimeContract);
75
+ const verifiedPorts = started.lease.resources.ports.map(port => port.name);
76
+ const verified = requiredPorts.length > 0 && requiredPorts.every(name => verifiedPorts.includes(name));
77
+ result = verified
78
+ ? {
79
+ status: "parallel_ready",
80
+ runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
81
+ runtimeContractSource: started.runtimeContract.source,
82
+ reasonCode: null,
83
+ reasonMessage: null,
84
+ qaAppLaunchTarget: managedQaLaunchTarget(started.runtimeContract),
85
+ checkedAt: new Date().toISOString(),
86
+ }
87
+ : failedProbe("host_runtime_probe_unverified", "The application did not prove ownership and readiness on every Forge-assigned port.");
88
+ }
89
+ }
90
+ catch (error) {
91
+ result = failedProbe(probeFailureCode(error), probeFailureMessage(error));
92
+ }
93
+ finally {
94
+ if (runtimeStarted || admissionAcquired) {
95
+ try {
96
+ const released = await this.hostRuntime.release(request.requestId, request.fencingToken);
97
+ if (runtimeStarted && !released)
98
+ cleanupFailure = new Error("The runtime lease was no longer current during cleanup.");
99
+ }
100
+ catch (error) {
101
+ cleanupFailure = error instanceof Error ? error : new Error(String(error));
102
+ }
103
+ }
104
+ if (worktree) {
105
+ try {
106
+ await this.worktrees.release(worktree);
107
+ }
108
+ catch (error) {
109
+ cleanupFailure ??= error instanceof Error ? error : new Error(String(error));
110
+ }
111
+ }
112
+ }
113
+ if (cleanupFailure) {
114
+ await this.hostRuntime.invalidatePromotion?.(request.repoId).catch(() => undefined);
115
+ return failedProbe("host_runtime_cleanup_degraded", `Forge could not prove exact readiness cleanup: ${cleanupFailure.message}`);
116
+ }
117
+ return result;
13
118
  }
14
119
  async runOnce(dispatch, sink, signal) {
15
120
  validateApiKeySecretGrant(dispatch);
@@ -19,7 +124,14 @@ export class BridgeSessionService {
19
124
  if (signal?.aborted) {
20
125
  return;
21
126
  }
22
- const redacted = redactSecrets(event, redactionSecrets);
127
+ const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
128
+ if (redacted.eventType !== "provider_tool_heartbeat") {
129
+ events.push(redacted);
130
+ }
131
+ await sink.emit(redacted);
132
+ };
133
+ const emitLifecycle = async (event) => {
134
+ const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
23
135
  events.push(redacted);
24
136
  await sink.emit(redacted);
25
137
  };
@@ -30,44 +142,131 @@ export class BridgeSessionService {
30
142
  content: "forge-bridge accepted run-once dispatch.",
31
143
  eventType: "dispatch_accepted"
32
144
  });
33
- const worktree = await this.worktrees.prepare({
34
- repo: dispatch.repo,
35
- workItem: dispatch.workItem,
36
- baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
37
- worktreeKey: dispatch.lease.worktreeKey,
38
- ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
39
- ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
40
- lane: dispatch.execution.lane,
41
- phase: dispatch.cycleRun.phase,
42
- allowedRepoRoot: dispatch.serverLocal.repoRoot,
43
- cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
44
- artifacts: dispatch.artifacts,
45
- qaRework: dispatch.qaRework,
46
- warmPathOverrides: dispatch.warmPathOverrides
47
- });
48
- const providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
49
- throwIfAborted(signal);
50
- await emit({
51
- role: "runner",
52
- kind: "checkpoint",
53
- content: `Prepared worktree ${worktree.branchName}.`,
54
- eventType: "worktree_prepared"
55
- });
56
- if (worktree.dependencyWarm) {
57
- await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
58
- }
145
+ let worktree = null;
146
+ let hostSingletonKeys = null;
147
+ let hostLease = null;
59
148
  try {
149
+ if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
150
+ await emitLifecycle({
151
+ role: "runner",
152
+ kind: "checkpoint",
153
+ content: "Evaluating runner-local managed host admission before preparing the worktree.",
154
+ eventType: "host_runtime_admission_waiting",
155
+ });
156
+ try {
157
+ hostSingletonKeys = await this.hostRuntime.acquire(dispatch, signal);
158
+ }
159
+ catch (error) {
160
+ if (error instanceof HostRuntimeAdmissionExpiredError) {
161
+ await emitLifecycle({
162
+ role: "runner",
163
+ kind: "error",
164
+ content: "Managed host admission expired while the project singleton remained busy; no worktree or runtime resources were created.",
165
+ eventType: "host_runtime_admission_expired",
166
+ }).catch(() => undefined);
167
+ }
168
+ throw error;
169
+ }
170
+ throwIfAborted(signal);
171
+ await emitLifecycle({
172
+ role: "runner",
173
+ kind: "checkpoint",
174
+ content: hostSingletonKeys.length > 0
175
+ ? `Acquired managed host singleton ${hostSingletonKeys.join(", ")}.`
176
+ : "Observed assigned-port evidence permits provisional parallel host admission; the prepared runtime contract must still match before launch.",
177
+ eventType: "host_runtime_admission_acquired",
178
+ });
179
+ }
180
+ try {
181
+ worktree = await this.worktrees.prepare({
182
+ repo: dispatch.repo,
183
+ workItem: dispatch.workItem,
184
+ baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
185
+ worktreeKey: dispatch.lease.worktreeKey,
186
+ ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
187
+ ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
188
+ lane: dispatch.execution.lane,
189
+ phase: dispatch.cycleRun.phase,
190
+ allowedRepoRoot: dispatch.serverLocal.repoRoot,
191
+ cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
192
+ artifacts: dispatch.artifacts,
193
+ priorPhaseArtifacts: dispatch.priorPhaseArtifacts,
194
+ qaRework: dispatch.qaRework,
195
+ warmPathOverrides: dispatch.warmPathOverrides
196
+ });
197
+ }
198
+ catch (error) {
199
+ throw new Error(`worktree_prepare_failed: ${failureMessage(error)}`, { cause: error });
200
+ }
201
+ let providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
202
+ throwIfAborted(signal);
203
+ await emit({
204
+ role: "runner",
205
+ kind: "checkpoint",
206
+ content: `Prepared worktree ${worktree.branchName}.`,
207
+ eventType: "worktree_prepared"
208
+ });
209
+ if ((worktree.hydratedPriorPhaseArtifactCount ?? 0) > 0) {
210
+ await emit({
211
+ role: "runner",
212
+ kind: "checkpoint",
213
+ content: `Hydrated ${worktree.hydratedPriorPhaseArtifactCount} persisted prior-phase artifact(s) into the prepared worktree.`,
214
+ eventType: "prior_phase_artifacts_hydrated"
215
+ });
216
+ }
217
+ if (worktree.dependencyWarm) {
218
+ await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
219
+ }
220
+ if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
221
+ hostLease = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal);
222
+ await emitLifecycle({
223
+ role: "runner",
224
+ kind: "checkpoint",
225
+ content: `Host runtime lease ${hostLease.lease.leaseId} is active for this worktree.`,
226
+ eventType: "sandbox_started",
227
+ occurredAt: hostLease.lease.stateChangedAt,
228
+ data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
229
+ });
230
+ await emitLifecycle({
231
+ role: "runner",
232
+ kind: "checkpoint",
233
+ 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.",
234
+ eventType: "host_runtime_isolation_notice",
235
+ data: JSON.stringify({
236
+ isolationStrength: "managed_host_process",
237
+ remediation: "Enable Run in Docker in Project Settings.",
238
+ }),
239
+ });
240
+ const unconfiguredPortCount = hostLease.lease.resources.ports.length;
241
+ hostLease = await this.hostRuntime.startConfiguredProject(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken, signal);
242
+ if (hostLease.lease.resources.ports.length > unconfiguredPortCount) {
243
+ const assigned = hostLease.lease.resources.ports.at(-1);
244
+ await emitLifecycle({
245
+ role: "runner",
246
+ kind: "checkpoint",
247
+ content: `Verified supervised project runtime ${assigned.name} on ${assigned.hostIp}:${assigned.hostPort}.`,
248
+ eventType: "host_runtime_project_ready",
249
+ data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
250
+ });
251
+ }
252
+ }
253
+ providerDispatch = withMaterializedManagedQaTarget(providerDispatch, hostLease);
60
254
  if (dispatch.draft) {
61
255
  throwIfAborted(signal);
256
+ const processRunner = hostLease && dispatch.sandbox
257
+ ? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
258
+ : undefined;
62
259
  const run = await this.providerCli.runDraft({
63
260
  dispatch: providerDispatch,
64
261
  worktree,
262
+ runtimeEnvironment: hostLease?.environment,
263
+ processRunner,
65
264
  emit,
66
265
  signal
67
266
  });
68
267
  throwIfAborted(signal);
69
268
  const draftArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
70
- return redactSecrets({
269
+ return redactRunOnceResultPacket({
71
270
  schemaVersion: 1,
72
271
  exitCode: 0,
73
272
  summary: run.summary,
@@ -88,21 +287,36 @@ export class BridgeSessionService {
88
287
  const context = {
89
288
  dispatch: providerDispatch,
90
289
  worktree,
290
+ runtimeEnvironment: hostLease?.environment,
291
+ processRunner: hostLease && dispatch.sandbox
292
+ ? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
293
+ : undefined,
91
294
  emit,
92
295
  signal
93
296
  };
94
- const run = isQa
95
- ? await this.providerCli.runQa(context)
96
- : isArchitect
97
- ? await this.providerCli.runArchitect(context)
98
- : isCiRepair
99
- ? await this.providerCli.runCiRepair(context)
100
- : isConflictResolve
101
- ? await this.providerCli.runConflictResolve(context)
102
- : await this.providerCli.runImplementation(context);
297
+ let run;
298
+ try {
299
+ run = isQa
300
+ ? await this.providerCli.runQa(context)
301
+ : isArchitect
302
+ ? await this.providerCli.runArchitect(context)
303
+ : isCiRepair
304
+ ? await this.providerCli.runCiRepair(context)
305
+ : isConflictResolve
306
+ ? await this.providerCli.runConflictResolve(context)
307
+ : await this.providerCli.runImplementation(context);
308
+ }
309
+ catch (error) {
310
+ const message = failureMessage(error);
311
+ if (message.includes("attempt_process_supervisor_start_")
312
+ || message.includes("host_process_supervisor_start_")) {
313
+ throw new Error(`provider_start_unavailable: ${message}`, { cause: error });
314
+ }
315
+ throw error;
316
+ }
103
317
  throwIfAborted(signal);
104
318
  const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
105
- return redactSecrets({
319
+ return redactRunOnceResultPacket({
106
320
  schemaVersion: 1,
107
321
  exitCode: 0,
108
322
  summary: run.summary,
@@ -113,10 +327,191 @@ export class BridgeSessionService {
113
327
  }, redactionSecrets);
114
328
  }
115
329
  finally {
116
- await this.worktrees.release(worktree);
330
+ if (hostLease && dispatch.sandbox) {
331
+ const teardownAt = new Date().toISOString();
332
+ await emitLifecycle({
333
+ role: "runner",
334
+ kind: "checkpoint",
335
+ content: `Releasing host runtime lease ${hostLease.lease.leaseId}.`,
336
+ eventType: "sandbox_teardown_started",
337
+ occurredAt: teardownAt,
338
+ }).catch(() => undefined);
339
+ try {
340
+ const released = await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
341
+ if (!released) {
342
+ await emitLifecycle({
343
+ role: "runner",
344
+ kind: "error",
345
+ content: "Host runtime cleanup requires reconciliation: the runtime lease was not current during release.",
346
+ eventType: "teardown_failed",
347
+ occurredAt: new Date().toISOString(),
348
+ }).catch(() => undefined);
349
+ }
350
+ else {
351
+ await emitLifecycle({
352
+ role: "runner",
353
+ kind: "checkpoint",
354
+ content: `Released host runtime lease ${released.leaseId}.`,
355
+ eventType: "sandbox_teardown_completed",
356
+ occurredAt: released.releasedAt,
357
+ }).catch(() => undefined);
358
+ }
359
+ }
360
+ catch (error) {
361
+ await emitLifecycle({
362
+ role: "runner",
363
+ kind: "error",
364
+ content: `Host runtime cleanup requires reconciliation: ${error instanceof Error ? error.message : String(error)}`,
365
+ eventType: "teardown_failed",
366
+ occurredAt: new Date().toISOString(),
367
+ }).catch(() => undefined);
368
+ }
369
+ }
370
+ if (!hostLease && hostSingletonKeys && dispatch.sandbox) {
371
+ await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken).catch(() => undefined);
372
+ }
373
+ if (worktree)
374
+ await this.worktrees.release(worktree);
117
375
  }
118
376
  }
119
377
  }
378
+ function failureMessage(error) {
379
+ return error instanceof Error ? error.message : String(error);
380
+ }
381
+ function readinessProbeDispatch(request) {
382
+ return {
383
+ schemaVersion: 2,
384
+ userId: request.userId,
385
+ workItem: {
386
+ id: request.projectId,
387
+ kind: "runtime_readiness",
388
+ title: "Automatic host-isolation readiness check",
389
+ description: "Internal readiness proof without a provider agent.",
390
+ acceptanceCriteria: "",
391
+ reproSteps: "",
392
+ systemInfo: "",
393
+ },
394
+ repo: {
395
+ id: request.repoId,
396
+ name: request.repoName,
397
+ remoteUrl: request.remoteUrl,
398
+ defaultBranch: request.defaultBranch,
399
+ cloneOnDemand: request.cloneOnDemand,
400
+ serverLocalPath: request.runnerRepoLocalPath,
401
+ },
402
+ cycleRun: {
403
+ id: request.requestId,
404
+ ownerType: "project",
405
+ ownerId: request.projectId,
406
+ kind: "runtime_readiness",
407
+ phase: "readiness_probe",
408
+ requestedBase: request.revision,
409
+ baseBranch: request.revision,
410
+ attempt: request.attempt,
411
+ model: "none",
412
+ },
413
+ session: { id: request.requestId, model: "none", lane: "runtime_readiness" },
414
+ lease: {
415
+ id: request.requestId,
416
+ ownerType: "project",
417
+ ownerId: request.projectId,
418
+ worktreeKey: `runtime-readiness-${request.requestId}`,
419
+ lane: "runtime_readiness",
420
+ fencingToken: request.fencingToken,
421
+ expiresAt: request.expiresAt,
422
+ },
423
+ execution: {
424
+ provider: "none",
425
+ model: "none",
426
+ authKind: "none",
427
+ location: "bridge",
428
+ runnerId: request.runnerId,
429
+ repoId: request.repoId,
430
+ role: "runtime_readiness",
431
+ lane: "runtime_readiness",
432
+ requiresApiKeySecretGrant: false,
433
+ runnerRepoLocalPath: request.runnerRepoLocalPath,
434
+ },
435
+ runtimeIntent: { executionClass: "managed_runtime", applicationRuntimeRequired: true },
436
+ serverLocal: { repoRoot: request.repoRoot, cleanupWorktrees: request.cleanupWorktrees },
437
+ sandbox: {
438
+ dispatchAttemptId: request.requestId,
439
+ executionLeaseId: request.requestId,
440
+ runnerInstanceId: request.runnerInstanceId,
441
+ executionMode: "host",
442
+ executionPolicy: "host_allowed",
443
+ networkPolicy: { mode: "offline", allowedHosts: [] },
444
+ requestedResources: {
445
+ sandboxSlots: 1,
446
+ cpuMillis: 2000,
447
+ workerMemoryLimitBytes: 1024 * 1024 * 1024,
448
+ serviceMemoryLimitBytes: 1024 * 1024 * 1024,
449
+ runtimeOverheadBytes: 256 * 1024 * 1024,
450
+ memoryBytes: 2 * 1024 * 1024 * 1024,
451
+ workspaceBytes: 10 * 1024 * 1024 * 1024,
452
+ pids: 512,
453
+ buildSlots: 1,
454
+ },
455
+ publicationMode: "shared_filesystem",
456
+ },
457
+ artifacts: [],
458
+ };
459
+ }
460
+ function failedProbe(reasonCode, reasonMessage) {
461
+ return { status: "failed", reasonCode, reasonMessage, checkedAt: new Date().toISOString() };
462
+ }
463
+ function assertReadinessProbeStartupBudget(expiresAt) {
464
+ const remainingMs = Date.parse(expiresAt) - Date.now();
465
+ if (!Number.isFinite(remainingMs) || remainingMs < minimumReadinessProbeStartupBudgetMs) {
466
+ 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.");
467
+ }
468
+ }
469
+ function managedQaLaunchTarget(runtimeContract) {
470
+ const playwright = runtimeContract.contract.runtime.playwright;
471
+ if (!playwright)
472
+ return null;
473
+ const port = runtimeContract.contract.runtime.ports.find(candidate => candidate.name === playwright.baseUrlPort);
474
+ return port?.launch && port.baseUrlEnv && port.format
475
+ ? { managedRuntimePort: port.name, routes: ["/"] }
476
+ : null;
477
+ }
478
+ function withMaterializedManagedQaTarget(dispatch, hostLease) {
479
+ const target = dispatch.qaBrowser?.launchTarget;
480
+ const portName = target?.managed_runtime_port?.trim().toLowerCase();
481
+ if (!target || !portName)
482
+ return dispatch;
483
+ if (!hostLease) {
484
+ throw new Error("qa_managed_runtime_unavailable: Browser QA requires the verified managed host runtime selected during readiness.");
485
+ }
486
+ const normalizedName = portName.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_");
487
+ const url = hostLease.environment[`FORGE_${normalizedName}_URL`];
488
+ if (!url) {
489
+ throw new Error(`qa_managed_runtime_unavailable: verified runtime port '${portName}' was not launched for this attempt.`);
490
+ }
491
+ return {
492
+ ...dispatch,
493
+ qaBrowser: dispatch.qaBrowser ? {
494
+ ...dispatch.qaBrowser,
495
+ launchTarget: {
496
+ ...target,
497
+ url: target.url ?? url,
498
+ ready_url: target.ready_url ?? url,
499
+ notes: [target.notes, "Forge launched and verified this URL inside the current fenced attempt."]
500
+ .filter(Boolean)
501
+ .join(" "),
502
+ },
503
+ } : dispatch.qaBrowser,
504
+ };
505
+ }
506
+ function probeFailureCode(error) {
507
+ const message = error instanceof Error ? error.message : String(error);
508
+ const candidate = message.split(":", 1)[0]?.trim().toLowerCase() ?? "";
509
+ return /^[a-z][a-z0-9_.-]{2,159}$/u.test(candidate) ? candidate : "host_runtime_probe_failed";
510
+ }
511
+ function probeFailureMessage(error) {
512
+ const message = error instanceof Error ? error.message : String(error);
513
+ return message.length > 1000 ? `${message.slice(0, 997)}...` : message;
514
+ }
120
515
  function throwIfAborted(signal) {
121
516
  if (signal?.aborted) {
122
517
  throw new Error(String(signal.reason ?? "Dispatch cancelled."));
@@ -7,12 +7,14 @@ export interface RunOnceDispatchPacket {
7
7
  readonly session: DispatchSession;
8
8
  readonly lease: DispatchLease;
9
9
  readonly execution: DispatchExecution;
10
+ readonly runtimeIntent: DispatchRuntimeIntent;
10
11
  readonly serverLocal: DispatchServerLocalConfig;
11
12
  readonly sandbox?: DispatchSandboxContext | null;
12
13
  readonly pullRequest?: DispatchPullRequestContext | null;
13
14
  readonly apiKeySecretGrant?: ApiKeySecretGrant | null;
14
15
  readonly apiKeySecretGrantHandle?: ApiKeySecretGrantHandle | null;
15
16
  readonly artifacts: readonly DispatchResolvedArtifact[];
17
+ readonly priorPhaseArtifacts?: readonly DispatchPriorPhaseArtifact[];
16
18
  readonly warmPathOverrides?: readonly string[];
17
19
  readonly qaBrowser?: DispatchQaBrowserContext | null;
18
20
  readonly qaScope?: DispatchQaScope | null;
@@ -24,6 +26,10 @@ export interface RunOnceDispatchPacket {
24
26
  readonly designerRefs?: DispatchDesignerRefHandoffContext | null;
25
27
  readonly draft?: DispatchDraftPrompt | null;
26
28
  }
29
+ export interface DispatchRuntimeIntent {
30
+ readonly executionClass: "analysis_only" | "managed_runtime";
31
+ readonly applicationRuntimeRequired: boolean;
32
+ }
27
33
  export interface DispatchSandboxContext {
28
34
  readonly dispatchAttemptId: string;
29
35
  readonly executionLeaseId: string;
@@ -48,6 +54,9 @@ export interface DispatchPublishGrantHandle {
48
54
  export interface DispatchSandboxResourceRequest {
49
55
  readonly sandboxSlots: number;
50
56
  readonly cpuMillis: number;
57
+ readonly workerMemoryLimitBytes: number;
58
+ readonly serviceMemoryLimitBytes: number;
59
+ readonly runtimeOverheadBytes: number;
51
60
  readonly memoryBytes: number;
52
61
  readonly workspaceBytes: number;
53
62
  readonly pids: number;
@@ -101,6 +110,7 @@ export interface DispatchQaAppLaunchTarget {
101
110
  readonly command?: string | null;
102
111
  readonly working_directory?: string | null;
103
112
  readonly ready_url?: string | null;
113
+ readonly managed_runtime_port?: string | null;
104
114
  readonly routes: readonly string[];
105
115
  readonly notes?: string | null;
106
116
  }
@@ -189,6 +199,19 @@ export interface DispatchStoryQaDependencyContext {
189
199
  readonly fromWorkItemId: string;
190
200
  readonly toWorkItemId: string;
191
201
  }
202
+ export interface DispatchPriorPhaseArtifact {
203
+ readonly workItemId: string;
204
+ readonly artifactId: string;
205
+ readonly cycleRunId: string;
206
+ readonly phase: "implementation" | "self_review" | "validation";
207
+ readonly status: string;
208
+ readonly producedAt: string;
209
+ readonly sourcePath: string;
210
+ readonly verdictJson: string;
211
+ readonly report: string;
212
+ readonly verdictContentHash: string;
213
+ readonly reportContentHash: string;
214
+ }
192
215
  export interface DispatchDesignerRefHandoffContext {
193
216
  readonly schemaVersion: number;
194
217
  readonly kind: string;
@@ -368,4 +391,36 @@ export interface RunnerEventLine {
368
391
  readonly event: RunnerDispatchEvent;
369
392
  }
370
393
  export declare function parseRunOnceDispatchPacket(value: unknown): RunOnceDispatchPacket;
394
+ export interface HostRuntimeReadinessProbeRequest {
395
+ readonly requestId: string;
396
+ readonly fencingToken: string;
397
+ readonly userId: string;
398
+ readonly projectId: string;
399
+ readonly repoId: string;
400
+ readonly runnerId: string;
401
+ readonly runnerInstanceId: string;
402
+ readonly attempt: number;
403
+ readonly repoName: string;
404
+ readonly remoteUrl: string;
405
+ readonly defaultBranch: string;
406
+ readonly cloneOnDemand: boolean;
407
+ readonly runnerRepoLocalPath: string;
408
+ readonly revision: string;
409
+ readonly expiresAt: string;
410
+ readonly repoRoot: string;
411
+ readonly cleanupWorktrees: boolean;
412
+ }
413
+ export interface HostRuntimeReadinessProbeResult {
414
+ readonly status: "parallel_ready" | "serialized" | "failed";
415
+ readonly runtimeContractFingerprint?: string | null;
416
+ readonly runtimeContractSource?: string | null;
417
+ readonly reasonCode?: string | null;
418
+ readonly reasonMessage?: string | null;
419
+ readonly qaAppLaunchTarget?: HostRuntimeReadinessQaLaunchTarget | null;
420
+ readonly checkedAt: string;
421
+ }
422
+ export interface HostRuntimeReadinessQaLaunchTarget {
423
+ readonly managedRuntimePort: string;
424
+ readonly routes: readonly string[];
425
+ }
371
426
  export declare function failureResult(summary: string, events?: readonly RunnerDispatchEvent[]): RunOnceResultPacket;