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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/bridge/bridge.base.d.ts +2 -1
  2. package/dist/bridge/bridge.service.d.ts +8 -0
  3. package/dist/bridge/bridge.service.js +184 -12
  4. package/dist/bridge/bridge.types.d.ts +37 -0
  5. package/dist/cli/cli.help.js +1 -1
  6. package/dist/cli/cli.service.js +1 -1
  7. package/dist/cli/local-config.js +2 -1
  8. package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
  9. package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
  10. package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
  11. package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
  12. package/dist/runtime/attempt-process-supervisor.js +75 -15
  13. package/dist/runtime/generated-output-reset.d.ts +10 -0
  14. package/dist/runtime/generated-output-reset.js +103 -0
  15. package/dist/runtime/host-listener-inspector.d.ts +16 -0
  16. package/dist/runtime/host-listener-inspector.js +143 -4
  17. package/dist/runtime/host-runtime-contract.d.ts +9 -1
  18. package/dist/runtime/host-runtime-contract.js +411 -6
  19. package/dist/runtime/host-runtime-lease.d.ts +12 -1
  20. package/dist/runtime/host-runtime-lease.js +271 -50
  21. package/dist/runtime/host-runtime-promotion.d.ts +2 -0
  22. package/dist/runtime/host-runtime-promotion.js +35 -4
  23. package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
  24. package/dist/runtime/node-host-runtime-adapter.js +71 -8
  25. package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
  26. package/dist/sandbox/preflight.d.ts +9 -0
  27. package/dist/sandbox/preflight.js +39 -4
  28. package/dist/sandbox/workspace.service.js +7 -2
  29. package/dist/session/managed-attempt-tools.d.ts +17 -0
  30. package/dist/session/managed-attempt-tools.js +712 -0
  31. package/dist/session/phase-completion.artifacts.js +9 -2
  32. package/dist/session/phase-completion.d.ts +11 -2
  33. package/dist/session/phase-completion.helpers.d.ts +5 -0
  34. package/dist/session/phase-completion.helpers.js +45 -9
  35. package/dist/session/phase-completion.js +12 -3
  36. package/dist/session/phase-completion.prompts.d.ts +3 -3
  37. package/dist/session/phase-completion.prompts.js +21 -6
  38. package/dist/session/phase-completion.runtime.d.ts +13 -1
  39. package/dist/session/phase-completion.runtime.js +183 -8
  40. package/dist/session/phase-completion.schemas.js +108 -3
  41. package/dist/session/phase-completion.validation.d.ts +4 -1
  42. package/dist/session/phase-completion.validation.js +144 -18
  43. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  44. package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
  45. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  46. package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
  47. package/dist/session/provider-cli/provider-cli.process.js +176 -18
  48. package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
  49. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  50. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
  51. package/dist/session/provider-cli/provider-cli.service.js +390 -83
  52. package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
  53. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  54. package/dist/session/provider-cli/provider-output-state.js +30 -2
  55. package/dist/session/session.base.d.ts +2 -1
  56. package/dist/session/session.service.d.ts +2 -1
  57. package/dist/session/session.service.js +303 -30
  58. package/dist/session/session.types.d.ts +38 -0
  59. package/dist/session/session.types.js +17 -0
  60. package/dist/shared/process/host-process-supervisor.d.ts +18 -1
  61. package/dist/shared/process/host-process-supervisor.js +94 -7
  62. package/dist/shared/process/process.d.ts +1 -0
  63. package/dist/shared/process/process.js +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/dist/worktree/artifact-injector.d.ts +3 -2
  67. package/dist/worktree/artifact-injector.js +43 -4
  68. package/dist/worktree/dependency-warmer.d.ts +10 -1
  69. package/dist/worktree/dependency-warmer.js +94 -5
  70. package/dist/worktree/worktree.service.d.ts +1 -0
  71. package/dist/worktree/worktree.service.js +12 -1
  72. package/package.json +1 -1
  73. package/runtime/windows-job-supervisor.ps1 +89 -20
@@ -1,7 +1,9 @@
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
8
  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";
7
9
  import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
@@ -9,34 +11,37 @@ import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt
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";
12
15
  export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
13
16
  export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
14
- export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, StreamingProviderProcessRunner } from "./provider-cli.process.js";
17
+ export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli.process.js";
15
18
  export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
16
- async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
17
- try {
18
- await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
19
- return;
20
- }
21
- catch (err) {
22
- if (!isNonFastForwardPushError(err)) {
23
- throw err;
24
- }
25
- await emit?.({
26
- role: "runner",
27
- kind: "checkpoint",
28
- content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
29
- eventType: "worktree_branch_push_rebase"
30
- });
19
+ async function pushWorktreeBranch(git, worktreePath, branchName, repoPath, coordinator, signal, emit) {
20
+ await coordinator.withLock(repoPath, async () => {
31
21
  try {
32
- await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
33
- await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
34
22
  await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
23
+ return;
35
24
  }
36
- catch (retryErr) {
37
- throw new Error(`git push origin HEAD:${branchName} was rejected because the remote branch moved, and automatic fetch/rebase failed: ${errorMessage(retryErr)}`, { cause: retryErr });
25
+ catch (err) {
26
+ if (!isNonFastForwardPushError(err)) {
27
+ throw err;
28
+ }
29
+ await emit?.({
30
+ role: "runner",
31
+ kind: "checkpoint",
32
+ content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
33
+ eventType: "worktree_branch_push_rebase"
34
+ });
35
+ try {
36
+ await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
37
+ await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
38
+ await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
39
+ }
40
+ catch (retryErr) {
41
+ 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 });
42
+ }
38
43
  }
39
- }
44
+ }, signal);
40
45
  }
41
46
  function providerOwnsBranchPublication(context) {
42
47
  return context.dispatch.sandbox?.executionMode !== "docker";
@@ -55,6 +60,179 @@ function isNonFastForwardPushError(err) {
55
60
  function errorMessage(err) {
56
61
  return err instanceof Error ? err.message : String(err);
57
62
  }
63
+ const defaultProviderToolHeartbeatIntervalMs = 60_000;
64
+ const defaultManagedVerificationProgressIntervalMs = 30_000;
65
+ function compareManagedVerificationSnapshots(left, right) {
66
+ const leftOrder = managedVerificationStartedAt(left.startedAt) ?? Number.MAX_SAFE_INTEGER;
67
+ const rightOrder = managedVerificationStartedAt(right.startedAt) ?? Number.MAX_SAFE_INTEGER;
68
+ return leftOrder - rightOrder || left.commandId.localeCompare(right.commandId);
69
+ }
70
+ function managedVerificationStartedAt(value) {
71
+ const parsed = Date.parse(value ?? "");
72
+ return Number.isFinite(parsed) ? parsed : null;
73
+ }
74
+ export class ManagedVerificationProgressEmitter {
75
+ logDirectory;
76
+ emit;
77
+ intervalMs;
78
+ timer = null;
79
+ observedStatuses = new Map();
80
+ completedCommands = new Set();
81
+ constructor(logDirectory, emit, intervalMs = defaultManagedVerificationProgressIntervalMs) {
82
+ this.logDirectory = logDirectory;
83
+ this.emit = emit;
84
+ this.intervalMs = intervalMs;
85
+ }
86
+ start() {
87
+ if (!this.logDirectory?.trim() || this.timer)
88
+ return;
89
+ this.timer = setInterval(() => {
90
+ void this.scan().catch(() => undefined);
91
+ }, Math.max(1, this.intervalMs));
92
+ void this.scan().catch(() => undefined);
93
+ }
94
+ async stop() {
95
+ if (this.timer) {
96
+ clearInterval(this.timer);
97
+ this.timer = null;
98
+ }
99
+ await this.scan().catch(() => undefined);
100
+ }
101
+ async scan() {
102
+ if (!this.logDirectory?.trim())
103
+ return;
104
+ const directory = path.join(this.logDirectory, "managed-commands");
105
+ const names = await fs.readdir(directory).catch(() => []);
106
+ const snapshots = await Promise.all(names
107
+ .filter(name => name.endsWith(".receipt.json"))
108
+ .map(async (name) => parseManagedVerificationSnapshot(path.join(directory, name))));
109
+ const current = snapshots
110
+ .filter((entry) => entry !== null)
111
+ .sort(compareManagedVerificationSnapshots);
112
+ for (const snapshot of current) {
113
+ await this.emitTransition(snapshot);
114
+ }
115
+ }
116
+ async emitTransition(current) {
117
+ const terminal = ["pass", "fail", "timed_out", "timeout_termination_failed", "runner_error"].includes(current.status);
118
+ if (terminal && this.completedCommands.has(current.commandId))
119
+ return;
120
+ const previousStatus = this.observedStatuses.get(current.commandId);
121
+ if (previousStatus === current.status)
122
+ return;
123
+ this.observedStatuses.set(current.commandId, current.status);
124
+ if (terminal)
125
+ this.completedCommands.add(current.commandId);
126
+ // Starting and running are one externally meaningful state. The receipt is
127
+ // still refreshed on disk for UI telemetry, but repeated output/elapsed
128
+ // changes must not inflate the provider transcript or session history.
129
+ if (!terminal && previousStatus === "starting" && current.status === "running")
130
+ return;
131
+ const startedAt = managedVerificationStartedAt(current.startedAt);
132
+ const elapsedMs = startedAt !== null
133
+ ? Math.max(current.elapsedMs ?? 0, Date.now() - startedAt)
134
+ : current.elapsedMs;
135
+ const elapsed = elapsedMs === null ? "an unknown duration" : formatElapsed(elapsedMs);
136
+ const outputTail = current.lastOutputStream === "stderr"
137
+ ? current.stderrTail || current.stdoutTail
138
+ : current.stdoutTail || current.stderrTail;
139
+ const recovered = previousStatus === undefined && terminal;
140
+ await this.emit({
141
+ role: "runner",
142
+ kind: terminal && current.status !== "pass" ? "error" : "status",
143
+ content: terminal
144
+ ? `Managed verification ${current.status} after ${elapsed}${recovered ? " (recovered from its durable receipt)" : ""}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`
145
+ : `QA started managed verification ${current.commandId}. Forge is supervising it; durable progress remains available in the UI while the provider continues independent work.`,
146
+ eventType: terminal ? "verification_completed" : "verification_waiting",
147
+ data: JSON.stringify({
148
+ command_id: current.commandId,
149
+ status: current.status,
150
+ elapsed_ms: elapsedMs,
151
+ output_revision: current.outputRevision,
152
+ last_output_at: current.lastOutputAt,
153
+ recovered,
154
+ }),
155
+ });
156
+ }
157
+ }
158
+ async function parseManagedVerificationSnapshot(receiptPath) {
159
+ try {
160
+ const receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
161
+ const commandId = typeof receipt.command_id === "string" ? receipt.command_id : null;
162
+ const status = typeof receipt.status === "string" ? receipt.status : null;
163
+ if (!commandId || !status)
164
+ return null;
165
+ return {
166
+ commandId,
167
+ status,
168
+ elapsedMs: typeof receipt.elapsed_ms === "number" ? receipt.elapsed_ms : null,
169
+ startedAt: typeof receipt.started_at === "string" ? receipt.started_at : null,
170
+ outputRevision: typeof receipt.output_revision === "number" ? receipt.output_revision : null,
171
+ lastOutputAt: typeof receipt.last_output_at === "string" ? receipt.last_output_at : null,
172
+ lastOutputStream: receipt.last_output_stream === "stdout" || receipt.last_output_stream === "stderr"
173
+ ? receipt.last_output_stream
174
+ : null,
175
+ stdoutTail: tail(typeof receipt.stdout === "string" ? receipt.stdout : ""),
176
+ stderrTail: tail(typeof receipt.stderr === "string" ? receipt.stderr : ""),
177
+ };
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ }
183
+ function tail(value) {
184
+ return value.trim().split(/\r?\n/u).slice(-4).join("\n");
185
+ }
186
+ function formatElapsed(milliseconds) {
187
+ const seconds = Math.max(0, Math.round(milliseconds / 1000));
188
+ const minutes = Math.floor(seconds / 60);
189
+ return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
190
+ }
191
+ class ProviderToolActivityHeartbeat {
192
+ outputState;
193
+ emit;
194
+ intervalMs;
195
+ timer = null;
196
+ constructor(outputState, emit, intervalMs) {
197
+ this.outputState = outputState;
198
+ this.emit = emit;
199
+ this.intervalMs = intervalMs;
200
+ }
201
+ async consume(line) {
202
+ const events = this.outputState.consume(line);
203
+ this.sync();
204
+ for (const event of events) {
205
+ await this.emit(event);
206
+ }
207
+ }
208
+ stop() {
209
+ if (this.timer) {
210
+ clearInterval(this.timer);
211
+ this.timer = null;
212
+ }
213
+ }
214
+ sync() {
215
+ if (!this.outputState.hasActiveToolCall) {
216
+ this.stop();
217
+ return;
218
+ }
219
+ if (this.timer) {
220
+ return;
221
+ }
222
+ this.timer = setInterval(() => {
223
+ if (!this.outputState.hasActiveToolCall) {
224
+ this.stop();
225
+ return;
226
+ }
227
+ void this.emit({
228
+ role: "runner",
229
+ kind: "status",
230
+ content: "A supervised provider tool is still running.",
231
+ eventType: "provider_tool_heartbeat",
232
+ }).catch(() => undefined);
233
+ }, Math.max(1, this.intervalMs));
234
+ }
235
+ }
58
236
  const fakeWorkerModel = "server-local-fake-worker";
59
237
  export class RoutingProviderCliService {
60
238
  realProvider;
@@ -97,9 +275,13 @@ export class RoutingProviderCliService {
97
275
  export class ProcessBackedProviderCliService {
98
276
  processRunner;
99
277
  git;
100
- constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient()) {
278
+ providerToolHeartbeatIntervalMs;
279
+ publicationCoordinator;
280
+ constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs, publicationCoordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot())) {
101
281
  this.processRunner = processRunner;
102
282
  this.git = git;
283
+ this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
284
+ this.publicationCoordinator = publicationCoordinator;
103
285
  }
104
286
  async runDraft(context) {
105
287
  const { dispatch } = context;
@@ -113,10 +295,10 @@ export class ProcessBackedProviderCliService {
113
295
  await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
114
296
  const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
115
297
  const decomposeDraft = isDecomposeDraftDispatch(dispatch);
116
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft" });
298
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
117
299
  const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
118
300
  const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
119
- const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt);
301
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
120
302
  const command = buildProviderCliCommand(dispatch, {
121
303
  worktreePath: worktree.worktreePath,
122
304
  prompt: draftPrompt,
@@ -139,7 +321,7 @@ export class ProcessBackedProviderCliService {
139
321
  role: "runner",
140
322
  kind: "checkpoint",
141
323
  content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
142
- eventType: "provider_cli_started"
324
+ eventType: "provider_cli_starting"
143
325
  });
144
326
  await context.emit({
145
327
  role: "runner",
@@ -150,6 +332,7 @@ export class ProcessBackedProviderCliService {
150
332
  eventType: "completion_tools_configured"
151
333
  });
152
334
  const outputState = createProviderOutputState(dispatch.execution.provider);
335
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
153
336
  try {
154
337
  const providerCancellation = new AbortController();
155
338
  const abortProvider = () => providerCancellation.abort();
@@ -160,17 +343,23 @@ export class ProcessBackedProviderCliService {
160
343
  context.signal?.addEventListener("abort", abortProvider, { once: true });
161
344
  }
162
345
  try {
163
- const runPromise = this.processRunner.run({
346
+ const runPromise = (context.processRunner ?? this.processRunner).run({
164
347
  command: command.command,
165
348
  args: command.args,
166
349
  cwd: worktree.worktreePath,
167
350
  env: providerProcessEnv(dispatch, context.runtimeEnvironment),
168
351
  stdin: pipePromptToStdin ? providerPrompt : undefined
169
352
  }, {
353
+ onStarted: async () => {
354
+ await context.emit({
355
+ role: "runner",
356
+ kind: "checkpoint",
357
+ content: "Provider CLI Draft is supervised and ready.",
358
+ eventType: "provider_cli_started"
359
+ });
360
+ },
170
361
  onStdoutLine: async (line) => {
171
- for (const event of outputState.consume(line)) {
172
- await context.emit(event);
173
- }
362
+ await toolHeartbeat.consume(line);
174
363
  }
175
364
  }, providerCancellation.signal);
176
365
  const artifactWaitCancellation = new AbortController();
@@ -266,6 +455,7 @@ export class ProcessBackedProviderCliService {
266
455
  }
267
456
  }
268
457
  finally {
458
+ toolHeartbeat.stop();
269
459
  await cleanupCompletionTools(completionTools, context.emit);
270
460
  }
271
461
  }
@@ -284,14 +474,18 @@ export class ProcessBackedProviderCliService {
284
474
  const specPath = path.join(metadataRoot, "spec.md");
285
475
  await fs.writeFile(specPath, specBody, "utf8");
286
476
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
287
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
477
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase, runtimeEnvironment: context.runtimeEnvironment });
478
+ const implementationPrompt = debugCycle
479
+ ? buildDebugCyclePrompt(dispatch, phase)
480
+ : developCycle
481
+ ? buildDevelopCyclePrompt(dispatch, phase)
482
+ : buildImplementationPrompt(dispatch, phase);
483
+ const providerPrompt = buildProviderCliPrompt(dispatch, implementationPrompt, context.runtimeEnvironment);
484
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
288
485
  const command = buildProviderCliCommand(dispatch, {
289
486
  worktreePath: worktree.worktreePath,
290
- prompt: debugCycle
291
- ? buildDebugCyclePrompt(dispatch, phase)
292
- : developCycle
293
- ? buildDevelopCyclePrompt(dispatch, phase)
294
- : buildImplementationPrompt(dispatch, phase),
487
+ prompt: implementationPrompt,
488
+ promptFromStdin: pipePromptToStdin,
295
489
  completionTools,
296
490
  runtimeEnvironment: context.runtimeEnvironment
297
491
  });
@@ -299,7 +493,7 @@ export class ProcessBackedProviderCliService {
299
493
  role: "runner",
300
494
  kind: "checkpoint",
301
495
  content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
302
- eventType: "provider_cli_started"
496
+ eventType: "provider_cli_starting"
303
497
  });
304
498
  await context.emit({
305
499
  role: "runner",
@@ -312,24 +506,33 @@ export class ProcessBackedProviderCliService {
312
506
  eventType: "completion_tools_configured"
313
507
  });
314
508
  const outputState = createProviderOutputState(dispatch.execution.provider);
509
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
315
510
  const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
316
511
  let result;
317
512
  try {
318
513
  liveCompletionEvents.start();
319
- result = await this.processRunner.run({
514
+ result = await (context.processRunner ?? this.processRunner).run({
320
515
  command: command.command,
321
516
  args: command.args,
322
517
  cwd: worktree.worktreePath,
323
- env: providerProcessEnv(dispatch, context.runtimeEnvironment)
518
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
519
+ stdin: pipePromptToStdin ? providerPrompt : undefined
324
520
  }, {
521
+ onStarted: async () => {
522
+ await context.emit({
523
+ role: "runner",
524
+ kind: "checkpoint",
525
+ content: "Provider CLI is supervised and ready.",
526
+ eventType: "provider_cli_started"
527
+ });
528
+ },
325
529
  onStdoutLine: async (line) => {
326
- for (const event of outputState.consume(line)) {
327
- await context.emit(event);
328
- }
530
+ await toolHeartbeat.consume(line);
329
531
  }
330
532
  }, context.signal);
331
533
  }
332
534
  finally {
535
+ toolHeartbeat.stop();
333
536
  await liveCompletionEvents.stop();
334
537
  await cleanupCompletionTools(completionTools, context.emit);
335
538
  }
@@ -346,7 +549,7 @@ export class ProcessBackedProviderCliService {
346
549
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
347
550
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
348
551
  if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
349
- await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
552
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit);
350
553
  await context.emit({
351
554
  role: "runner",
352
555
  kind: "checkpoint",
@@ -404,10 +607,14 @@ export class ProcessBackedProviderCliService {
404
607
  const specPath = path.join(metadataRoot, "spec.md");
405
608
  await fs.writeFile(specPath, specBody, "utf8");
406
609
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
407
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
610
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect", runtimeEnvironment: context.runtimeEnvironment });
611
+ const architectPrompt = buildArchitectPrompt(dispatch);
612
+ const providerPrompt = buildProviderCliPrompt(dispatch, architectPrompt, context.runtimeEnvironment);
613
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
408
614
  const command = buildProviderCliCommand(dispatch, {
409
615
  worktreePath: worktree.worktreePath,
410
- prompt: buildArchitectPrompt(dispatch),
616
+ prompt: architectPrompt,
617
+ promptFromStdin: pipePromptToStdin,
411
618
  completionTools,
412
619
  runtimeEnvironment: context.runtimeEnvironment
413
620
  });
@@ -416,7 +623,7 @@ export class ProcessBackedProviderCliService {
416
623
  role: "runner",
417
624
  kind: "checkpoint",
418
625
  content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
419
- eventType: "provider_cli_started"
626
+ eventType: "provider_cli_starting"
420
627
  });
421
628
  await context.emit({
422
629
  role: "runner",
@@ -425,24 +632,31 @@ export class ProcessBackedProviderCliService {
425
632
  eventType: "completion_tools_configured"
426
633
  });
427
634
  const outputState = createProviderOutputState(dispatch.execution.provider);
635
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
428
636
  const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
429
637
  let result;
430
638
  try {
431
639
  liveArchitectEvents.start();
432
- result = await this.processRunner.run({
640
+ result = await (context.processRunner ?? this.processRunner).run({
433
641
  command: command.command,
434
642
  args: command.args,
435
643
  cwd: worktree.worktreePath,
436
- env: providerProcessEnv(dispatch, context.runtimeEnvironment)
644
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
645
+ stdin: pipePromptToStdin ? providerPrompt : undefined
437
646
  }, {
438
- onStdoutLine: async (line) => {
439
- for (const event of outputState.consume(line)) {
440
- await context.emit(event);
441
- }
442
- }
647
+ onStarted: async () => {
648
+ await context.emit({
649
+ role: "runner",
650
+ kind: "checkpoint",
651
+ content: "Provider CLI Architect is supervised and ready.",
652
+ eventType: "provider_cli_started"
653
+ });
654
+ },
655
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
443
656
  }, context.signal);
444
657
  }
445
658
  finally {
659
+ toolHeartbeat.stop();
446
660
  await liveArchitectEvents.stop();
447
661
  await cleanupCompletionTools(completionTools, context.emit);
448
662
  }
@@ -496,10 +710,14 @@ export class ProcessBackedProviderCliService {
496
710
  const specPath = path.join(metadataRoot, "spec.md");
497
711
  await fs.writeFile(specPath, specBody, "utf8");
498
712
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
499
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
713
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa", runtimeEnvironment: context.runtimeEnvironment });
714
+ const qaPrompt = buildQaPrompt(dispatch, specBody);
715
+ const providerPrompt = buildProviderCliPrompt(dispatch, qaPrompt, context.runtimeEnvironment);
716
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
500
717
  const command = buildProviderCliCommand(dispatch, {
501
718
  worktreePath: worktree.worktreePath,
502
- prompt: buildQaPrompt(dispatch, specBody),
719
+ prompt: qaPrompt,
720
+ promptFromStdin: pipePromptToStdin,
503
721
  completionTools,
504
722
  runtimeEnvironment: context.runtimeEnvironment
505
723
  });
@@ -508,7 +726,7 @@ export class ProcessBackedProviderCliService {
508
726
  role: "runner",
509
727
  kind: "checkpoint",
510
728
  content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
511
- eventType: "provider_cli_started"
729
+ eventType: "provider_cli_starting"
512
730
  });
513
731
  await context.emit({
514
732
  role: "runner",
@@ -517,24 +735,34 @@ export class ProcessBackedProviderCliService {
517
735
  eventType: "completion_tools_configured"
518
736
  });
519
737
  const outputState = createProviderOutputState(dispatch.execution.provider);
738
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
739
+ const verificationProgress = new ManagedVerificationProgressEmitter(context.runtimeEnvironment?.FORGE_LOG_DIR, context.emit);
520
740
  const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
521
741
  let result;
522
742
  try {
523
743
  liveQaEvents.start();
524
- result = await this.processRunner.run({
744
+ verificationProgress.start();
745
+ result = await (context.processRunner ?? this.processRunner).run({
525
746
  command: command.command,
526
747
  args: command.args,
527
748
  cwd: worktree.worktreePath,
528
- env: providerProcessEnv(dispatch, context.runtimeEnvironment)
749
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
750
+ stdin: pipePromptToStdin ? providerPrompt : undefined
529
751
  }, {
530
- onStdoutLine: async (line) => {
531
- for (const event of outputState.consume(line)) {
532
- await context.emit(event);
533
- }
534
- }
752
+ onStarted: async () => {
753
+ await context.emit({
754
+ role: "runner",
755
+ kind: "checkpoint",
756
+ content: "Provider CLI QA is supervised and ready.",
757
+ eventType: "provider_cli_started"
758
+ });
759
+ },
760
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
535
761
  }, context.signal);
536
762
  }
537
763
  finally {
764
+ toolHeartbeat.stop();
765
+ await verificationProgress.stop();
538
766
  await liveQaEvents.stop();
539
767
  await cleanupCompletionTools(completionTools, context.emit);
540
768
  }
@@ -590,41 +818,113 @@ export class ProcessBackedProviderCliService {
590
818
  const { dispatch, worktree } = context;
591
819
  const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
592
820
  const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
821
+ let repairSpec = null;
822
+ let repairEnvironment = context.runtimeEnvironment;
823
+ let completionTools = null;
824
+ if (repairChecks) {
825
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
826
+ await fs.mkdir(metadataRoot, { recursive: true });
827
+ const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
828
+ if (!laneCommand) {
829
+ throw new Error(`CI repair readiness failed: lane command for '${dispatch.execution.lane}' was not resolved, so the approved spec cannot be materialized.`);
830
+ }
831
+ const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
832
+ const specPath = path.join(metadataRoot, "spec.md");
833
+ await fs.writeFile(specPath, specBody, "utf8");
834
+ await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
835
+ repairSpec = {
836
+ repoPath: worktree.worktreePath,
837
+ path: specPath,
838
+ body: specBody,
839
+ producedBy: "forge-lane-command",
840
+ status: "approved",
841
+ };
842
+ const readiness = await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
843
+ repairEnvironment = readiness.environment;
844
+ completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "ci_repair", runtimeEnvironment: repairEnvironment });
845
+ await context.emit({
846
+ role: "runner",
847
+ kind: "checkpoint",
848
+ 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.`,
849
+ eventType: "ci_repair_readiness_ready",
850
+ });
851
+ await context.emit({
852
+ role: "runner",
853
+ kind: "checkpoint",
854
+ content: "Forge CI repair completion tool configured; provider must call forge.complete_ci_repair before Bridge-owned commit and push.",
855
+ eventType: "completion_tools_configured",
856
+ });
857
+ }
858
+ const repairPrompt = repairChecks
859
+ ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
860
+ : addressComments
861
+ ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
862
+ : buildConflictResolvePrompt(dispatch, worktree.branchName);
863
+ const providerPrompt = buildProviderCliPrompt(dispatch, repairPrompt, repairEnvironment);
864
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
593
865
  const command = buildProviderCliCommand(dispatch, {
594
866
  worktreePath: worktree.worktreePath,
595
- prompt: repairChecks
596
- ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
597
- : addressComments
598
- ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
599
- : buildConflictResolvePrompt(dispatch, worktree.branchName),
600
- runtimeEnvironment: context.runtimeEnvironment
867
+ prompt: repairPrompt,
868
+ promptFromStdin: pipePromptToStdin,
869
+ completionTools: completionTools ?? undefined,
870
+ runtimeEnvironment: repairEnvironment
601
871
  });
602
872
  await context.emit({
603
873
  role: "runner",
604
874
  kind: "checkpoint",
605
875
  content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
606
- eventType: "provider_cli_started"
876
+ eventType: "provider_cli_starting"
607
877
  });
608
878
  const outputState = createProviderOutputState(dispatch.execution.provider);
609
- const result = await this.processRunner.run({
610
- command: command.command,
611
- args: command.args,
612
- cwd: worktree.worktreePath,
613
- env: providerProcessEnv(dispatch, context.runtimeEnvironment)
614
- }, {
615
- onStdoutLine: async (line) => {
616
- for (const event of outputState.consume(line)) {
617
- await context.emit(event);
879
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
880
+ let result;
881
+ try {
882
+ result = await (context.processRunner ?? this.processRunner).run({
883
+ command: command.command,
884
+ args: command.args,
885
+ cwd: worktree.worktreePath,
886
+ env: providerProcessEnv(dispatch, repairEnvironment),
887
+ stdin: pipePromptToStdin ? providerPrompt : undefined
888
+ }, {
889
+ onStarted: async () => {
890
+ await context.emit({
891
+ role: "runner",
892
+ kind: "checkpoint",
893
+ content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} is supervised and ready.`,
894
+ eventType: "provider_cli_started"
895
+ });
896
+ },
897
+ onStdoutLine: async (line) => {
898
+ await toolHeartbeat.consume(line);
618
899
  }
900
+ }, context.signal);
901
+ }
902
+ finally {
903
+ toolHeartbeat.stop();
904
+ if (completionTools) {
905
+ await cleanupCompletionTools(completionTools, context.emit);
619
906
  }
620
- }, context.signal);
907
+ }
621
908
  if (result.exitCode !== 0) {
622
909
  const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
623
910
  throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
624
911
  }
912
+ let repairArtifact = null;
913
+ if (repairChecks) {
914
+ repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
915
+ if (!repairArtifact) {
916
+ throw new Error("Provider CLI completed but did not call forge.complete_ci_repair. Forge did not commit or push the repair.");
917
+ }
918
+ if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
919
+ throw new Error(`CI repair completion gate recorded ${repairArtifact.status}; Forge did not commit or push the repair.`);
920
+ }
921
+ await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
922
+ await emitPhaseCompletionEvent(context.emit, repairArtifact);
923
+ await emitPhaseGateEvents(context.emit, repairArtifact);
924
+ }
625
925
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
626
926
  if (providerOwnsBranchPublication(context)) {
627
- await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
927
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit);
628
928
  }
629
929
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
630
930
  await context.emit({
@@ -642,7 +942,10 @@ export class ProcessBackedProviderCliService {
642
942
  ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
643
943
  : addressComments
644
944
  ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
645
- : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
945
+ : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`,
946
+ spec: repairSpec,
947
+ artifact: repairArtifact,
948
+ artifacts: repairArtifact ? [repairArtifact] : [],
646
949
  };
647
950
  }
648
951
  }
@@ -708,8 +1011,12 @@ function clipFileName(fileName, maxLength) {
708
1011
  return `${stem.slice(0, maxStemLength)}${extension}`;
709
1012
  }
710
1013
  const providerPromptStdinThreshold = 8_000;
711
- function shouldPipePromptToStdin(provider, prompt) {
712
- return provider.trim().toLowerCase() === "codex" && Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold;
1014
+ function shouldPipePromptToStdin(provider, prompt, force = false) {
1015
+ return provider.trim().toLowerCase() === "codex"
1016
+ && (force || Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold);
1017
+ }
1018
+ function usesManagedWindowsHost(dispatch) {
1019
+ return process.platform === "win32" && dispatch.sandbox?.executionMode === "host";
713
1020
  }
714
1021
  function isFakeWorkerModel(model) {
715
1022
  return model.trim().toLowerCase() === fakeWorkerModel;