@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53

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 (73) hide show
  1. package/dist/bridge/bridge.base.d.ts +2 -1
  2. package/dist/bridge/bridge.service.d.ts +8 -0
  3. package/dist/bridge/bridge.service.js +184 -12
  4. package/dist/bridge/bridge.types.d.ts +37 -0
  5. package/dist/cli/cli.help.js +1 -1
  6. package/dist/cli/cli.service.js +1 -1
  7. package/dist/cli/local-config.js +2 -1
  8. package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
  9. package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
  10. package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
  11. package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
  12. package/dist/runtime/attempt-process-supervisor.js +75 -15
  13. package/dist/runtime/generated-output-reset.d.ts +10 -0
  14. package/dist/runtime/generated-output-reset.js +103 -0
  15. package/dist/runtime/host-listener-inspector.d.ts +16 -0
  16. package/dist/runtime/host-listener-inspector.js +143 -4
  17. package/dist/runtime/host-runtime-contract.d.ts +9 -1
  18. package/dist/runtime/host-runtime-contract.js +411 -6
  19. package/dist/runtime/host-runtime-lease.d.ts +12 -1
  20. package/dist/runtime/host-runtime-lease.js +271 -50
  21. package/dist/runtime/host-runtime-promotion.d.ts +2 -0
  22. package/dist/runtime/host-runtime-promotion.js +35 -4
  23. package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
  24. package/dist/runtime/node-host-runtime-adapter.js +71 -8
  25. package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
  26. package/dist/sandbox/preflight.d.ts +9 -0
  27. package/dist/sandbox/preflight.js +39 -4
  28. package/dist/sandbox/workspace.service.js +7 -2
  29. package/dist/session/managed-attempt-tools.d.ts +17 -0
  30. package/dist/session/managed-attempt-tools.js +712 -0
  31. package/dist/session/phase-completion.artifacts.js +9 -2
  32. package/dist/session/phase-completion.d.ts +11 -2
  33. package/dist/session/phase-completion.helpers.d.ts +5 -0
  34. package/dist/session/phase-completion.helpers.js +45 -9
  35. package/dist/session/phase-completion.js +12 -3
  36. package/dist/session/phase-completion.prompts.d.ts +3 -3
  37. package/dist/session/phase-completion.prompts.js +21 -6
  38. package/dist/session/phase-completion.runtime.d.ts +13 -1
  39. package/dist/session/phase-completion.runtime.js +183 -8
  40. package/dist/session/phase-completion.schemas.js +108 -3
  41. package/dist/session/phase-completion.validation.d.ts +4 -1
  42. package/dist/session/phase-completion.validation.js +144 -18
  43. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  44. package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
  45. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  46. package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
  47. package/dist/session/provider-cli/provider-cli.process.js +176 -18
  48. package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
  49. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  50. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
  51. package/dist/session/provider-cli/provider-cli.service.js +390 -83
  52. package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
  53. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  54. package/dist/session/provider-cli/provider-output-state.js +30 -2
  55. package/dist/session/session.base.d.ts +2 -1
  56. package/dist/session/session.service.d.ts +2 -1
  57. package/dist/session/session.service.js +303 -30
  58. package/dist/session/session.types.d.ts +38 -0
  59. package/dist/session/session.types.js +17 -0
  60. package/dist/shared/process/host-process-supervisor.d.ts +18 -1
  61. package/dist/shared/process/host-process-supervisor.js +94 -7
  62. package/dist/shared/process/process.d.ts +1 -0
  63. package/dist/shared/process/process.js +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/dist/worktree/artifact-injector.d.ts +3 -2
  67. package/dist/worktree/artifact-injector.js +43 -4
  68. package/dist/worktree/dependency-warmer.d.ts +10 -1
  69. package/dist/worktree/dependency-warmer.js +94 -5
  70. package/dist/worktree/worktree.service.d.ts +1 -0
  71. package/dist/worktree/worktree.service.js +12 -1
  72. package/package.json +1 -1
  73. package/runtime/windows-job-supervisor.ps1 +89 -20
@@ -1,9 +1,11 @@
1
1
  import type { RunOnceDispatchPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec } from "../session.types.js";
2
2
  import type { WorktreeHandle } from "../../worktree/worktree.types.js";
3
+ import type { ProviderProcessRunner } from "./provider-cli.process.js";
3
4
  export interface ProviderCliRunContext {
4
5
  readonly dispatch: RunOnceDispatchPacket;
5
6
  readonly worktree: WorktreeHandle;
6
7
  readonly runtimeEnvironment?: Readonly<Record<string, string>>;
8
+ readonly processRunner?: ProviderProcessRunner;
7
9
  readonly signal?: AbortSignal;
8
10
  emit(event: RunnerDispatchEvent): Promise<void>;
9
11
  }
@@ -11,6 +13,7 @@ export interface ProviderCliDraftContext {
11
13
  readonly dispatch: RunOnceDispatchPacket;
12
14
  readonly worktree: WorktreeHandle;
13
15
  readonly runtimeEnvironment?: Readonly<Record<string, string>>;
16
+ readonly processRunner?: ProviderProcessRunner;
14
17
  readonly signal?: AbortSignal;
15
18
  emit(event: RunnerDispatchEvent): Promise<void>;
16
19
  }
@@ -5,6 +5,8 @@ export declare function clip(value: string, maxLength: number): string;
5
5
  export interface ProviderOutputState {
6
6
  readonly sessionLabel: string | null;
7
7
  finalMessage: string | null;
8
+ /** True only while the provider has reported a tool/command start without a matching completion. */
9
+ readonly hasActiveToolCall: boolean;
8
10
  /** Accumulated provider-reported token usage for this run, or null when none was reported. */
9
11
  readonly usage: ProviderUsage | null;
10
12
  consume(line: string): readonly RunnerDispatchEvent[];
@@ -25,12 +25,16 @@ class CursorStreamJsonState {
25
25
  finalMessage = null;
26
26
  emittedSession = false;
27
27
  accumulatedUsage = null;
28
+ activeToolCallCount = 0;
28
29
  get sessionLabel() {
29
30
  return this.sessionId ? `session ${this.sessionId}` : null;
30
31
  }
31
32
  get usage() {
32
33
  return this.accumulatedUsage;
33
34
  }
35
+ get hasActiveToolCall() {
36
+ return this.activeToolCallCount > 0;
37
+ }
34
38
  consume(line) {
35
39
  const event = parseJsonLine(line);
36
40
  if (!event) {
@@ -65,6 +69,7 @@ class CursorStreamJsonState {
65
69
  const subtype = stringField(event, "subtype");
66
70
  const toolCall = objectField(event, "tool_call");
67
71
  if (subtype === "started") {
72
+ this.activeToolCallCount += 1;
68
73
  const summary = formatCursorToolCallStarted(toolCall);
69
74
  if (summary) {
70
75
  return [{
@@ -76,6 +81,7 @@ class CursorStreamJsonState {
76
81
  }
77
82
  }
78
83
  if (subtype === "completed") {
84
+ this.activeToolCallCount = Math.max(0, this.activeToolCallCount - 1);
79
85
  const completed = formatCursorToolCallCompleted(toolCall);
80
86
  if (completed) {
81
87
  return [{
@@ -224,18 +230,23 @@ class CodexJsonState {
224
230
  finalMessage = null;
225
231
  emittedThread = false;
226
232
  accumulatedUsage = null;
233
+ activeToolCallCount = 0;
227
234
  get sessionLabel() {
228
235
  return this.threadId ? `thread ${this.threadId}` : null;
229
236
  }
230
237
  get usage() {
231
238
  return this.accumulatedUsage;
232
239
  }
240
+ get hasActiveToolCall() {
241
+ return this.activeToolCallCount > 0;
242
+ }
233
243
  consume(line) {
234
244
  const event = parseJsonLine(line);
235
245
  if (!event) {
236
246
  return [];
237
247
  }
238
248
  const type = stringField(event, "type") ?? "";
249
+ this.trackToolActivity(type, event);
239
250
  if (type === "thread.started") {
240
251
  this.threadId = stringField(event, "thread_id");
241
252
  if (this.threadId && !this.emittedThread) {
@@ -265,7 +276,7 @@ class CodexJsonState {
265
276
  eventType: "provider_cli_error"
266
277
  }];
267
278
  }
268
- if (type === "item.completed" || type === "item.updated") {
279
+ if (type === "item.started" || type === "item.completed" || type === "item.updated") {
269
280
  const item = objectField(event, "item");
270
281
  const itemType = stringField(item, "type");
271
282
  if (itemType === "agent_message") {
@@ -287,7 +298,7 @@ class CodexJsonState {
287
298
  return [{
288
299
  role: "worker",
289
300
  kind: "checkpoint",
290
- content: `Codex command ${status || "completed"}: ${clip(command, 500)}`,
301
+ content: `Codex command ${status || (type === "item.started" ? "started" : "completed")}: ${clip(command, 500)}`,
291
302
  eventType: "provider_command"
292
303
  }];
293
304
  }
@@ -299,6 +310,23 @@ class CodexJsonState {
299
310
  }
300
311
  return [];
301
312
  }
313
+ trackToolActivity(type, event) {
314
+ if (type !== "item.started" && type !== "item.completed") {
315
+ return;
316
+ }
317
+ const item = objectField(event, "item");
318
+ const itemType = (stringField(item, "type") ?? "").toLowerCase();
319
+ const toolName = mcpToolName(item);
320
+ const isTool = itemType === "command_execution"
321
+ || itemType.includes("mcp")
322
+ || Boolean(toolName);
323
+ if (!isTool) {
324
+ return;
325
+ }
326
+ this.activeToolCallCount = type === "item.started"
327
+ ? this.activeToolCallCount + 1
328
+ : Math.max(0, this.activeToolCallCount - 1);
329
+ }
302
330
  accumulateUsage(event) {
303
331
  const usage = objectField(event, "usage");
304
332
  if (Object.keys(usage).length === 0) {
@@ -1,4 +1,4 @@
1
- import type { RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "./session.types.js";
1
+ import type { HostRuntimeReadinessProbeRequest, HostRuntimeReadinessProbeResult, RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "./session.types.js";
2
2
  import type { HostRuntimeCapacitySnapshot, HostRuntimeReapOptions, HostRuntimeReapReport } from "../runtime/host-runtime-lease.js";
3
3
  export interface SessionEventSink {
4
4
  emit(event: RunnerDispatchEvent): Promise<void>;
@@ -8,4 +8,5 @@ export interface SessionService {
8
8
  updateLease?(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
9
9
  reapHostRuntime?(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport | null>;
10
10
  runtimeCapacity?(): HostRuntimeCapacitySnapshot | null;
11
+ probeHostRuntime?(request: HostRuntimeReadinessProbeRequest, signal?: AbortSignal): Promise<HostRuntimeReadinessProbeResult>;
11
12
  }
@@ -1,6 +1,6 @@
1
1
  import type { ProviderCliService } from "./provider-cli/provider-cli.base.js";
2
2
  import type { SessionEventSink, SessionService } from "./session.base.js";
3
- import type { RunOnceDispatchPacket, RunOnceResultPacket } from "./session.types.js";
3
+ import type { HostRuntimeReadinessProbeRequest, HostRuntimeReadinessProbeResult, RunOnceDispatchPacket, RunOnceResultPacket } from "./session.types.js";
4
4
  import type { WorktreeService } from "../worktree/worktree.base.js";
5
5
  import { type HostRuntimeCapacitySnapshot, type HostRuntimeLeaseRuntime, type HostRuntimeReapOptions, type HostRuntimeReapReport } from "../runtime/host-runtime-lease.js";
6
6
  export declare class BridgeSessionService implements SessionService {
@@ -11,5 +11,6 @@ export declare class BridgeSessionService implements SessionService {
11
11
  updateLease(dispatchAttemptId: string, fencingToken: string, expiresAt: string): Promise<void>;
12
12
  reapHostRuntime(options: HostRuntimeReapOptions): Promise<HostRuntimeReapReport | null>;
13
13
  runtimeCapacity(): HostRuntimeCapacitySnapshot | null;
14
+ probeHostRuntime(request: HostRuntimeReadinessProbeRequest, signal?: AbortSignal): Promise<HostRuntimeReadinessProbeResult>;
14
15
  runOnce(dispatch: RunOnceDispatchPacket, sink: SessionEventSink, signal?: AbortSignal): Promise<RunOnceResultPacket>;
15
16
  }
@@ -3,6 +3,8 @@ import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.j
3
3
  import { GitWorktreeService } from "../worktree/worktree.service.js";
4
4
  import { SandboxWorkspaceService } from "../sandbox/workspace.service.js";
5
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;
6
8
  export class BridgeSessionService {
7
9
  worktrees;
8
10
  providerCli;
@@ -23,6 +25,100 @@ export class BridgeSessionService {
23
25
  runtimeCapacity() {
24
26
  return this.hostRuntime?.snapshot?.() ?? null;
25
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
+ // Readiness probes are disposable infrastructure checks, not agent lanes.
55
+ // Always remove their worktree even when the bridge is configured to retain
56
+ // failed agent worktrees for inspection.
57
+ cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
58
+ artifacts: [],
59
+ });
60
+ assertReadinessProbeStartupBudget(request.expiresAt);
61
+ let started = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal);
62
+ runtimeStarted = true;
63
+ const discovery = started.runtimeContract.discovery;
64
+ if (!hostRuntimeParallelEligible(started.runtimeContract)) {
65
+ result = {
66
+ status: "serialized",
67
+ runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
68
+ runtimeContractSource: started.runtimeContract.source,
69
+ reasonCode: discovery?.reasonCode ?? "host_runtime_contract_serialized",
70
+ reasonMessage: discovery?.summary ?? "The detected runtime requires serialized host execution.",
71
+ checkedAt: new Date().toISOString(),
72
+ };
73
+ }
74
+ else {
75
+ assertReadinessProbeStartupBudget(request.expiresAt);
76
+ started = await this.hostRuntime.startConfiguredProject(request.requestId, request.fencingToken, signal);
77
+ const requiredPorts = configuredLaunchPortNames(started.runtimeContract);
78
+ const verifiedPorts = started.lease.resources.ports.map(port => port.name);
79
+ const verified = requiredPorts.length > 0 && requiredPorts.every(name => verifiedPorts.includes(name));
80
+ result = verified
81
+ ? {
82
+ status: "parallel_ready",
83
+ runtimeContractFingerprint: hostRuntimeContractFingerprint(started.runtimeContract),
84
+ runtimeContractSource: started.runtimeContract.source,
85
+ reasonCode: null,
86
+ reasonMessage: null,
87
+ qaAppLaunchTarget: managedQaLaunchTarget(started.runtimeContract),
88
+ checkedAt: new Date().toISOString(),
89
+ }
90
+ : failedProbe("host_runtime_probe_unverified", "The application did not prove ownership and readiness on every Forge-assigned port.");
91
+ }
92
+ }
93
+ catch (error) {
94
+ result = failedProbe(probeFailureCode(error), probeFailureMessage(error));
95
+ }
96
+ finally {
97
+ if (runtimeStarted || admissionAcquired) {
98
+ try {
99
+ const released = await this.hostRuntime.release(request.requestId, request.fencingToken);
100
+ if (runtimeStarted && !released)
101
+ cleanupFailure = new Error("The runtime lease was no longer current during cleanup.");
102
+ }
103
+ catch (error) {
104
+ cleanupFailure = error instanceof Error ? error : new Error(String(error));
105
+ }
106
+ }
107
+ if (worktree) {
108
+ try {
109
+ await this.worktrees.release(worktree);
110
+ }
111
+ catch (error) {
112
+ cleanupFailure ??= error instanceof Error ? error : new Error(String(error));
113
+ }
114
+ }
115
+ }
116
+ if (cleanupFailure) {
117
+ await this.hostRuntime.invalidatePromotion?.(request.repoId).catch(() => undefined);
118
+ return failedProbe("host_runtime_cleanup_degraded", `Forge could not prove exact readiness cleanup: ${cleanupFailure.message}`);
119
+ }
120
+ return result;
121
+ }
26
122
  async runOnce(dispatch, sink, signal) {
27
123
  validateApiKeySecretGrant(dispatch);
28
124
  const redactionSecrets = exactSecrets(dispatch);
@@ -32,7 +128,9 @@ export class BridgeSessionService {
32
128
  return;
33
129
  }
34
130
  const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
35
- events.push(redacted);
131
+ if (redacted.eventType !== "provider_tool_heartbeat") {
132
+ events.push(redacted);
133
+ }
36
134
  await sink.emit(redacted);
37
135
  };
38
136
  const emitLifecycle = async (event) => {
@@ -52,11 +150,25 @@ export class BridgeSessionService {
52
150
  let hostLease = null;
53
151
  try {
54
152
  if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
153
+ const capacity = this.hostRuntime.snapshot?.() ?? null;
154
+ const projectCapacity = capacity?.projects.find(project => project.repoId === dispatch.repo.id) ?? null;
55
155
  await emitLifecycle({
56
156
  role: "runner",
57
157
  kind: "checkpoint",
58
- content: "Evaluating runner-local managed host admission before preparing the worktree.",
158
+ content: capacity
159
+ ? `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.`
160
+ : "Waiting for runner capacity before preparing the worktree. Forge will continue automatically when admission is available.",
59
161
  eventType: "host_runtime_admission_waiting",
162
+ data: capacity ? JSON.stringify({
163
+ queue_position: capacity.waitingAttempts + 1,
164
+ runner_active_attempts: capacity.activeAttempts,
165
+ runner_reserved_attempts: capacity.reservedAttempts,
166
+ runner_waiting_attempts: capacity.waitingAttempts,
167
+ project_active_attempts: projectCapacity?.activeAttempts ?? 0,
168
+ project_reserved_attempts: projectCapacity?.reservedAttempts ?? 0,
169
+ project_waiting_attempts: projectCapacity?.waitingAttempts ?? 0,
170
+ project_safety_class: projectCapacity?.safetyClass ?? "unknown",
171
+ }) : undefined,
60
172
  });
61
173
  try {
62
174
  hostSingletonKeys = await this.hostRuntime.acquire(dispatch, signal);
@@ -82,23 +194,28 @@ export class BridgeSessionService {
82
194
  eventType: "host_runtime_admission_acquired",
83
195
  });
84
196
  }
85
- worktree = await this.worktrees.prepare({
86
- repo: dispatch.repo,
87
- workItem: dispatch.workItem,
88
- baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
89
- worktreeKey: dispatch.lease.worktreeKey,
90
- ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
91
- ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
92
- lane: dispatch.execution.lane,
93
- phase: dispatch.cycleRun.phase,
94
- allowedRepoRoot: dispatch.serverLocal.repoRoot,
95
- cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
96
- artifacts: dispatch.artifacts,
97
- priorPhaseArtifacts: dispatch.priorPhaseArtifacts,
98
- qaRework: dispatch.qaRework,
99
- warmPathOverrides: dispatch.warmPathOverrides
100
- });
101
- const providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
197
+ try {
198
+ worktree = await this.worktrees.prepare({
199
+ repo: dispatch.repo,
200
+ workItem: dispatch.workItem,
201
+ baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
202
+ worktreeKey: dispatch.lease.worktreeKey,
203
+ ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
204
+ ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
205
+ lane: dispatch.execution.lane,
206
+ phase: dispatch.cycleRun.phase,
207
+ allowedRepoRoot: dispatch.serverLocal.repoRoot,
208
+ cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
209
+ artifacts: dispatch.artifacts,
210
+ priorPhaseArtifacts: dispatch.priorPhaseArtifacts,
211
+ qaRework: dispatch.qaRework,
212
+ warmPathOverrides: dispatch.warmPathOverrides
213
+ });
214
+ }
215
+ catch (error) {
216
+ throw new Error(`worktree_prepare_failed: ${failureMessage(error)}`, { cause: error });
217
+ }
218
+ let providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
102
219
  throwIfAborted(signal);
103
220
  await emit({
104
221
  role: "runner",
@@ -125,7 +242,7 @@ export class BridgeSessionService {
125
242
  content: `Host runtime lease ${hostLease.lease.leaseId} is active for this worktree.`,
126
243
  eventType: "sandbox_started",
127
244
  occurredAt: hostLease.lease.stateChangedAt,
128
- data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract)),
245
+ data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
129
246
  });
130
247
  await emitLifecycle({
131
248
  role: "runner",
@@ -146,16 +263,21 @@ export class BridgeSessionService {
146
263
  kind: "checkpoint",
147
264
  content: `Verified supervised project runtime ${assigned.name} on ${assigned.hostIp}:${assigned.hostPort}.`,
148
265
  eventType: "host_runtime_project_ready",
149
- data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract)),
266
+ data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract, hostLease.executionClass)),
150
267
  });
151
268
  }
152
269
  }
270
+ providerDispatch = withMaterializedManagedQaTarget(providerDispatch, hostLease);
153
271
  if (dispatch.draft) {
154
272
  throwIfAborted(signal);
273
+ const processRunner = hostLease && dispatch.sandbox
274
+ ? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
275
+ : undefined;
155
276
  const run = await this.providerCli.runDraft({
156
277
  dispatch: providerDispatch,
157
278
  worktree,
158
279
  runtimeEnvironment: hostLease?.environment,
280
+ processRunner,
159
281
  emit,
160
282
  signal
161
283
  });
@@ -183,18 +305,32 @@ export class BridgeSessionService {
183
305
  dispatch: providerDispatch,
184
306
  worktree,
185
307
  runtimeEnvironment: hostLease?.environment,
308
+ processRunner: hostLease && dispatch.sandbox
309
+ ? this.hostRuntime?.providerProcessRunner?.(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken) ?? undefined
310
+ : undefined,
186
311
  emit,
187
312
  signal
188
313
  };
189
- const run = isQa
190
- ? await this.providerCli.runQa(context)
191
- : isArchitect
192
- ? await this.providerCli.runArchitect(context)
193
- : isCiRepair
194
- ? await this.providerCli.runCiRepair(context)
195
- : isConflictResolve
196
- ? await this.providerCli.runConflictResolve(context)
197
- : await this.providerCli.runImplementation(context);
314
+ let run;
315
+ try {
316
+ run = isQa
317
+ ? await this.providerCli.runQa(context)
318
+ : isArchitect
319
+ ? await this.providerCli.runArchitect(context)
320
+ : isCiRepair
321
+ ? await this.providerCli.runCiRepair(context)
322
+ : isConflictResolve
323
+ ? await this.providerCli.runConflictResolve(context)
324
+ : await this.providerCli.runImplementation(context);
325
+ }
326
+ catch (error) {
327
+ const message = failureMessage(error);
328
+ if (message.includes("attempt_process_supervisor_start_")
329
+ || message.includes("host_process_supervisor_start_")) {
330
+ throw new Error(`provider_start_unavailable: ${message}`, { cause: error });
331
+ }
332
+ throw error;
333
+ }
198
334
  throwIfAborted(signal);
199
335
  const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
200
336
  return redactRunOnceResultPacket({
@@ -256,6 +392,143 @@ export class BridgeSessionService {
256
392
  }
257
393
  }
258
394
  }
395
+ function failureMessage(error) {
396
+ return error instanceof Error ? error.message : String(error);
397
+ }
398
+ function readinessProbeDispatch(request) {
399
+ return {
400
+ schemaVersion: 2,
401
+ userId: request.userId,
402
+ workItem: {
403
+ id: request.projectId,
404
+ kind: "runtime_readiness",
405
+ title: "Automatic host-isolation readiness check",
406
+ description: "Internal readiness proof without a provider agent.",
407
+ acceptanceCriteria: "",
408
+ reproSteps: "",
409
+ systemInfo: "",
410
+ },
411
+ repo: {
412
+ id: request.repoId,
413
+ name: request.repoName,
414
+ remoteUrl: request.remoteUrl,
415
+ defaultBranch: request.defaultBranch,
416
+ cloneOnDemand: request.cloneOnDemand,
417
+ serverLocalPath: request.runnerRepoLocalPath,
418
+ },
419
+ cycleRun: {
420
+ id: request.requestId,
421
+ ownerType: "project",
422
+ ownerId: request.projectId,
423
+ kind: "runtime_readiness",
424
+ phase: "readiness_probe",
425
+ requestedBase: request.revision,
426
+ baseBranch: request.revision,
427
+ attempt: request.attempt,
428
+ model: "none",
429
+ },
430
+ session: { id: request.requestId, model: "none", lane: "runtime_readiness" },
431
+ lease: {
432
+ id: request.requestId,
433
+ ownerType: "project",
434
+ ownerId: request.projectId,
435
+ worktreeKey: `runtime-readiness-${request.requestId}`,
436
+ lane: "runtime_readiness",
437
+ fencingToken: request.fencingToken,
438
+ expiresAt: request.expiresAt,
439
+ },
440
+ execution: {
441
+ provider: "none",
442
+ model: "none",
443
+ authKind: "none",
444
+ location: "bridge",
445
+ runnerId: request.runnerId,
446
+ repoId: request.repoId,
447
+ role: "runtime_readiness",
448
+ lane: "runtime_readiness",
449
+ requiresApiKeySecretGrant: false,
450
+ runnerRepoLocalPath: request.runnerRepoLocalPath,
451
+ },
452
+ runtimeIntent: { executionClass: "managed_runtime", applicationRuntimeRequired: true },
453
+ serverLocal: { repoRoot: request.repoRoot, cleanupWorktrees: true },
454
+ sandbox: {
455
+ dispatchAttemptId: request.requestId,
456
+ executionLeaseId: request.requestId,
457
+ runnerInstanceId: request.runnerInstanceId,
458
+ executionMode: "host",
459
+ executionPolicy: "host_allowed",
460
+ networkPolicy: { mode: "offline", allowedHosts: [] },
461
+ requestedResources: {
462
+ sandboxSlots: 1,
463
+ cpuMillis: 2000,
464
+ workerMemoryLimitBytes: 1024 * 1024 * 1024,
465
+ serviceMemoryLimitBytes: 1024 * 1024 * 1024,
466
+ runtimeOverheadBytes: 256 * 1024 * 1024,
467
+ memoryBytes: 2 * 1024 * 1024 * 1024,
468
+ workspaceBytes: 10 * 1024 * 1024 * 1024,
469
+ pids: 512,
470
+ buildSlots: 1,
471
+ },
472
+ publicationMode: "shared_filesystem",
473
+ },
474
+ artifacts: [],
475
+ };
476
+ }
477
+ function failedProbe(reasonCode, reasonMessage) {
478
+ return { status: "failed", reasonCode, reasonMessage, checkedAt: new Date().toISOString() };
479
+ }
480
+ function assertReadinessProbeStartupBudget(expiresAt) {
481
+ const remainingMs = Date.parse(expiresAt) - Date.now();
482
+ if (!Number.isFinite(remainingMs) || remainingMs < minimumReadinessProbeStartupBudgetMs) {
483
+ 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.");
484
+ }
485
+ }
486
+ function managedQaLaunchTarget(runtimeContract) {
487
+ const playwright = runtimeContract.contract.runtime.playwright;
488
+ if (!playwright)
489
+ return null;
490
+ const port = runtimeContract.contract.runtime.ports.find(candidate => candidate.name === playwright.baseUrlPort);
491
+ return port?.launch && port.baseUrlEnv && port.format
492
+ ? { managedRuntimePort: port.name, routes: ["/"] }
493
+ : null;
494
+ }
495
+ function withMaterializedManagedQaTarget(dispatch, hostLease) {
496
+ const target = dispatch.qaBrowser?.launchTarget;
497
+ const portName = target?.managed_runtime_port?.trim().toLowerCase();
498
+ if (!target || !portName)
499
+ return dispatch;
500
+ if (!hostLease) {
501
+ throw new Error("qa_managed_runtime_unavailable: Browser QA requires the verified managed host runtime selected during readiness.");
502
+ }
503
+ const normalizedName = portName.toUpperCase().replaceAll(/[^A-Z0-9]/gu, "_");
504
+ const url = hostLease.environment[`FORGE_${normalizedName}_URL`];
505
+ if (!url) {
506
+ throw new Error(`qa_managed_runtime_unavailable: verified runtime port '${portName}' was not launched for this attempt.`);
507
+ }
508
+ return {
509
+ ...dispatch,
510
+ qaBrowser: dispatch.qaBrowser ? {
511
+ ...dispatch.qaBrowser,
512
+ launchTarget: {
513
+ ...target,
514
+ url: target.url ?? url,
515
+ ready_url: target.ready_url ?? url,
516
+ notes: [target.notes, "Forge launched and verified this URL inside the current fenced attempt."]
517
+ .filter(Boolean)
518
+ .join(" "),
519
+ },
520
+ } : dispatch.qaBrowser,
521
+ };
522
+ }
523
+ function probeFailureCode(error) {
524
+ const message = error instanceof Error ? error.message : String(error);
525
+ const candidate = message.split(":", 1)[0]?.trim().toLowerCase() ?? "";
526
+ return /^[a-z][a-z0-9_.-]{2,159}$/u.test(candidate) ? candidate : "host_runtime_probe_failed";
527
+ }
528
+ function probeFailureMessage(error) {
529
+ const message = error instanceof Error ? error.message : String(error);
530
+ return message.length > 1000 ? `${message.slice(0, 997)}...` : message;
531
+ }
259
532
  function throwIfAborted(signal) {
260
533
  if (signal?.aborted) {
261
534
  throw new Error(String(signal.reason ?? "Dispatch cancelled."));
@@ -7,6 +7,7 @@ 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;
@@ -25,6 +26,10 @@ export interface RunOnceDispatchPacket {
25
26
  readonly designerRefs?: DispatchDesignerRefHandoffContext | null;
26
27
  readonly draft?: DispatchDraftPrompt | null;
27
28
  }
29
+ export interface DispatchRuntimeIntent {
30
+ readonly executionClass: "analysis_only" | "managed_runtime";
31
+ readonly applicationRuntimeRequired: boolean;
32
+ }
28
33
  export interface DispatchSandboxContext {
29
34
  readonly dispatchAttemptId: string;
30
35
  readonly executionLeaseId: string;
@@ -105,6 +110,7 @@ export interface DispatchQaAppLaunchTarget {
105
110
  readonly command?: string | null;
106
111
  readonly working_directory?: string | null;
107
112
  readonly ready_url?: string | null;
113
+ readonly managed_runtime_port?: string | null;
108
114
  readonly routes: readonly string[];
109
115
  readonly notes?: string | null;
110
116
  }
@@ -385,4 +391,36 @@ export interface RunnerEventLine {
385
391
  readonly event: RunnerDispatchEvent;
386
392
  }
387
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
+ }
388
426
  export declare function failureResult(summary: string, events?: readonly RunnerDispatchEvent[]): RunOnceResultPacket;
@@ -14,6 +14,7 @@ export function parseRunOnceDispatchPacket(value) {
14
14
  session: parseSession(packet.session),
15
15
  lease: parseLease(packet.lease),
16
16
  execution: parseExecution(packet.execution),
17
+ runtimeIntent: parseRuntimeIntent(packet.runtimeIntent),
17
18
  serverLocal: parseServerLocal(packet.serverLocal),
18
19
  sandbox: parseSandbox(packet.sandbox, schemaVersion),
19
20
  pullRequest: parsePullRequest(packet.pullRequest),
@@ -33,6 +34,21 @@ export function parseRunOnceDispatchPacket(value) {
33
34
  draft: parseDraftPrompt(packet.draft)
34
35
  };
35
36
  }
37
+ function parseRuntimeIntent(value) {
38
+ if (value === undefined || value === null) {
39
+ return { executionClass: "managed_runtime", applicationRuntimeRequired: true };
40
+ }
41
+ const intent = asObject(value, "runtimeIntent");
42
+ const executionClass = requiredString(intent, "executionClass").trim().toLowerCase();
43
+ if (executionClass !== "analysis_only" && executionClass !== "managed_runtime") {
44
+ throw new Error("runtimeIntent.executionClass must be analysis_only or managed_runtime.");
45
+ }
46
+ const applicationRuntimeRequired = requiredBoolean(intent, "applicationRuntimeRequired");
47
+ if ((executionClass === "analysis_only") === applicationRuntimeRequired) {
48
+ throw new Error("runtimeIntent execution class and application runtime requirement are inconsistent.");
49
+ }
50
+ return { executionClass, applicationRuntimeRequired };
51
+ }
36
52
  function parseSandbox(value, schemaVersion) {
37
53
  if (value === undefined || value === null) {
38
54
  if (schemaVersion === 2) {
@@ -406,6 +422,7 @@ function parseQaAppLaunchTarget(value) {
406
422
  command: optionalString(launchTarget, "command"),
407
423
  working_directory: optionalString(launchTarget, "working_directory"),
408
424
  ready_url: optionalString(launchTarget, "ready_url"),
425
+ managed_runtime_port: optionalString(launchTarget, "managed_runtime_port"),
409
426
  routes: Array.isArray(routes)
410
427
  ? routes.map((entry, index) => {
411
428
  if (typeof entry !== "string") {