@loucompanion/forge-bridge 0.1.3-dev.f2b8822ce953 → 0.1.3-dev.fbe15023cdfe

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 +78 -0
  2. package/dist/bridge/bridge.base.d.ts +6 -1
  3. package/dist/bridge/bridge.service.d.ts +60 -3
  4. package/dist/bridge/bridge.service.js +970 -24
  5. package/dist/bridge/bridge.types.d.ts +172 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +47 -1
  14. package/dist/cli/cli.service.d.ts +42 -2
  15. package/dist/cli/cli.service.js +351 -22
  16. package/dist/cli/local-config.d.ts +6 -0
  17. package/dist/cli/local-config.js +47 -3
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  23. package/dist/runtime/attempt-process-supervisor.js +350 -0
  24. package/dist/runtime/generated-output-reset.d.ts +10 -0
  25. package/dist/runtime/generated-output-reset.js +103 -0
  26. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  27. package/dist/runtime/host-listener-inspector.js +324 -0
  28. package/dist/runtime/host-port-broker.d.ts +76 -0
  29. package/dist/runtime/host-port-broker.js +333 -0
  30. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  31. package/dist/runtime/host-runtime-contract.js +700 -0
  32. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  33. package/dist/runtime/host-runtime-lease.js +1460 -0
  34. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  35. package/dist/runtime/host-runtime-promotion.js +159 -0
  36. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  37. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  38. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  40. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  41. package/dist/runtime/runtime-isolation-lease.js +158 -0
  42. package/dist/sandbox/command-runner.d.ts +4 -0
  43. package/dist/sandbox/command-runner.js +47 -0
  44. package/dist/sandbox/contract.d.ts +5 -0
  45. package/dist/sandbox/contract.js +276 -0
  46. package/dist/sandbox/event-budget.d.ts +11 -0
  47. package/dist/sandbox/event-budget.js +27 -0
  48. package/dist/sandbox/image-policy.d.ts +35 -0
  49. package/dist/sandbox/image-policy.js +247 -0
  50. package/dist/sandbox/index.d.ts +5 -0
  51. package/dist/sandbox/index.js +5 -0
  52. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  53. package/dist/sandbox/lease-watchdog.js +67 -0
  54. package/dist/sandbox/preflight.d.ts +54 -0
  55. package/dist/sandbox/preflight.js +335 -0
  56. package/dist/sandbox/process.d.ts +20 -0
  57. package/dist/sandbox/process.js +209 -0
  58. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  59. package/dist/sandbox/provider-auth/codex.js +212 -0
  60. package/dist/sandbox/repository-lock.d.ts +5 -0
  61. package/dist/sandbox/repository-lock.js +119 -0
  62. package/dist/sandbox/runtime.d.ts +297 -0
  63. package/dist/sandbox/runtime.js +2338 -0
  64. package/dist/sandbox/sandbox.types.d.ts +86 -0
  65. package/dist/sandbox/sandbox.types.js +1 -0
  66. package/dist/sandbox/smoke.d.ts +76 -0
  67. package/dist/sandbox/smoke.js +380 -0
  68. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  69. package/dist/sandbox/worker-supervisor.js +36 -0
  70. package/dist/sandbox/worker.d.ts +1 -0
  71. package/dist/sandbox/worker.js +225 -0
  72. package/dist/sandbox/workspace.service.d.ts +13 -0
  73. package/dist/sandbox/workspace.service.js +72 -0
  74. package/dist/session/managed-attempt-tools.d.ts +12 -0
  75. package/dist/session/managed-attempt-tools.js +561 -0
  76. package/dist/session/phase-completion.artifacts.js +32 -4
  77. package/dist/session/phase-completion.d.ts +4 -2
  78. package/dist/session/phase-completion.helpers.js +27 -3
  79. package/dist/session/phase-completion.js +12 -3
  80. package/dist/session/phase-completion.prompts.d.ts +3 -3
  81. package/dist/session/phase-completion.prompts.js +20 -6
  82. package/dist/session/phase-completion.runtime.d.ts +12 -1
  83. package/dist/session/phase-completion.runtime.js +129 -7
  84. package/dist/session/phase-completion.schemas.js +132 -5
  85. package/dist/session/phase-completion.validation.d.ts +4 -1
  86. package/dist/session/phase-completion.validation.js +106 -2
  87. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  88. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  89. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  90. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  91. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  92. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  93. package/dist/session/provider-cli/provider-cli.deterministic.js +66 -9
  94. package/dist/session/provider-cli/provider-cli.process.d.ts +17 -0
  95. package/dist/session/provider-cli/provider-cli.process.js +223 -14
  96. package/dist/session/provider-cli/provider-cli.prompts.d.ts +17 -2
  97. package/dist/session/provider-cli/provider-cli.prompts.js +226 -18
  98. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  99. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  100. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  101. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -3
  102. package/dist/session/provider-cli/provider-cli.service.js +503 -98
  103. package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
  104. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  105. package/dist/session/provider-cli/provider-output-state.js +30 -2
  106. package/dist/session/session-redaction.d.ts +3 -0
  107. package/dist/session/session-redaction.js +23 -0
  108. package/dist/session/session.base.d.ts +6 -1
  109. package/dist/session/session.service.d.ts +8 -2
  110. package/dist/session/session.service.js +463 -58
  111. package/dist/session/session.types.d.ts +144 -1
  112. package/dist/session/session.types.js +279 -3
  113. package/dist/shared/path/path.js +3 -1
  114. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  115. package/dist/shared/process/host-process-recovery.js +78 -0
  116. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  117. package/dist/shared/process/host-process-supervisor.js +282 -0
  118. package/dist/shared/process/process.d.ts +1 -0
  119. package/dist/shared/process/process.js +1 -0
  120. package/dist/shared/process/windows-launch.d.ts +1 -1
  121. package/dist/shared/process/windows-launch.js +6 -3
  122. package/dist/shared/provider-capabilities.d.ts +1 -0
  123. package/dist/shared/provider-capabilities.js +3 -0
  124. package/dist/shared/redaction/redaction.js +10 -3
  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 +21 -1
  130. package/dist/worktree/dependency-warmer.js +138 -5
  131. package/dist/worktree/index.d.ts +1 -0
  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 +7 -2
  139. package/dist/worktree/worktree.service.js +72 -44
  140. package/dist/worktree/worktree.types.d.ts +3 -1
  141. package/package.json +6 -3
  142. package/runtime/windows-job-supervisor.ps1 +356 -0
  143. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,19 +1,208 @@
1
1
  import fs from "node:fs/promises";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import { GitClient } from "../../shared/git/git.js";
5
4
  import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
6
5
  import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
7
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
7
  import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
9
- import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommand, isDecomposeDraftDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
8
+ import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
10
9
  import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
11
10
  import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
12
11
  import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
12
+ import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
13
13
  export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
14
- export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand } from "./provider-cli.prompts.js";
15
- export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, StreamingProviderProcessRunner } from "./provider-cli.process.js";
14
+ export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
15
+ export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli.process.js";
16
16
  export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
17
+ async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
18
+ try {
19
+ await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
20
+ return;
21
+ }
22
+ catch (err) {
23
+ if (!isNonFastForwardPushError(err)) {
24
+ throw err;
25
+ }
26
+ await emit?.({
27
+ role: "runner",
28
+ kind: "checkpoint",
29
+ content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
30
+ eventType: "worktree_branch_push_rebase"
31
+ });
32
+ try {
33
+ await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
34
+ await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
35
+ await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
36
+ }
37
+ catch (retryErr) {
38
+ throw new Error(`git push origin HEAD:${branchName} was rejected because the remote branch moved, and automatic fetch/rebase failed: ${errorMessage(retryErr)}`, { cause: retryErr });
39
+ }
40
+ }
41
+ }
42
+ function providerOwnsBranchPublication(context) {
43
+ return context.dispatch.sandbox?.executionMode !== "docker";
44
+ }
45
+ function remoteTrackingRefspec(branchName) {
46
+ return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
47
+ }
48
+ function isNonFastForwardPushError(err) {
49
+ const message = errorMessage(err).toLowerCase();
50
+ return message.includes("non-fast-forward")
51
+ || message.includes("fetch first")
52
+ || message.includes("tip of your current branch is behind")
53
+ || message.includes("updates were rejected because the tip of your current branch is behind")
54
+ || message.includes("stale info");
55
+ }
56
+ function errorMessage(err) {
57
+ return err instanceof Error ? err.message : String(err);
58
+ }
59
+ const defaultProviderToolHeartbeatIntervalMs = 60_000;
60
+ const defaultManagedVerificationProgressIntervalMs = 30_000;
61
+ export class ManagedVerificationProgressEmitter {
62
+ logDirectory;
63
+ emit;
64
+ intervalMs;
65
+ timer = null;
66
+ lastSignature = null;
67
+ constructor(logDirectory, emit, intervalMs = defaultManagedVerificationProgressIntervalMs) {
68
+ this.logDirectory = logDirectory;
69
+ this.emit = emit;
70
+ this.intervalMs = intervalMs;
71
+ }
72
+ start() {
73
+ if (!this.logDirectory?.trim() || this.timer)
74
+ return;
75
+ this.timer = setInterval(() => {
76
+ void this.scan().catch(() => undefined);
77
+ }, Math.max(1, this.intervalMs));
78
+ void this.scan().catch(() => undefined);
79
+ }
80
+ async stop() {
81
+ if (this.timer) {
82
+ clearInterval(this.timer);
83
+ this.timer = null;
84
+ }
85
+ await this.scan().catch(() => undefined);
86
+ }
87
+ async scan() {
88
+ if (!this.logDirectory?.trim())
89
+ return;
90
+ const directory = path.join(this.logDirectory, "managed-commands");
91
+ const names = await fs.readdir(directory).catch(() => []);
92
+ const snapshots = await Promise.all(names
93
+ .filter(name => name.endsWith(".receipt.json"))
94
+ .map(async (name) => parseManagedVerificationSnapshot(path.join(directory, name))));
95
+ const current = snapshots
96
+ .filter((entry) => entry !== null)
97
+ .sort((left, right) => right.elapsedMs - left.elapsedMs)[0];
98
+ if (!current)
99
+ return;
100
+ const elapsedMs = current.startedAt
101
+ ? Math.max(current.elapsedMs ?? 0, Date.now() - Date.parse(current.startedAt))
102
+ : current.elapsedMs;
103
+ const progressBucket = elapsedMs === null ? "none" : Math.floor(elapsedMs / this.intervalMs);
104
+ const signature = `${current.commandId}:${current.status}:${current.outputRevision ?? "none"}:${progressBucket}`;
105
+ if (signature === this.lastSignature)
106
+ return;
107
+ const firstObservation = this.lastSignature === null;
108
+ this.lastSignature = signature;
109
+ const terminal = ["pass", "fail", "timed_out", "timeout_termination_failed", "runner_error"].includes(current.status);
110
+ const elapsed = elapsedMs === null ? "an unknown duration" : formatElapsed(elapsedMs);
111
+ const outputTail = current.stderrTail || current.stdoutTail;
112
+ await this.emit({
113
+ role: "runner",
114
+ kind: terminal && current.status !== "pass" ? "error" : "status",
115
+ content: terminal
116
+ ? `Managed verification ${current.status} after ${elapsed}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`
117
+ : firstObservation
118
+ ? `QA is waiting on managed verification ${current.commandId}; Forge will keep supervising it while the provider waits.`
119
+ : `Managed verification is still running after ${elapsed}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`,
120
+ eventType: terminal ? "verification_completed" : firstObservation ? "verification_waiting" : "verification_progress",
121
+ data: JSON.stringify({
122
+ command_id: current.commandId,
123
+ status: current.status,
124
+ elapsed_ms: elapsedMs,
125
+ output_revision: current.outputRevision,
126
+ last_output_at: current.lastOutputAt,
127
+ }),
128
+ });
129
+ }
130
+ }
131
+ async function parseManagedVerificationSnapshot(receiptPath) {
132
+ try {
133
+ const receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
134
+ const commandId = typeof receipt.command_id === "string" ? receipt.command_id : null;
135
+ const status = typeof receipt.status === "string" ? receipt.status : null;
136
+ if (!commandId || !status)
137
+ return null;
138
+ return {
139
+ commandId,
140
+ status,
141
+ elapsedMs: typeof receipt.elapsed_ms === "number" ? receipt.elapsed_ms : null,
142
+ startedAt: typeof receipt.started_at === "string" ? receipt.started_at : null,
143
+ outputRevision: typeof receipt.output_revision === "number" ? receipt.output_revision : null,
144
+ lastOutputAt: typeof receipt.last_output_at === "string" ? receipt.last_output_at : null,
145
+ stdoutTail: tail(typeof receipt.stdout === "string" ? receipt.stdout : ""),
146
+ stderrTail: tail(typeof receipt.stderr === "string" ? receipt.stderr : ""),
147
+ };
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ function tail(value) {
154
+ return value.trim().split(/\r?\n/u).slice(-4).join("\n");
155
+ }
156
+ function formatElapsed(milliseconds) {
157
+ const seconds = Math.max(0, Math.round(milliseconds / 1000));
158
+ const minutes = Math.floor(seconds / 60);
159
+ return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
160
+ }
161
+ class ProviderToolActivityHeartbeat {
162
+ outputState;
163
+ emit;
164
+ intervalMs;
165
+ timer = null;
166
+ constructor(outputState, emit, intervalMs) {
167
+ this.outputState = outputState;
168
+ this.emit = emit;
169
+ this.intervalMs = intervalMs;
170
+ }
171
+ async consume(line) {
172
+ const events = this.outputState.consume(line);
173
+ this.sync();
174
+ for (const event of events) {
175
+ await this.emit(event);
176
+ }
177
+ }
178
+ stop() {
179
+ if (this.timer) {
180
+ clearInterval(this.timer);
181
+ this.timer = null;
182
+ }
183
+ }
184
+ sync() {
185
+ if (!this.outputState.hasActiveToolCall) {
186
+ this.stop();
187
+ return;
188
+ }
189
+ if (this.timer) {
190
+ return;
191
+ }
192
+ this.timer = setInterval(() => {
193
+ if (!this.outputState.hasActiveToolCall) {
194
+ this.stop();
195
+ return;
196
+ }
197
+ void this.emit({
198
+ role: "runner",
199
+ kind: "status",
200
+ content: "A supervised provider tool is still running.",
201
+ eventType: "provider_tool_heartbeat",
202
+ }).catch(() => undefined);
203
+ }, Math.max(1, this.intervalMs));
204
+ }
205
+ }
17
206
  const fakeWorkerModel = "server-local-fake-worker";
18
207
  export class RoutingProviderCliService {
19
208
  realProvider;
@@ -32,6 +221,11 @@ export class RoutingProviderCliService {
32
221
  ? await this.deterministicProvider.runArchitect(context)
33
222
  : await this.realProvider.runArchitect(context);
34
223
  }
224
+ async runCiRepair(context) {
225
+ return isFakeWorkerModel(context.dispatch.execution.model)
226
+ ? await this.deterministicProvider.runCiRepair(context)
227
+ : await this.realProvider.runCiRepair(context);
228
+ }
35
229
  async runConflictResolve(context) {
36
230
  return isFakeWorkerModel(context.dispatch.execution.model)
37
231
  ? await this.deterministicProvider.runConflictResolve(context)
@@ -51,34 +245,36 @@ export class RoutingProviderCliService {
51
245
  export class ProcessBackedProviderCliService {
52
246
  processRunner;
53
247
  git;
54
- constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient()) {
248
+ providerToolHeartbeatIntervalMs;
249
+ constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs) {
55
250
  this.processRunner = processRunner;
56
251
  this.git = git;
252
+ this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
57
253
  }
58
254
  async runDraft(context) {
59
255
  const { dispatch } = context;
60
256
  if (!dispatch.draft) {
61
257
  throw new Error("Draft dispatch context is missing.");
62
258
  }
63
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "forge-bridge-draft-"));
64
- const draftWorktree = {
65
- repoPath: tempDir,
66
- worktreePath: tempDir,
67
- branchName: "draft",
68
- baseBranch: dispatch.cycleRun.baseBranch || dispatch.repo.defaultBranch || "draft",
69
- cleanupWorktrees: true
70
- };
71
- const metadataRoot = path.join(tempDir, ".forge", "work-items", dispatch.workItem.id);
259
+ const { worktree } = context;
260
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
72
261
  await fs.mkdir(metadataRoot, { recursive: true });
73
- await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(dispatch.draft, null, 2) + "\n", "utf8");
262
+ await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(durableDraftInput(dispatch), null, 2) + "\n", "utf8");
74
263
  await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
264
+ const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
75
265
  const decomposeDraft = isDecomposeDraftDispatch(dispatch);
76
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree: draftWorktree, phase: decomposeDraft ? "decompose" : "draft" });
266
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
267
+ const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
268
+ const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
269
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
77
270
  const command = buildProviderCliCommand(dispatch, {
78
- worktreePath: tempDir,
79
- prompt: buildDraftPrompt(dispatch),
271
+ worktreePath: worktree.worktreePath,
272
+ prompt: draftPrompt,
273
+ promptFromStdin: pipePromptToStdin,
80
274
  completionTools: completionTools ?? undefined,
81
- skipGitRepoCheck: true
275
+ skipGitRepoCheck: false,
276
+ imagePaths: stagedAttachments.imagePaths,
277
+ runtimeEnvironment: context.runtimeEnvironment
82
278
  });
83
279
  const draftArtifactSlugs = dispatch.artifacts
84
280
  .filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
@@ -92,8 +288,8 @@ export class ProcessBackedProviderCliService {
92
288
  await context.emit({
93
289
  role: "runner",
94
290
  kind: "checkpoint",
95
- content: `Starting provider CLI Draft: ${formatProviderCliCommand(command)}`,
96
- eventType: "provider_cli_started"
291
+ content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
292
+ eventType: "provider_cli_starting"
97
293
  });
98
294
  await context.emit({
99
295
  role: "runner",
@@ -104,6 +300,7 @@ export class ProcessBackedProviderCliService {
104
300
  eventType: "completion_tools_configured"
105
301
  });
106
302
  const outputState = createProviderOutputState(dispatch.execution.provider);
303
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
107
304
  try {
108
305
  const providerCancellation = new AbortController();
109
306
  const abortProvider = () => providerCancellation.abort();
@@ -114,16 +311,23 @@ export class ProcessBackedProviderCliService {
114
311
  context.signal?.addEventListener("abort", abortProvider, { once: true });
115
312
  }
116
313
  try {
117
- const runPromise = this.processRunner.run({
314
+ const runPromise = (context.processRunner ?? this.processRunner).run({
118
315
  command: command.command,
119
316
  args: command.args,
120
- cwd: tempDir,
121
- env: providerProcessEnv(dispatch)
317
+ cwd: worktree.worktreePath,
318
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
319
+ stdin: pipePromptToStdin ? providerPrompt : undefined
122
320
  }, {
321
+ onStarted: async () => {
322
+ await context.emit({
323
+ role: "runner",
324
+ kind: "checkpoint",
325
+ content: "Provider CLI Draft is supervised and ready.",
326
+ eventType: "provider_cli_started"
327
+ });
328
+ },
123
329
  onStdoutLine: async (line) => {
124
- for (const event of outputState.consume(line)) {
125
- await context.emit(event);
126
- }
330
+ await toolHeartbeat.consume(line);
127
331
  }
128
332
  }, providerCancellation.signal);
129
333
  const artifactWaitCancellation = new AbortController();
@@ -134,7 +338,7 @@ export class ProcessBackedProviderCliService {
134
338
  else {
135
339
  context.signal?.addEventListener("abort", abortArtifactWait, { once: true });
136
340
  }
137
- const artifactWaitPromise = waitForCompletionArtifact(tempDir, dispatch.workItem.id, decomposeDraft ? "decompose" : "draft", artifactWaitCancellation.signal);
341
+ const artifactWaitPromise = waitForCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, decomposeDraft ? "decompose" : "draft", artifactWaitCancellation.signal);
138
342
  let result;
139
343
  let earlyProposalArtifact = null;
140
344
  try {
@@ -168,8 +372,8 @@ export class ProcessBackedProviderCliService {
168
372
  });
169
373
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
170
374
  if (decomposeDraft) {
171
- await emitDecomposeGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
172
- const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "decompose");
375
+ await emitDecomposeGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
376
+ const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "decompose");
173
377
  if (!proposalArtifact) {
174
378
  throw new Error("Provider CLI Decompose completed but did not call forge.complete_decompose_proposal.");
175
379
  }
@@ -191,8 +395,8 @@ export class ProcessBackedProviderCliService {
191
395
  artifacts: [proposalArtifact]
192
396
  };
193
397
  }
194
- await emitDraftGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
195
- const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "draft");
398
+ await emitDraftGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
399
+ const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "draft");
196
400
  if (!proposalArtifact) {
197
401
  throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
198
402
  }
@@ -219,8 +423,8 @@ export class ProcessBackedProviderCliService {
219
423
  }
220
424
  }
221
425
  finally {
426
+ toolHeartbeat.stop();
222
427
  await cleanupCompletionTools(completionTools, context.emit);
223
- await fs.rm(tempDir, { recursive: true, force: true });
224
428
  }
225
429
  }
226
430
  async runImplementation(context) {
@@ -238,21 +442,26 @@ export class ProcessBackedProviderCliService {
238
442
  const specPath = path.join(metadataRoot, "spec.md");
239
443
  await fs.writeFile(specPath, specBody, "utf8");
240
444
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
241
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
445
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase, runtimeEnvironment: context.runtimeEnvironment });
446
+ const implementationPrompt = debugCycle
447
+ ? buildDebugCyclePrompt(dispatch, phase)
448
+ : developCycle
449
+ ? buildDevelopCyclePrompt(dispatch, phase)
450
+ : buildImplementationPrompt(dispatch, phase);
451
+ const providerPrompt = buildProviderCliPrompt(dispatch, implementationPrompt, context.runtimeEnvironment);
452
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
242
453
  const command = buildProviderCliCommand(dispatch, {
243
454
  worktreePath: worktree.worktreePath,
244
- prompt: debugCycle
245
- ? buildDebugCyclePrompt(dispatch, phase)
246
- : developCycle
247
- ? buildDevelopCyclePrompt(dispatch, phase)
248
- : buildImplementationPrompt(dispatch, phase),
249
- completionTools
455
+ prompt: implementationPrompt,
456
+ promptFromStdin: pipePromptToStdin,
457
+ completionTools,
458
+ runtimeEnvironment: context.runtimeEnvironment
250
459
  });
251
460
  await context.emit({
252
461
  role: "runner",
253
462
  kind: "checkpoint",
254
- content: `Starting provider CLI: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
255
- eventType: "provider_cli_started"
463
+ content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
464
+ eventType: "provider_cli_starting"
256
465
  });
257
466
  await context.emit({
258
467
  role: "runner",
@@ -265,24 +474,33 @@ export class ProcessBackedProviderCliService {
265
474
  eventType: "completion_tools_configured"
266
475
  });
267
476
  const outputState = createProviderOutputState(dispatch.execution.provider);
477
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
268
478
  const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
269
479
  let result;
270
480
  try {
271
481
  liveCompletionEvents.start();
272
- result = await this.processRunner.run({
482
+ result = await (context.processRunner ?? this.processRunner).run({
273
483
  command: command.command,
274
484
  args: command.args,
275
485
  cwd: worktree.worktreePath,
276
- env: providerProcessEnv(dispatch)
486
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
487
+ stdin: pipePromptToStdin ? providerPrompt : undefined
277
488
  }, {
489
+ onStarted: async () => {
490
+ await context.emit({
491
+ role: "runner",
492
+ kind: "checkpoint",
493
+ content: "Provider CLI is supervised and ready.",
494
+ eventType: "provider_cli_started"
495
+ });
496
+ },
278
497
  onStdoutLine: async (line) => {
279
- for (const event of outputState.consume(line)) {
280
- await context.emit(event);
281
- }
498
+ await toolHeartbeat.consume(line);
282
499
  }
283
500
  }, context.signal);
284
501
  }
285
502
  finally {
503
+ toolHeartbeat.stop();
286
504
  await liveCompletionEvents.stop();
287
505
  await cleanupCompletionTools(completionTools, context.emit);
288
506
  }
@@ -298,8 +516,8 @@ export class ProcessBackedProviderCliService {
298
516
  });
299
517
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
300
518
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
301
- if (developCycle || debugCycle || phase === "implementation") {
302
- await this.git.run(worktree.worktreePath, ["push", "origin", `HEAD:${worktree.branchName}`]);
519
+ if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
520
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
303
521
  await context.emit({
304
522
  role: "runner",
305
523
  kind: "checkpoint",
@@ -357,18 +575,23 @@ export class ProcessBackedProviderCliService {
357
575
  const specPath = path.join(metadataRoot, "spec.md");
358
576
  await fs.writeFile(specPath, specBody, "utf8");
359
577
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
360
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
578
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect", runtimeEnvironment: context.runtimeEnvironment });
579
+ const architectPrompt = buildArchitectPrompt(dispatch);
580
+ const providerPrompt = buildProviderCliPrompt(dispatch, architectPrompt, context.runtimeEnvironment);
581
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
361
582
  const command = buildProviderCliCommand(dispatch, {
362
583
  worktreePath: worktree.worktreePath,
363
- prompt: buildArchitectPrompt(dispatch),
364
- completionTools
584
+ prompt: architectPrompt,
585
+ promptFromStdin: pipePromptToStdin,
586
+ completionTools,
587
+ runtimeEnvironment: context.runtimeEnvironment
365
588
  });
366
- const commandText = formatProviderCliCommand(command);
589
+ const commandText = formatProviderCliCommandForCheckpoint(command);
367
590
  await context.emit({
368
591
  role: "runner",
369
592
  kind: "checkpoint",
370
593
  content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
371
- eventType: "provider_cli_started"
594
+ eventType: "provider_cli_starting"
372
595
  });
373
596
  await context.emit({
374
597
  role: "runner",
@@ -377,24 +600,31 @@ export class ProcessBackedProviderCliService {
377
600
  eventType: "completion_tools_configured"
378
601
  });
379
602
  const outputState = createProviderOutputState(dispatch.execution.provider);
603
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
380
604
  const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
381
605
  let result;
382
606
  try {
383
607
  liveArchitectEvents.start();
384
- result = await this.processRunner.run({
608
+ result = await (context.processRunner ?? this.processRunner).run({
385
609
  command: command.command,
386
610
  args: command.args,
387
611
  cwd: worktree.worktreePath,
388
- env: providerProcessEnv(dispatch)
612
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
613
+ stdin: pipePromptToStdin ? providerPrompt : undefined
389
614
  }, {
390
- onStdoutLine: async (line) => {
391
- for (const event of outputState.consume(line)) {
392
- await context.emit(event);
393
- }
394
- }
615
+ onStarted: async () => {
616
+ await context.emit({
617
+ role: "runner",
618
+ kind: "checkpoint",
619
+ content: "Provider CLI Architect is supervised and ready.",
620
+ eventType: "provider_cli_started"
621
+ });
622
+ },
623
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
395
624
  }, context.signal);
396
625
  }
397
626
  finally {
627
+ toolHeartbeat.stop();
398
628
  await liveArchitectEvents.stop();
399
629
  await cleanupCompletionTools(completionTools, context.emit);
400
630
  }
@@ -448,18 +678,23 @@ export class ProcessBackedProviderCliService {
448
678
  const specPath = path.join(metadataRoot, "spec.md");
449
679
  await fs.writeFile(specPath, specBody, "utf8");
450
680
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
451
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
681
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa", runtimeEnvironment: context.runtimeEnvironment });
682
+ const qaPrompt = buildQaPrompt(dispatch, specBody);
683
+ const providerPrompt = buildProviderCliPrompt(dispatch, qaPrompt, context.runtimeEnvironment);
684
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
452
685
  const command = buildProviderCliCommand(dispatch, {
453
686
  worktreePath: worktree.worktreePath,
454
- prompt: buildQaPrompt(dispatch, specBody),
455
- completionTools
687
+ prompt: qaPrompt,
688
+ promptFromStdin: pipePromptToStdin,
689
+ completionTools,
690
+ runtimeEnvironment: context.runtimeEnvironment
456
691
  });
457
- const commandText = formatProviderCliCommand(command);
692
+ const commandText = formatProviderCliCommandForCheckpoint(command);
458
693
  await context.emit({
459
694
  role: "runner",
460
695
  kind: "checkpoint",
461
696
  content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
462
- eventType: "provider_cli_started"
697
+ eventType: "provider_cli_starting"
463
698
  });
464
699
  await context.emit({
465
700
  role: "runner",
@@ -468,24 +703,34 @@ export class ProcessBackedProviderCliService {
468
703
  eventType: "completion_tools_configured"
469
704
  });
470
705
  const outputState = createProviderOutputState(dispatch.execution.provider);
706
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
707
+ const verificationProgress = new ManagedVerificationProgressEmitter(context.runtimeEnvironment?.FORGE_LOG_DIR, context.emit);
471
708
  const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
472
709
  let result;
473
710
  try {
474
711
  liveQaEvents.start();
475
- result = await this.processRunner.run({
712
+ verificationProgress.start();
713
+ result = await (context.processRunner ?? this.processRunner).run({
476
714
  command: command.command,
477
715
  args: command.args,
478
716
  cwd: worktree.worktreePath,
479
- env: providerProcessEnv(dispatch)
717
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
718
+ stdin: pipePromptToStdin ? providerPrompt : undefined
480
719
  }, {
481
- onStdoutLine: async (line) => {
482
- for (const event of outputState.consume(line)) {
483
- await context.emit(event);
484
- }
485
- }
720
+ onStarted: async () => {
721
+ await context.emit({
722
+ role: "runner",
723
+ kind: "checkpoint",
724
+ content: "Provider CLI QA is supervised and ready.",
725
+ eventType: "provider_cli_started"
726
+ });
727
+ },
728
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
486
729
  }, context.signal);
487
730
  }
488
731
  finally {
732
+ toolHeartbeat.stop();
733
+ await verificationProgress.stop();
489
734
  await liveQaEvents.stop();
490
735
  await cleanupCompletionTools(completionTools, context.emit);
491
736
  }
@@ -507,7 +752,7 @@ export class ProcessBackedProviderCliService {
507
752
  ? "Provider CLI QA completed but did not produce the complete durable QA artifact bundle."
508
753
  : "Provider CLI QA completed but did not call the Forge QA completion tool.");
509
754
  }
510
- enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
755
+ await enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
511
756
  if (!liveQaEvents.hasEmitted()) {
512
757
  await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
513
758
  await emitQaStructuredEvents(context.emit, qaArtifact);
@@ -531,56 +776,216 @@ export class ProcessBackedProviderCliService {
531
776
  artifacts: [qaArtifact]
532
777
  };
533
778
  }
779
+ async runCiRepair(context) {
780
+ return await this.runPullRequestRepair(context, "ci_repair");
781
+ }
534
782
  async runConflictResolve(context) {
783
+ return await this.runPullRequestRepair(context, "conflict_resolve");
784
+ }
785
+ async runPullRequestRepair(context, route) {
535
786
  const { dispatch, worktree } = context;
536
- const addressComments = isPullRequestCommentsDispatch(dispatch);
787
+ const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
788
+ const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
789
+ let repairSpec = null;
790
+ let repairEnvironment = context.runtimeEnvironment;
791
+ let completionTools = null;
792
+ if (repairChecks) {
793
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
794
+ await fs.mkdir(metadataRoot, { recursive: true });
795
+ const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
796
+ if (!laneCommand) {
797
+ throw new Error(`CI repair readiness failed: lane command for '${dispatch.execution.lane}' was not resolved, so the approved spec cannot be materialized.`);
798
+ }
799
+ const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
800
+ const specPath = path.join(metadataRoot, "spec.md");
801
+ await fs.writeFile(specPath, specBody, "utf8");
802
+ await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
803
+ repairSpec = {
804
+ repoPath: worktree.worktreePath,
805
+ path: specPath,
806
+ body: specBody,
807
+ producedBy: "forge-lane-command",
808
+ status: "approved",
809
+ };
810
+ const readiness = await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
811
+ repairEnvironment = readiness.environment;
812
+ completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "ci_repair", runtimeEnvironment: repairEnvironment });
813
+ await context.emit({
814
+ role: "runner",
815
+ kind: "checkpoint",
816
+ 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.`,
817
+ eventType: "ci_repair_readiness_ready",
818
+ });
819
+ await context.emit({
820
+ role: "runner",
821
+ kind: "checkpoint",
822
+ content: "Forge CI repair completion tool configured; provider must call forge.complete_ci_repair before Bridge-owned commit and push.",
823
+ eventType: "completion_tools_configured",
824
+ });
825
+ }
826
+ const repairPrompt = repairChecks
827
+ ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
828
+ : addressComments
829
+ ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
830
+ : buildConflictResolvePrompt(dispatch, worktree.branchName);
831
+ const providerPrompt = buildProviderCliPrompt(dispatch, repairPrompt, repairEnvironment);
832
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
537
833
  const command = buildProviderCliCommand(dispatch, {
538
834
  worktreePath: worktree.worktreePath,
539
- prompt: addressComments
540
- ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
541
- : buildConflictResolvePrompt(dispatch, worktree.branchName)
835
+ prompt: repairPrompt,
836
+ promptFromStdin: pipePromptToStdin,
837
+ completionTools: completionTools ?? undefined,
838
+ runtimeEnvironment: repairEnvironment
542
839
  });
543
840
  await context.emit({
544
841
  role: "runner",
545
842
  kind: "checkpoint",
546
- content: `Starting provider CLI ${addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
547
- eventType: "provider_cli_started"
843
+ content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
844
+ eventType: "provider_cli_starting"
548
845
  });
549
846
  const outputState = createProviderOutputState(dispatch.execution.provider);
550
- const result = await this.processRunner.run({
551
- command: command.command,
552
- args: command.args,
553
- cwd: worktree.worktreePath,
554
- env: providerProcessEnv(dispatch)
555
- }, {
556
- onStdoutLine: async (line) => {
557
- for (const event of outputState.consume(line)) {
558
- await context.emit(event);
847
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
848
+ let result;
849
+ try {
850
+ result = await (context.processRunner ?? this.processRunner).run({
851
+ command: command.command,
852
+ args: command.args,
853
+ cwd: worktree.worktreePath,
854
+ env: providerProcessEnv(dispatch, repairEnvironment),
855
+ stdin: pipePromptToStdin ? providerPrompt : undefined
856
+ }, {
857
+ onStarted: async () => {
858
+ await context.emit({
859
+ role: "runner",
860
+ kind: "checkpoint",
861
+ content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} is supervised and ready.`,
862
+ eventType: "provider_cli_started"
863
+ });
864
+ },
865
+ onStdoutLine: async (line) => {
866
+ await toolHeartbeat.consume(line);
559
867
  }
868
+ }, context.signal);
869
+ }
870
+ finally {
871
+ toolHeartbeat.stop();
872
+ if (completionTools) {
873
+ await cleanupCompletionTools(completionTools, context.emit);
560
874
  }
561
- }, context.signal);
875
+ }
562
876
  if (result.exitCode !== 0) {
563
877
  const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
564
878
  throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
565
879
  }
880
+ let repairArtifact = null;
881
+ if (repairChecks) {
882
+ repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
883
+ if (!repairArtifact) {
884
+ throw new Error("Provider CLI completed but did not call forge.complete_ci_repair. Forge did not commit or push the repair.");
885
+ }
886
+ if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
887
+ throw new Error(`CI repair completion gate recorded ${repairArtifact.status}; Forge did not commit or push the repair.`);
888
+ }
889
+ await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
890
+ await emitPhaseCompletionEvent(context.emit, repairArtifact);
891
+ await emitPhaseGateEvents(context.emit, repairArtifact);
892
+ }
566
893
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
567
- await this.git.run(worktree.worktreePath, ["push", "origin", `HEAD:${worktree.branchName}`]);
894
+ if (providerOwnsBranchPublication(context)) {
895
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
896
+ }
568
897
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
569
898
  await context.emit({
570
899
  role: "assistant",
571
900
  kind: "message",
572
- content: outputState.finalMessage || (addressComments
573
- ? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
574
- : `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
575
- eventType: addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
901
+ content: outputState.finalMessage || (repairChecks
902
+ ? `${dispatch.execution.provider} CLI repaired pull request checks for ${worktree.branchName}.`
903
+ : addressComments
904
+ ? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
905
+ : `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
906
+ eventType: repairChecks ? "pr_checks_complete" : addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
576
907
  });
577
908
  return {
578
- summary: addressComments
579
- ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
580
- : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
909
+ summary: repairChecks
910
+ ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
911
+ : addressComments
912
+ ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
913
+ : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`,
914
+ spec: repairSpec,
915
+ artifact: repairArtifact,
916
+ artifacts: repairArtifact ? [repairArtifact] : [],
581
917
  };
582
918
  }
583
919
  }
920
+ async function stageDraftAttachments(dispatch, metadataRoot) {
921
+ const attachments = dispatch.draft?.attachments ?? [];
922
+ const stageable = attachments.filter((attachment) => (attachment.kind === "text" && typeof attachment.textContent === "string")
923
+ || (attachment.kind === "image" && typeof attachment.contentBase64 === "string"));
924
+ if (stageable.length === 0) {
925
+ return { textReferences: [], imagePaths: [] };
926
+ }
927
+ const attachmentRoot = path.join(metadataRoot, "draft-attachments");
928
+ const worktreeRoot = path.resolve(metadataRoot, "../../..");
929
+ await fs.mkdir(attachmentRoot, { recursive: true });
930
+ const textReferences = [];
931
+ const imagePaths = [];
932
+ for (const attachment of stageable) {
933
+ const fileName = `${String(attachment.index + 1).padStart(2, "0")}-${safeAttachmentFileName(attachment.fileName)}`;
934
+ const absolutePath = path.join(attachmentRoot, fileName);
935
+ if (attachment.kind === "text") {
936
+ await fs.writeFile(absolutePath, attachment.textContent ?? "", { encoding: "utf8", mode: 0o600 });
937
+ textReferences.push({ index: attachment.index, path: path.relative(worktreeRoot, absolutePath).replaceAll(path.sep, "/") });
938
+ continue;
939
+ }
940
+ const imageContent = decodeDraftImageAttachment(attachment.contentBase64 ?? "", attachment.fileName, attachment.index);
941
+ await fs.writeFile(absolutePath, imageContent, { mode: 0o600 });
942
+ imagePaths.push(absolutePath);
943
+ }
944
+ return { textReferences, imagePaths };
945
+ }
946
+ function decodeDraftImageAttachment(contentBase64, fileName, index) {
947
+ const normalized = contentBase64.replace(/\s+/gu, "");
948
+ if (normalized.length === 0 || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(normalized)) {
949
+ throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
950
+ }
951
+ const decoded = Buffer.from(normalized, "base64");
952
+ if (decoded.length === 0 || decoded.toString("base64") !== normalized) {
953
+ throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
954
+ }
955
+ return decoded;
956
+ }
957
+ function safeAttachmentFileName(fileName) {
958
+ const sanitized = path.basename(fileName || "attachment.txt")
959
+ .replace(/[^a-zA-Z0-9._-]/gu, "_")
960
+ .replace(/[. ]+$/gu, "") || "attachment.txt";
961
+ const maxFileNameLength = 120;
962
+ const clipped = clipFileName(sanitized, maxFileNameLength);
963
+ const extension = path.extname(clipped);
964
+ const stem = path.basename(clipped, extension).replace(/[. ]+$/gu, "") || "attachment";
965
+ const windowsReservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
966
+ const safeStem = stem === "." || stem === ".." || windowsReservedName.test(stem) ? `attachment-${stem.replaceAll(".", "_")}` : stem;
967
+ return clipFileName(`${safeStem}${extension}`, maxFileNameLength);
968
+ }
969
+ function clipFileName(fileName, maxLength) {
970
+ if (fileName.length <= maxLength) {
971
+ return fileName;
972
+ }
973
+ const originalExtension = path.extname(fileName);
974
+ const extension = originalExtension.length > maxLength - 1
975
+ ? originalExtension.slice(0, maxLength - 1)
976
+ : originalExtension;
977
+ const stem = path.basename(fileName, originalExtension);
978
+ const maxStemLength = Math.max(1, maxLength - extension.length);
979
+ return `${stem.slice(0, maxStemLength)}${extension}`;
980
+ }
981
+ const providerPromptStdinThreshold = 8_000;
982
+ function shouldPipePromptToStdin(provider, prompt, force = false) {
983
+ return provider.trim().toLowerCase() === "codex"
984
+ && (force || Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold);
985
+ }
986
+ function usesManagedWindowsHost(dispatch) {
987
+ return process.platform === "win32" && dispatch.sandbox?.executionMode === "host";
988
+ }
584
989
  function isFakeWorkerModel(model) {
585
990
  return model.trim().toLowerCase() === fakeWorkerModel;
586
991
  }