@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 0.1.3-dev.ba293ccf876b

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