@loucompanion/forge-bridge 0.1.3-dev.9d8f2e7e5241 → 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 (143) hide show
  1. package/README.md +83 -0
  2. package/dist/bridge/bridge.base.d.ts +4 -1
  3. package/dist/bridge/bridge.service.d.ts +55 -3
  4. package/dist/bridge/bridge.service.js +834 -87
  5. package/dist/bridge/bridge.types.d.ts +89 -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 +37 -3
  16. package/dist/cli/cli.service.d.ts +33 -2
  17. package/dist/cli/cli.service.js +209 -27
  18. package/dist/cli/local-config.d.ts +5 -0
  19. package/dist/cli/local-config.js +45 -1
  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/preflight.d.ts +45 -1
  53. package/dist/sandbox/preflight.js +299 -34
  54. package/dist/sandbox/process.js +21 -2
  55. package/dist/sandbox/repository-lock.js +41 -3
  56. package/dist/sandbox/runtime.d.ts +66 -3
  57. package/dist/sandbox/runtime.js +670 -105
  58. package/dist/sandbox/sandbox.types.d.ts +10 -0
  59. package/dist/sandbox/smoke.d.ts +76 -0
  60. package/dist/sandbox/smoke.js +380 -0
  61. package/dist/sandbox/worker-supervisor.js +1 -1
  62. package/dist/sandbox/worker.js +2 -5
  63. package/dist/sandbox/workspace.service.d.ts +1 -0
  64. package/dist/sandbox/workspace.service.js +28 -5
  65. package/dist/session/managed-attempt-tools.d.ts +21 -0
  66. package/dist/session/managed-attempt-tools.js +912 -0
  67. package/dist/session/managed-build-coordinator.d.ts +5 -0
  68. package/dist/session/managed-build-coordinator.js +105 -0
  69. package/dist/session/managed-build-receipt.d.ts +21 -0
  70. package/dist/session/managed-build-receipt.js +335 -0
  71. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  72. package/dist/session/phase-completion.artifacts.js +233 -24
  73. package/dist/session/phase-completion.d.ts +12 -2
  74. package/dist/session/phase-completion.helpers.d.ts +5 -0
  75. package/dist/session/phase-completion.helpers.js +51 -10
  76. package/dist/session/phase-completion.js +14 -3
  77. package/dist/session/phase-completion.prompts.d.ts +3 -3
  78. package/dist/session/phase-completion.prompts.js +24 -8
  79. package/dist/session/phase-completion.runtime.d.ts +13 -1
  80. package/dist/session/phase-completion.runtime.js +189 -8
  81. package/dist/session/phase-completion.schemas.js +117 -4
  82. package/dist/session/phase-completion.validation.d.ts +4 -1
  83. package/dist/session/phase-completion.validation.js +153 -22
  84. package/dist/session/pre-publication-validation.d.ts +46 -0
  85. package/dist/session/pre-publication-validation.js +208 -0
  86. package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
  87. package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
  88. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
  89. package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
  90. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  91. package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
  92. package/dist/session/provider-cli/provider-cli.process.js +463 -34
  93. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  94. package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
  95. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  96. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  97. package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
  98. package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
  99. package/dist/session/provider-cli/provider-cli.service.js +529 -100
  100. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  101. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  102. package/dist/session/provider-cli/provider-output-state.js +91 -16
  103. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  104. package/dist/session/provider-cli/provider-usage.js +28 -5
  105. package/dist/session/session-redaction.d.ts +3 -0
  106. package/dist/session/session-redaction.js +38 -0
  107. package/dist/session/session.base.d.ts +6 -1
  108. package/dist/session/session.service.d.ts +8 -2
  109. package/dist/session/session.service.js +522 -41
  110. package/dist/session/session.types.d.ts +72 -0
  111. package/dist/session/session.types.js +74 -3
  112. package/dist/shared/format/format-bytes.d.ts +1 -0
  113. package/dist/shared/format/format-bytes.js +9 -0
  114. package/dist/shared/path/path.d.ts +1 -0
  115. package/dist/shared/path/path.js +21 -1
  116. package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
  117. package/dist/shared/process/dotnet-toolchain.js +179 -0
  118. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  119. package/dist/shared/process/host-process-recovery.js +78 -0
  120. package/dist/shared/process/host-process-supervisor.d.ts +59 -0
  121. package/dist/shared/process/host-process-supervisor.js +330 -0
  122. package/dist/shared/process/process.d.ts +1 -0
  123. package/dist/shared/process/process.js +1 -0
  124. package/dist/shared/redaction/redaction.js +12 -2
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/dist/worktree/artifact-injector.d.ts +3 -2
  128. package/dist/worktree/artifact-injector.js +43 -4
  129. package/dist/worktree/dependency-warmer.d.ts +28 -1
  130. package/dist/worktree/dependency-warmer.js +308 -9
  131. package/dist/worktree/index.d.ts +2 -1
  132. package/dist/worktree/index.js +1 -0
  133. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  134. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  135. package/dist/worktree/worktree-paths.d.ts +4 -0
  136. package/dist/worktree/worktree-paths.js +30 -0
  137. package/dist/worktree/worktree.base.d.ts +3 -0
  138. package/dist/worktree/worktree.service.d.ts +13 -2
  139. package/dist/worktree/worktree.service.js +238 -47
  140. package/dist/worktree/worktree.types.d.ts +5 -1
  141. package/package.json +5 -3
  142. package/runtime/windows-job-supervisor.ps1 +444 -0
  143. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,42 +1,84 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { RepositoryCoordinatorLock } from "../../sandbox/repository-lock.js";
3
4
  import { GitClient } from "../../shared/git/git.js";
4
5
  import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
6
+ import { defaultWorktreeCoordinatorStateRoot } from "../../worktree/worktree-paths.js";
5
7
  import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
6
- import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle, summarizeStderr, waitForCompletionArtifact } from "./provider-cli.artifacts.js";
8
+ import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle, waitForCompletionArtifact } from "./provider-cli.artifacts.js";
7
9
  import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
8
- import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
10
+ import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
9
11
  import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
10
12
  import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
11
13
  import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
14
+ import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
15
+ import { PrePublicationValidator, } from "../pre-publication-validation.js";
12
16
  export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
13
17
  export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
14
- export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, StreamingProviderProcessRunner } from "./provider-cli.process.js";
18
+ export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, PROVIDER_OUTPUT_CAPTURE_LIMIT_CHARS, StreamingProviderProcessRunner } from "./provider-cli.process.js";
15
19
  export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
16
- async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
17
- try {
18
- await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
19
- return;
20
- }
21
- catch (err) {
22
- if (!isNonFastForwardPushError(err)) {
23
- throw err;
20
+ async function pushWorktreeBranch(git, worktreePath, branchName, repoPath, coordinator, signal, emit, expectedRemoteHeadSha) {
21
+ await coordinator.withLock(repoPath, async () => {
22
+ if (expectedRemoteHeadSha) {
23
+ const expected = expectedRemoteHeadSha.trim().toLowerCase();
24
+ const remoteListing = await git.run(worktreePath, ["ls-remote", "--heads", "origin", `refs/heads/${branchName}`]);
25
+ const observed = remoteListing.trim().split(/\s+/u)[0]?.toLowerCase() ?? "";
26
+ if (observed !== expected) {
27
+ throw new Error(`stale_head: repair publication expected remote ${branchName} at ${expected}, but observed ${observed || "no branch"}; refresh provider state before retrying.`);
28
+ }
29
+ try {
30
+ await git.run(worktreePath, [
31
+ "push",
32
+ "--porcelain",
33
+ "origin",
34
+ `HEAD:refs/heads/${branchName}`,
35
+ `--force-with-lease=refs/heads/${branchName}:${expected}`,
36
+ ]);
37
+ }
38
+ catch (error) {
39
+ throw new Error(`stale_head: repair publication for ${branchName} lost its expected-head lease; Forge did not rebase or publish the stale repair: ${errorMessage(error)}`, { cause: error });
40
+ }
41
+ return;
24
42
  }
25
- await emit?.({
26
- role: "runner",
27
- kind: "checkpoint",
28
- content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
29
- eventType: "worktree_branch_push_rebase"
30
- });
31
43
  try {
32
- await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
33
- await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
34
44
  await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
45
+ return;
35
46
  }
36
- catch (retryErr) {
37
- throw new Error(`git push origin HEAD:${branchName} was rejected because the remote branch moved, and automatic fetch/rebase failed: ${errorMessage(retryErr)}`, { cause: retryErr });
47
+ catch (err) {
48
+ if (!isNonFastForwardPushError(err)) {
49
+ throw err;
50
+ }
51
+ await emit?.({
52
+ role: "runner",
53
+ kind: "checkpoint",
54
+ content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
55
+ eventType: "worktree_branch_push_rebase"
56
+ });
57
+ try {
58
+ await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
59
+ await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
60
+ await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
61
+ }
62
+ catch (retryErr) {
63
+ throw new Error(`worktree_branch_integration_conflict: git push origin HEAD:${branchName} was rejected because the remote branch moved, and automatic fetch/rebase failed: ${errorMessage(retryErr)}`, { cause: retryErr });
64
+ }
38
65
  }
66
+ }, signal);
67
+ }
68
+ function providerOwnsBranchPublication(context) {
69
+ return context.dispatch.sandbox?.executionMode !== "docker";
70
+ }
71
+ function prePublicationExecution(context, environment, defaultRunner) {
72
+ if (context.processRunner)
73
+ return { environment, processRunner: context.processRunner };
74
+ if (context.dispatch.sandbox?.executionMode !== "docker")
75
+ return { environment };
76
+ const expectedSandboxId = context.dispatch.sandbox.dispatchAttemptId;
77
+ const observedSandboxId = context.runtimeEnvironment?.FORGE_SANDBOX_ID ?? process.env.FORGE_SANDBOX_ID;
78
+ if (observedSandboxId !== expectedSandboxId) {
79
+ throw new Error(`prepublication_execution_scope_invalid: Docker attempt ${expectedSandboxId} has no runner proven to execute inside that sandbox.`);
39
80
  }
81
+ return { environment, processRunner: defaultRunner };
40
82
  }
41
83
  function remoteTrackingRefspec(branchName) {
42
84
  return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
@@ -52,6 +94,226 @@ function isNonFastForwardPushError(err) {
52
94
  function errorMessage(err) {
53
95
  return err instanceof Error ? err.message : String(err);
54
96
  }
97
+ function providerCliExitFailure(command, result) {
98
+ const output = [result.stderr.trim(), result.stdout.trim()]
99
+ .filter(value => value.length > 0)
100
+ .join("\n");
101
+ const detail = clip(output, 1200);
102
+ const suffix = detail ? `: ${detail}` : "";
103
+ if (isProviderNetworkFailure(output)) {
104
+ return new Error(`provider_network_unavailable: ${command} could not reach its provider service${suffix}`);
105
+ }
106
+ return new Error(`${command} exited ${result.exitCode}${suffix}`);
107
+ }
108
+ export function isProviderNetworkFailure(output) {
109
+ const normalized = output.trim().toLowerCase();
110
+ if (!normalized) {
111
+ return false;
112
+ }
113
+ return [
114
+ "eai_again",
115
+ "enotfound",
116
+ "could not resolve host",
117
+ "failed to lookup address",
118
+ "name or service not known",
119
+ "no such host is known",
120
+ "temporary failure in name resolution",
121
+ "network is unreachable",
122
+ "no route to host",
123
+ "error sending request for url",
124
+ "client error (connect)",
125
+ "connection refused",
126
+ "connection reset by peer",
127
+ "stream disconnected before completion",
128
+ ].some(marker => normalized.includes(marker));
129
+ }
130
+ const defaultProviderToolHeartbeatIntervalMs = 60_000;
131
+ const defaultManagedVerificationProgressIntervalMs = 30_000;
132
+ function compareManagedVerificationSnapshots(left, right) {
133
+ const leftOrder = managedVerificationStartedAt(left.startedAt) ?? Number.MAX_SAFE_INTEGER;
134
+ const rightOrder = managedVerificationStartedAt(right.startedAt) ?? Number.MAX_SAFE_INTEGER;
135
+ return leftOrder - rightOrder || left.commandId.localeCompare(right.commandId);
136
+ }
137
+ function managedVerificationStartedAt(value) {
138
+ const parsed = Date.parse(value ?? "");
139
+ return Number.isFinite(parsed) ? parsed : null;
140
+ }
141
+ export class ManagedVerificationProgressEmitter {
142
+ logDirectory;
143
+ emit;
144
+ intervalMs;
145
+ timer = null;
146
+ observedStatuses = new Map();
147
+ completedCommands = new Set();
148
+ constructor(logDirectory, emit, intervalMs = defaultManagedVerificationProgressIntervalMs) {
149
+ this.logDirectory = logDirectory;
150
+ this.emit = emit;
151
+ this.intervalMs = intervalMs;
152
+ }
153
+ start() {
154
+ if (!this.logDirectory?.trim() || this.timer)
155
+ return;
156
+ this.timer = setInterval(() => {
157
+ void this.scan().catch(() => undefined);
158
+ }, Math.max(1, this.intervalMs));
159
+ void this.scan().catch(() => undefined);
160
+ }
161
+ async stop() {
162
+ if (this.timer) {
163
+ clearInterval(this.timer);
164
+ this.timer = null;
165
+ }
166
+ await this.scan().catch(() => undefined);
167
+ }
168
+ async scan() {
169
+ if (!this.logDirectory?.trim())
170
+ return;
171
+ const directory = path.join(this.logDirectory, "managed-commands");
172
+ const names = await fs.readdir(directory).catch(() => []);
173
+ const snapshots = await Promise.all(names
174
+ .filter(name => name.endsWith(".receipt.json"))
175
+ .map(async (name) => parseManagedVerificationSnapshot(path.join(directory, name))));
176
+ const current = snapshots
177
+ .filter((entry) => entry !== null)
178
+ .sort(compareManagedVerificationSnapshots);
179
+ if (current.some(snapshot => isManagedVerificationNonterminal(snapshot.status))) {
180
+ // Durable managed-command receipts are authoritative evidence that Forge
181
+ // still supervises work even when the provider is not inside an MCP tool
182
+ // call. Refresh the API idle deadline without inflating the transcript.
183
+ await this.emit({
184
+ role: "runner",
185
+ kind: "status",
186
+ content: "A supervised managed verification command is still running.",
187
+ eventType: "provider_tool_heartbeat",
188
+ });
189
+ }
190
+ for (const snapshot of current) {
191
+ await this.emitTransition(snapshot);
192
+ }
193
+ }
194
+ async emitTransition(current) {
195
+ const terminal = ["pass", "fail", "timed_out", "timeout_termination_failed", "runner_error"].includes(current.status);
196
+ if (terminal && this.completedCommands.has(current.commandId))
197
+ return;
198
+ const previousStatus = this.observedStatuses.get(current.commandId);
199
+ if (previousStatus === current.status)
200
+ return;
201
+ this.observedStatuses.set(current.commandId, current.status);
202
+ if (terminal)
203
+ this.completedCommands.add(current.commandId);
204
+ // Starting and running are one externally meaningful state. The receipt is
205
+ // still refreshed on disk for UI telemetry, but repeated output/elapsed
206
+ // changes must not inflate the provider transcript or session history.
207
+ if (!terminal && previousStatus === "starting" && current.status === "running")
208
+ return;
209
+ const startedAt = managedVerificationStartedAt(current.startedAt);
210
+ const elapsedMs = startedAt !== null
211
+ ? Math.max(current.elapsedMs ?? 0, Date.now() - startedAt)
212
+ : current.elapsedMs;
213
+ const elapsed = elapsedMs === null ? "an unknown duration" : formatElapsed(elapsedMs);
214
+ const outputTail = current.lastOutputStream === "stderr"
215
+ ? current.stderrTail || current.stdoutTail
216
+ : current.stdoutTail || current.stderrTail;
217
+ const recovered = previousStatus === undefined && terminal;
218
+ await this.emit({
219
+ role: "runner",
220
+ kind: terminal && current.status !== "pass" ? "error" : "status",
221
+ content: terminal
222
+ ? `Managed verification ${current.status} after ${elapsed}${recovered ? " (recovered from its durable receipt)" : ""}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`
223
+ : `QA started managed verification ${current.commandId}. Forge is supervising it; durable progress remains available in the UI while the provider continues independent work.`,
224
+ eventType: terminal ? "verification_completed" : "verification_waiting",
225
+ data: JSON.stringify({
226
+ command_id: current.commandId,
227
+ status: current.status,
228
+ elapsed_ms: elapsedMs,
229
+ output_revision: current.outputRevision,
230
+ last_output_at: current.lastOutputAt,
231
+ recovered,
232
+ }),
233
+ });
234
+ }
235
+ }
236
+ function isManagedVerificationNonterminal(status) {
237
+ return status === "starting" || status === "running" || status === "waiting_for_build_slot";
238
+ }
239
+ async function parseManagedVerificationSnapshot(receiptPath) {
240
+ try {
241
+ const receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
242
+ const commandId = typeof receipt.command_id === "string" ? receipt.command_id : null;
243
+ const status = typeof receipt.status === "string" ? receipt.status : null;
244
+ if (!commandId || !status)
245
+ return null;
246
+ return {
247
+ commandId,
248
+ status,
249
+ elapsedMs: typeof receipt.elapsed_ms === "number" ? receipt.elapsed_ms : null,
250
+ startedAt: typeof receipt.started_at === "string" ? receipt.started_at : null,
251
+ outputRevision: typeof receipt.output_revision === "number" ? receipt.output_revision : null,
252
+ lastOutputAt: typeof receipt.last_output_at === "string" ? receipt.last_output_at : null,
253
+ lastOutputStream: receipt.last_output_stream === "stdout" || receipt.last_output_stream === "stderr"
254
+ ? receipt.last_output_stream
255
+ : null,
256
+ stdoutTail: tail(typeof receipt.stdout === "string" ? receipt.stdout : ""),
257
+ stderrTail: tail(typeof receipt.stderr === "string" ? receipt.stderr : ""),
258
+ };
259
+ }
260
+ catch {
261
+ return null;
262
+ }
263
+ }
264
+ function tail(value) {
265
+ return value.trim().split(/\r?\n/u).slice(-4).join("\n");
266
+ }
267
+ function formatElapsed(milliseconds) {
268
+ const seconds = Math.max(0, Math.round(milliseconds / 1000));
269
+ const minutes = Math.floor(seconds / 60);
270
+ return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
271
+ }
272
+ class ProviderToolActivityHeartbeat {
273
+ outputState;
274
+ emit;
275
+ intervalMs;
276
+ timer = null;
277
+ constructor(outputState, emit, intervalMs) {
278
+ this.outputState = outputState;
279
+ this.emit = emit;
280
+ this.intervalMs = intervalMs;
281
+ }
282
+ async consume(line) {
283
+ const events = this.outputState.consume(line);
284
+ this.sync();
285
+ for (const event of events) {
286
+ await this.emit(event);
287
+ }
288
+ }
289
+ stop() {
290
+ if (this.timer) {
291
+ clearInterval(this.timer);
292
+ this.timer = null;
293
+ }
294
+ }
295
+ sync() {
296
+ if (!this.outputState.hasActiveToolCall) {
297
+ this.stop();
298
+ return;
299
+ }
300
+ if (this.timer) {
301
+ return;
302
+ }
303
+ this.timer = setInterval(() => {
304
+ if (!this.outputState.hasActiveToolCall) {
305
+ this.stop();
306
+ return;
307
+ }
308
+ void this.emit({
309
+ role: "runner",
310
+ kind: "status",
311
+ content: "A supervised provider tool is still running.",
312
+ eventType: "provider_tool_heartbeat",
313
+ }).catch(() => undefined);
314
+ }, Math.max(1, this.intervalMs));
315
+ }
316
+ }
55
317
  const fakeWorkerModel = "server-local-fake-worker";
56
318
  export class RoutingProviderCliService {
57
319
  realProvider;
@@ -94,9 +356,15 @@ export class RoutingProviderCliService {
94
356
  export class ProcessBackedProviderCliService {
95
357
  processRunner;
96
358
  git;
97
- constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient()) {
359
+ providerToolHeartbeatIntervalMs;
360
+ publicationCoordinator;
361
+ prePublicationValidator;
362
+ constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs, publicationCoordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot()), prePublicationValidator = new PrePublicationValidator()) {
98
363
  this.processRunner = processRunner;
99
364
  this.git = git;
365
+ this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
366
+ this.publicationCoordinator = publicationCoordinator;
367
+ this.prePublicationValidator = prePublicationValidator;
100
368
  }
101
369
  async runDraft(context) {
102
370
  const { dispatch } = context;
@@ -110,15 +378,18 @@ export class ProcessBackedProviderCliService {
110
378
  await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
111
379
  const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
112
380
  const decomposeDraft = isDecomposeDraftDispatch(dispatch);
113
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft" });
381
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
114
382
  const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
115
- const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, draftPrompt);
383
+ const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
384
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
116
385
  const command = buildProviderCliCommand(dispatch, {
117
386
  worktreePath: worktree.worktreePath,
118
- prompt: pipePromptToStdin ? "-" : draftPrompt,
387
+ prompt: draftPrompt,
388
+ promptFromStdin: pipePromptToStdin,
119
389
  completionTools: completionTools ?? undefined,
120
390
  skipGitRepoCheck: false,
121
- imagePaths: stagedAttachments.imagePaths
391
+ imagePaths: stagedAttachments.imagePaths,
392
+ runtimeEnvironment: context.runtimeEnvironment
122
393
  });
123
394
  const draftArtifactSlugs = dispatch.artifacts
124
395
  .filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
@@ -133,7 +404,7 @@ export class ProcessBackedProviderCliService {
133
404
  role: "runner",
134
405
  kind: "checkpoint",
135
406
  content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
136
- eventType: "provider_cli_started"
407
+ eventType: "provider_cli_starting"
137
408
  });
138
409
  await context.emit({
139
410
  role: "runner",
@@ -144,6 +415,7 @@ export class ProcessBackedProviderCliService {
144
415
  eventType: "completion_tools_configured"
145
416
  });
146
417
  const outputState = createProviderOutputState(dispatch.execution.provider);
418
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
147
419
  try {
148
420
  const providerCancellation = new AbortController();
149
421
  const abortProvider = () => providerCancellation.abort();
@@ -154,17 +426,23 @@ export class ProcessBackedProviderCliService {
154
426
  context.signal?.addEventListener("abort", abortProvider, { once: true });
155
427
  }
156
428
  try {
157
- const runPromise = this.processRunner.run({
429
+ const runPromise = (context.processRunner ?? this.processRunner).run({
158
430
  command: command.command,
159
431
  args: command.args,
160
432
  cwd: worktree.worktreePath,
161
- env: providerProcessEnv(dispatch),
162
- stdin: pipePromptToStdin ? draftPrompt : undefined
433
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
434
+ stdin: pipePromptToStdin ? providerPrompt : undefined
163
435
  }, {
436
+ onStarted: async () => {
437
+ await context.emit({
438
+ role: "runner",
439
+ kind: "checkpoint",
440
+ content: "Provider CLI Draft is supervised and ready.",
441
+ eventType: "provider_cli_started"
442
+ });
443
+ },
164
444
  onStdoutLine: async (line) => {
165
- for (const event of outputState.consume(line)) {
166
- await context.emit(event);
167
- }
445
+ await toolHeartbeat.consume(line);
168
446
  }
169
447
  }, providerCancellation.signal);
170
448
  const artifactWaitCancellation = new AbortController();
@@ -199,7 +477,7 @@ export class ProcessBackedProviderCliService {
199
477
  context.signal?.removeEventListener("abort", abortArtifactWait);
200
478
  }
201
479
  if (result.exitCode !== 0 && !earlyProposalArtifact) {
202
- throw new Error(`Provider CLI exited with code ${result.exitCode}${summarizeStderr(result.stderr)}.`);
480
+ throw providerCliExitFailure(command.command, result);
203
481
  }
204
482
  await context.emit({
205
483
  role: "runner",
@@ -260,6 +538,7 @@ export class ProcessBackedProviderCliService {
260
538
  }
261
539
  }
262
540
  finally {
541
+ toolHeartbeat.stop();
263
542
  await cleanupCompletionTools(completionTools, context.emit);
264
543
  }
265
544
  }
@@ -278,21 +557,26 @@ export class ProcessBackedProviderCliService {
278
557
  const specPath = path.join(metadataRoot, "spec.md");
279
558
  await fs.writeFile(specPath, specBody, "utf8");
280
559
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
281
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
560
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase, runtimeEnvironment: context.runtimeEnvironment });
561
+ const implementationPrompt = debugCycle
562
+ ? buildDebugCyclePrompt(dispatch, phase)
563
+ : developCycle
564
+ ? buildDevelopCyclePrompt(dispatch, phase)
565
+ : buildImplementationPrompt(dispatch, phase);
566
+ const providerPrompt = buildProviderCliPrompt(dispatch, implementationPrompt, context.runtimeEnvironment);
567
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
282
568
  const command = buildProviderCliCommand(dispatch, {
283
569
  worktreePath: worktree.worktreePath,
284
- prompt: debugCycle
285
- ? buildDebugCyclePrompt(dispatch, phase)
286
- : developCycle
287
- ? buildDevelopCyclePrompt(dispatch, phase)
288
- : buildImplementationPrompt(dispatch, phase),
289
- completionTools
570
+ prompt: implementationPrompt,
571
+ promptFromStdin: pipePromptToStdin,
572
+ completionTools,
573
+ runtimeEnvironment: context.runtimeEnvironment
290
574
  });
291
575
  await context.emit({
292
576
  role: "runner",
293
577
  kind: "checkpoint",
294
578
  content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
295
- eventType: "provider_cli_started"
579
+ eventType: "provider_cli_starting"
296
580
  });
297
581
  await context.emit({
298
582
  role: "runner",
@@ -305,30 +589,38 @@ export class ProcessBackedProviderCliService {
305
589
  eventType: "completion_tools_configured"
306
590
  });
307
591
  const outputState = createProviderOutputState(dispatch.execution.provider);
592
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
308
593
  const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
309
594
  let result;
310
595
  try {
311
596
  liveCompletionEvents.start();
312
- result = await this.processRunner.run({
597
+ result = await (context.processRunner ?? this.processRunner).run({
313
598
  command: command.command,
314
599
  args: command.args,
315
600
  cwd: worktree.worktreePath,
316
- env: providerProcessEnv(dispatch)
601
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
602
+ stdin: pipePromptToStdin ? providerPrompt : undefined
317
603
  }, {
604
+ onStarted: async () => {
605
+ await context.emit({
606
+ role: "runner",
607
+ kind: "checkpoint",
608
+ content: "Provider CLI is supervised and ready.",
609
+ eventType: "provider_cli_started"
610
+ });
611
+ },
318
612
  onStdoutLine: async (line) => {
319
- for (const event of outputState.consume(line)) {
320
- await context.emit(event);
321
- }
613
+ await toolHeartbeat.consume(line);
322
614
  }
323
615
  }, context.signal);
324
616
  }
325
617
  finally {
618
+ toolHeartbeat.stop();
326
619
  await liveCompletionEvents.stop();
327
620
  await cleanupCompletionTools(completionTools, context.emit);
328
621
  }
329
622
  if (result.exitCode !== 0) {
330
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
331
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
623
+ throw providerCliExitFailure(command.command, result);
332
624
  }
333
625
  await context.emit({
334
626
  role: "runner",
@@ -337,9 +629,12 @@ export class ProcessBackedProviderCliService {
337
629
  eventType: "provider_cli_completed"
338
630
  });
339
631
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
632
+ if (developCycle || phase === "implementation") {
633
+ await this.prePublicationValidator.validate(worktree.worktreePath, "implementation", publicationCapabilities(context, context.runtimeEnvironment), context.emit, context.signal, prePublicationExecution(context, providerProcessEnv(dispatch, context.runtimeEnvironment), this.processRunner));
634
+ }
340
635
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
341
- if (developCycle || debugCycle || phase === "implementation") {
342
- await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
636
+ if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
637
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit);
343
638
  await context.emit({
344
639
  role: "runner",
345
640
  kind: "checkpoint",
@@ -397,18 +692,23 @@ export class ProcessBackedProviderCliService {
397
692
  const specPath = path.join(metadataRoot, "spec.md");
398
693
  await fs.writeFile(specPath, specBody, "utf8");
399
694
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
400
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
695
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect", runtimeEnvironment: context.runtimeEnvironment });
696
+ const architectPrompt = buildArchitectPrompt(dispatch);
697
+ const providerPrompt = buildProviderCliPrompt(dispatch, architectPrompt, context.runtimeEnvironment);
698
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
401
699
  const command = buildProviderCliCommand(dispatch, {
402
700
  worktreePath: worktree.worktreePath,
403
- prompt: buildArchitectPrompt(dispatch),
404
- completionTools
701
+ prompt: architectPrompt,
702
+ promptFromStdin: pipePromptToStdin,
703
+ completionTools,
704
+ runtimeEnvironment: context.runtimeEnvironment
405
705
  });
406
706
  const commandText = formatProviderCliCommandForCheckpoint(command);
407
707
  await context.emit({
408
708
  role: "runner",
409
709
  kind: "checkpoint",
410
710
  content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
411
- eventType: "provider_cli_started"
711
+ eventType: "provider_cli_starting"
412
712
  });
413
713
  await context.emit({
414
714
  role: "runner",
@@ -417,30 +717,36 @@ export class ProcessBackedProviderCliService {
417
717
  eventType: "completion_tools_configured"
418
718
  });
419
719
  const outputState = createProviderOutputState(dispatch.execution.provider);
720
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
420
721
  const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
421
722
  let result;
422
723
  try {
423
724
  liveArchitectEvents.start();
424
- result = await this.processRunner.run({
725
+ result = await (context.processRunner ?? this.processRunner).run({
425
726
  command: command.command,
426
727
  args: command.args,
427
728
  cwd: worktree.worktreePath,
428
- env: providerProcessEnv(dispatch)
729
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
730
+ stdin: pipePromptToStdin ? providerPrompt : undefined
429
731
  }, {
430
- onStdoutLine: async (line) => {
431
- for (const event of outputState.consume(line)) {
432
- await context.emit(event);
433
- }
434
- }
732
+ onStarted: async () => {
733
+ await context.emit({
734
+ role: "runner",
735
+ kind: "checkpoint",
736
+ content: "Provider CLI Architect is supervised and ready.",
737
+ eventType: "provider_cli_started"
738
+ });
739
+ },
740
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
435
741
  }, context.signal);
436
742
  }
437
743
  finally {
744
+ toolHeartbeat.stop();
438
745
  await liveArchitectEvents.stop();
439
746
  await cleanupCompletionTools(completionTools, context.emit);
440
747
  }
441
748
  if (result.exitCode !== 0) {
442
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
443
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
749
+ throw providerCliExitFailure(command.command, result);
444
750
  }
445
751
  await context.emit({
446
752
  role: "runner",
@@ -488,18 +794,23 @@ export class ProcessBackedProviderCliService {
488
794
  const specPath = path.join(metadataRoot, "spec.md");
489
795
  await fs.writeFile(specPath, specBody, "utf8");
490
796
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
491
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
797
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa", runtimeEnvironment: context.runtimeEnvironment });
798
+ const qaPrompt = buildQaPrompt(dispatch, specBody);
799
+ const providerPrompt = buildProviderCliPrompt(dispatch, qaPrompt, context.runtimeEnvironment);
800
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
492
801
  const command = buildProviderCliCommand(dispatch, {
493
802
  worktreePath: worktree.worktreePath,
494
- prompt: buildQaPrompt(dispatch, specBody),
495
- completionTools
803
+ prompt: qaPrompt,
804
+ promptFromStdin: pipePromptToStdin,
805
+ completionTools,
806
+ runtimeEnvironment: context.runtimeEnvironment
496
807
  });
497
808
  const commandText = formatProviderCliCommandForCheckpoint(command);
498
809
  await context.emit({
499
810
  role: "runner",
500
811
  kind: "checkpoint",
501
812
  content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
502
- eventType: "provider_cli_started"
813
+ eventType: "provider_cli_starting"
503
814
  });
504
815
  await context.emit({
505
816
  role: "runner",
@@ -508,30 +819,39 @@ export class ProcessBackedProviderCliService {
508
819
  eventType: "completion_tools_configured"
509
820
  });
510
821
  const outputState = createProviderOutputState(dispatch.execution.provider);
822
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
823
+ const verificationProgress = new ManagedVerificationProgressEmitter(context.runtimeEnvironment?.FORGE_LOG_DIR, context.emit);
511
824
  const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
512
825
  let result;
513
826
  try {
514
827
  liveQaEvents.start();
515
- result = await this.processRunner.run({
828
+ verificationProgress.start();
829
+ result = await (context.processRunner ?? this.processRunner).run({
516
830
  command: command.command,
517
831
  args: command.args,
518
832
  cwd: worktree.worktreePath,
519
- env: providerProcessEnv(dispatch)
833
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
834
+ stdin: pipePromptToStdin ? providerPrompt : undefined
520
835
  }, {
521
- onStdoutLine: async (line) => {
522
- for (const event of outputState.consume(line)) {
523
- await context.emit(event);
524
- }
525
- }
836
+ onStarted: async () => {
837
+ await context.emit({
838
+ role: "runner",
839
+ kind: "checkpoint",
840
+ content: "Provider CLI QA is supervised and ready.",
841
+ eventType: "provider_cli_started"
842
+ });
843
+ },
844
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
526
845
  }, context.signal);
527
846
  }
528
847
  finally {
848
+ toolHeartbeat.stop();
849
+ await verificationProgress.stop();
529
850
  await liveQaEvents.stop();
530
851
  await cleanupCompletionTools(completionTools, context.emit);
531
852
  }
532
853
  if (result.exitCode !== 0) {
533
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
534
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
854
+ throw providerCliExitFailure(command.command, result);
535
855
  }
536
856
  await context.emit({
537
857
  role: "runner",
@@ -547,7 +867,7 @@ export class ProcessBackedProviderCliService {
547
867
  ? "Provider CLI QA completed but did not produce the complete durable QA artifact bundle."
548
868
  : "Provider CLI QA completed but did not call the Forge QA completion tool.");
549
869
  }
550
- enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
870
+ await enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
551
871
  if (!liveQaEvents.hasEmitted()) {
552
872
  await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
553
873
  await emitQaStructuredEvents(context.emit, qaArtifact);
@@ -579,41 +899,132 @@ export class ProcessBackedProviderCliService {
579
899
  }
580
900
  async runPullRequestRepair(context, route) {
581
901
  const { dispatch, worktree } = context;
902
+ const preparedHeadSha = (await this.git.run(worktree.worktreePath, ["rev-parse", "HEAD"])).trim();
903
+ const expectedRepairHeadSha = dispatch.cycleRun.repairHeadSha?.trim() || preparedHeadSha;
904
+ if (dispatch.cycleRun.repairHeadSha) {
905
+ if (preparedHeadSha.toLowerCase() !== expectedRepairHeadSha.toLowerCase()) {
906
+ throw new Error(`stale_head: repair dispatch expected ${expectedRepairHeadSha} but prepared worktree is ${preparedHeadSha}; refresh provider state before retrying.`);
907
+ }
908
+ }
582
909
  const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
583
910
  const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
911
+ let repairSpec = null;
912
+ let repairEnvironment = context.runtimeEnvironment;
913
+ let completionTools = null;
914
+ if (repairChecks) {
915
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
916
+ await fs.mkdir(metadataRoot, { recursive: true });
917
+ const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
918
+ if (!laneCommand) {
919
+ throw new Error(`CI repair readiness failed: lane command for '${dispatch.execution.lane}' was not resolved, so the approved spec cannot be materialized.`);
920
+ }
921
+ const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
922
+ const specPath = path.join(metadataRoot, "spec.md");
923
+ await fs.writeFile(specPath, specBody, "utf8");
924
+ await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
925
+ repairSpec = {
926
+ repoPath: worktree.worktreePath,
927
+ path: specPath,
928
+ body: specBody,
929
+ producedBy: "forge-lane-command",
930
+ status: "approved",
931
+ };
932
+ const readiness = await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
933
+ repairEnvironment = readiness.environment;
934
+ completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "ci_repair", runtimeEnvironment: repairEnvironment });
935
+ await context.emit({
936
+ role: "runner",
937
+ kind: "checkpoint",
938
+ content: `CI repair readiness contract prepared at ${path.relative(worktree.worktreePath, readiness.path).replaceAll(path.sep, "/")}; approved spec, attempt URLs, browser readiness, and local-equivalence policy are available before provider start.`,
939
+ eventType: "ci_repair_readiness_ready",
940
+ });
941
+ await context.emit({
942
+ role: "runner",
943
+ kind: "checkpoint",
944
+ content: "Forge CI repair completion tool configured; provider must call forge.complete_ci_repair before Bridge-owned commit and push.",
945
+ eventType: "completion_tools_configured",
946
+ });
947
+ }
948
+ const repairPrompt = repairChecks
949
+ ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
950
+ : addressComments
951
+ ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
952
+ : buildConflictResolvePrompt(dispatch, worktree.branchName);
953
+ const providerPrompt = buildProviderCliPrompt(dispatch, repairPrompt, repairEnvironment);
954
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
584
955
  const command = buildProviderCliCommand(dispatch, {
585
956
  worktreePath: worktree.worktreePath,
586
- prompt: repairChecks
587
- ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
588
- : addressComments
589
- ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
590
- : buildConflictResolvePrompt(dispatch, worktree.branchName)
957
+ prompt: repairPrompt,
958
+ promptFromStdin: pipePromptToStdin,
959
+ completionTools: completionTools ?? undefined,
960
+ runtimeEnvironment: repairEnvironment
591
961
  });
592
962
  await context.emit({
593
963
  role: "runner",
594
964
  kind: "checkpoint",
595
965
  content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
596
- eventType: "provider_cli_started"
966
+ eventType: "provider_cli_starting"
597
967
  });
598
968
  const outputState = createProviderOutputState(dispatch.execution.provider);
599
- const result = await this.processRunner.run({
600
- command: command.command,
601
- args: command.args,
602
- cwd: worktree.worktreePath,
603
- env: providerProcessEnv(dispatch)
604
- }, {
605
- onStdoutLine: async (line) => {
606
- for (const event of outputState.consume(line)) {
607
- await context.emit(event);
969
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
970
+ let result;
971
+ try {
972
+ result = await (context.processRunner ?? this.processRunner).run({
973
+ command: command.command,
974
+ args: command.args,
975
+ cwd: worktree.worktreePath,
976
+ env: providerProcessEnv(dispatch, repairEnvironment),
977
+ stdin: pipePromptToStdin ? providerPrompt : undefined
978
+ }, {
979
+ onStarted: async () => {
980
+ await context.emit({
981
+ role: "runner",
982
+ kind: "checkpoint",
983
+ content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} is supervised and ready.`,
984
+ eventType: "provider_cli_started"
985
+ });
986
+ },
987
+ onStdoutLine: async (line) => {
988
+ await toolHeartbeat.consume(line);
608
989
  }
990
+ }, context.signal);
991
+ }
992
+ finally {
993
+ toolHeartbeat.stop();
994
+ if (completionTools) {
995
+ await cleanupCompletionTools(completionTools, context.emit);
609
996
  }
610
- }, context.signal);
997
+ }
611
998
  if (result.exitCode !== 0) {
612
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
613
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
999
+ throw providerCliExitFailure(command.command, result);
1000
+ }
1001
+ const postProviderHeadSha = (await this.git.run(worktree.worktreePath, ["rev-parse", "HEAD"])).trim();
1002
+ if (postProviderHeadSha.toLowerCase() !== preparedHeadSha.toLowerCase()) {
1003
+ throw new Error(`provider_publication_boundary_violation: PR repair changed HEAD from ${preparedHeadSha} to ${postProviderHeadSha}; Forge requires provider changes to remain uncommitted and unpushed until validation passes.`);
614
1004
  }
1005
+ let repairArtifact = null;
1006
+ if (repairChecks) {
1007
+ repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
1008
+ if (!repairArtifact) {
1009
+ throw new Error("Provider CLI completed but did not call forge.complete_ci_repair. Forge did not commit or push the repair.");
1010
+ }
1011
+ if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
1012
+ throw new Error(`CI repair completion gate recorded ${repairArtifact.status}; Forge did not commit or push the repair.`);
1013
+ }
1014
+ await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
1015
+ await emitPhaseCompletionEvent(context.emit, repairArtifact);
1016
+ await emitPhaseGateEvents(context.emit, repairArtifact);
1017
+ }
1018
+ const validationProfile = repairChecks
1019
+ ? "ci_repair"
1020
+ : addressComments
1021
+ ? "comment_repair"
1022
+ : "conflict_repair";
1023
+ await this.prePublicationValidator.validate(worktree.worktreePath, validationProfile, publicationCapabilities(context, repairEnvironment), context.emit, context.signal, prePublicationExecution(context, providerProcessEnv(dispatch, repairEnvironment), this.processRunner));
615
1024
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
616
- await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
1025
+ if (providerOwnsBranchPublication(context)) {
1026
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit, expectedRepairHeadSha);
1027
+ }
617
1028
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
618
1029
  await context.emit({
619
1030
  role: "assistant",
@@ -630,10 +1041,24 @@ export class ProcessBackedProviderCliService {
630
1041
  ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
631
1042
  : addressComments
632
1043
  ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
633
- : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
1044
+ : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`,
1045
+ spec: repairSpec,
1046
+ artifact: repairArtifact,
1047
+ artifacts: repairArtifact ? [repairArtifact] : [],
634
1048
  };
635
1049
  }
636
1050
  }
1051
+ function publicationCapabilities(context, runtimeEnvironment) {
1052
+ const capabilities = new Set();
1053
+ const launchTarget = context.dispatch.qaBrowser?.launchTarget;
1054
+ if (runtimeEnvironment?.FORGE_PLAYWRIGHT_CONFIG
1055
+ || launchTarget?.url
1056
+ || launchTarget?.command
1057
+ || launchTarget?.managed_runtime_port) {
1058
+ capabilities.add("browser_launch");
1059
+ }
1060
+ return capabilities;
1061
+ }
637
1062
  async function stageDraftAttachments(dispatch, metadataRoot) {
638
1063
  const attachments = dispatch.draft?.attachments ?? [];
639
1064
  const stageable = attachments.filter((attachment) => (attachment.kind === "text" && typeof attachment.textContent === "string")
@@ -696,8 +1121,12 @@ function clipFileName(fileName, maxLength) {
696
1121
  return `${stem.slice(0, maxStemLength)}${extension}`;
697
1122
  }
698
1123
  const providerPromptStdinThreshold = 8_000;
699
- function shouldPipePromptToStdin(provider, prompt) {
700
- return provider.trim().toLowerCase() === "codex" && Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold;
1124
+ function shouldPipePromptToStdin(provider, prompt, force = false) {
1125
+ return provider.trim().toLowerCase() === "codex"
1126
+ && (force || Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold);
1127
+ }
1128
+ function usesManagedWindowsHost(dispatch) {
1129
+ return process.platform === "win32" && dispatch.sandbox?.executionMode === "host";
701
1130
  }
702
1131
  function isFakeWorkerModel(model) {
703
1132
  return model.trim().toLowerCase() === fakeWorkerModel;