@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.eeefdb0d19c1

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 (118) hide show
  1. package/README.md +34 -0
  2. package/dist/bridge/bridge.base.d.ts +2 -1
  3. package/dist/bridge/bridge.service.d.ts +22 -3
  4. package/dist/bridge/bridge.service.js +416 -40
  5. package/dist/bridge/bridge.types.d.ts +59 -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 +37 -3
  14. package/dist/cli/cli.service.d.ts +30 -2
  15. package/dist/cli/cli.service.js +182 -23
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +41 -1
  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 +343 -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/preflight.d.ts +14 -0
  43. package/dist/sandbox/preflight.js +91 -12
  44. package/dist/sandbox/process.js +125 -40
  45. package/dist/sandbox/repository-lock.js +37 -1
  46. package/dist/sandbox/runtime.d.ts +66 -3
  47. package/dist/sandbox/runtime.js +670 -105
  48. package/dist/sandbox/sandbox.types.d.ts +3 -0
  49. package/dist/sandbox/smoke.d.ts +76 -0
  50. package/dist/sandbox/smoke.js +380 -0
  51. package/dist/sandbox/worker-supervisor.js +1 -1
  52. package/dist/sandbox/worker.js +2 -5
  53. package/dist/sandbox/workspace.service.d.ts +1 -0
  54. package/dist/sandbox/workspace.service.js +28 -5
  55. package/dist/session/managed-attempt-tools.d.ts +12 -0
  56. package/dist/session/managed-attempt-tools.js +350 -0
  57. package/dist/session/phase-completion.artifacts.js +9 -2
  58. package/dist/session/phase-completion.d.ts +4 -2
  59. package/dist/session/phase-completion.helpers.js +27 -3
  60. package/dist/session/phase-completion.js +12 -3
  61. package/dist/session/phase-completion.prompts.d.ts +3 -3
  62. package/dist/session/phase-completion.prompts.js +19 -5
  63. package/dist/session/phase-completion.runtime.d.ts +12 -1
  64. package/dist/session/phase-completion.runtime.js +127 -7
  65. package/dist/session/phase-completion.schemas.js +108 -3
  66. package/dist/session/phase-completion.validation.d.ts +4 -1
  67. package/dist/session/phase-completion.validation.js +106 -2
  68. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  69. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  70. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  71. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  72. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  73. package/dist/session/provider-cli/provider-cli.process.d.ts +16 -0
  74. package/dist/session/provider-cli/provider-cli.process.js +212 -25
  75. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  76. package/dist/session/provider-cli/provider-cli.prompts.js +84 -17
  77. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  78. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  79. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  80. package/dist/session/provider-cli/provider-cli.service.d.ts +3 -2
  81. package/dist/session/provider-cli/provider-cli.service.js +254 -71
  82. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  83. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  84. package/dist/session/provider-cli/provider-output-state.js +30 -2
  85. package/dist/session/session-redaction.d.ts +3 -0
  86. package/dist/session/session-redaction.js +23 -0
  87. package/dist/session/session.base.d.ts +6 -1
  88. package/dist/session/session.service.d.ts +8 -2
  89. package/dist/session/session.service.js +436 -41
  90. package/dist/session/session.types.d.ts +55 -0
  91. package/dist/session/session.types.js +69 -2
  92. package/dist/shared/path/path.js +3 -1
  93. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  94. package/dist/shared/process/host-process-recovery.js +78 -0
  95. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  96. package/dist/shared/process/host-process-supervisor.js +282 -0
  97. package/dist/shared/process/process.d.ts +1 -0
  98. package/dist/shared/process/process.js +1 -0
  99. package/dist/shared/redaction/redaction.js +6 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/worktree/artifact-injector.d.ts +3 -2
  103. package/dist/worktree/artifact-injector.js +43 -4
  104. package/dist/worktree/dependency-warmer.d.ts +10 -1
  105. package/dist/worktree/dependency-warmer.js +94 -5
  106. package/dist/worktree/index.d.ts +1 -0
  107. package/dist/worktree/index.js +1 -0
  108. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  109. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  110. package/dist/worktree/worktree-paths.d.ts +4 -0
  111. package/dist/worktree/worktree-paths.js +30 -0
  112. package/dist/worktree/worktree.base.d.ts +3 -0
  113. package/dist/worktree/worktree.service.d.ts +7 -2
  114. package/dist/worktree/worktree.service.js +72 -44
  115. package/dist/worktree/worktree.types.d.ts +3 -1
  116. package/package.json +3 -2
  117. package/runtime/windows-job-supervisor.ps1 +356 -0
  118. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -5,13 +5,14 @@ import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-inject
5
5
  import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
6
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";
7
7
  import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
8
- import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
8
+ import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
9
9
  import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
10
10
  import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
11
11
  import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
12
+ import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
12
13
  export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
13
14
  export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
14
- export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, StreamingProviderProcessRunner } from "./provider-cli.process.js";
15
+ export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli.process.js";
15
16
  export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
16
17
  async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
17
18
  try {
@@ -38,6 +39,9 @@ async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
38
39
  }
39
40
  }
40
41
  }
42
+ function providerOwnsBranchPublication(context) {
43
+ return context.dispatch.sandbox?.executionMode !== "docker";
44
+ }
41
45
  function remoteTrackingRefspec(branchName) {
42
46
  return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
43
47
  }
@@ -52,6 +56,52 @@ function isNonFastForwardPushError(err) {
52
56
  function errorMessage(err) {
53
57
  return err instanceof Error ? err.message : String(err);
54
58
  }
59
+ const defaultProviderToolHeartbeatIntervalMs = 60_000;
60
+ class ProviderToolActivityHeartbeat {
61
+ outputState;
62
+ emit;
63
+ intervalMs;
64
+ timer = null;
65
+ constructor(outputState, emit, intervalMs) {
66
+ this.outputState = outputState;
67
+ this.emit = emit;
68
+ this.intervalMs = intervalMs;
69
+ }
70
+ async consume(line) {
71
+ const events = this.outputState.consume(line);
72
+ this.sync();
73
+ for (const event of events) {
74
+ await this.emit(event);
75
+ }
76
+ }
77
+ stop() {
78
+ if (this.timer) {
79
+ clearInterval(this.timer);
80
+ this.timer = null;
81
+ }
82
+ }
83
+ sync() {
84
+ if (!this.outputState.hasActiveToolCall) {
85
+ this.stop();
86
+ return;
87
+ }
88
+ if (this.timer) {
89
+ return;
90
+ }
91
+ this.timer = setInterval(() => {
92
+ if (!this.outputState.hasActiveToolCall) {
93
+ this.stop();
94
+ return;
95
+ }
96
+ void this.emit({
97
+ role: "runner",
98
+ kind: "status",
99
+ content: "A supervised provider tool is still running.",
100
+ eventType: "provider_tool_heartbeat",
101
+ }).catch(() => undefined);
102
+ }, Math.max(1, this.intervalMs));
103
+ }
104
+ }
55
105
  const fakeWorkerModel = "server-local-fake-worker";
56
106
  export class RoutingProviderCliService {
57
107
  realProvider;
@@ -94,9 +144,11 @@ export class RoutingProviderCliService {
94
144
  export class ProcessBackedProviderCliService {
95
145
  processRunner;
96
146
  git;
97
- constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient()) {
147
+ providerToolHeartbeatIntervalMs;
148
+ constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs) {
98
149
  this.processRunner = processRunner;
99
150
  this.git = git;
151
+ this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
100
152
  }
101
153
  async runDraft(context) {
102
154
  const { dispatch } = context;
@@ -110,15 +162,18 @@ export class ProcessBackedProviderCliService {
110
162
  await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
111
163
  const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
112
164
  const decomposeDraft = isDecomposeDraftDispatch(dispatch);
113
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft" });
165
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
114
166
  const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
115
- const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, draftPrompt);
167
+ const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
168
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
116
169
  const command = buildProviderCliCommand(dispatch, {
117
170
  worktreePath: worktree.worktreePath,
118
- prompt: pipePromptToStdin ? "-" : draftPrompt,
171
+ prompt: draftPrompt,
172
+ promptFromStdin: pipePromptToStdin,
119
173
  completionTools: completionTools ?? undefined,
120
174
  skipGitRepoCheck: false,
121
- imagePaths: stagedAttachments.imagePaths
175
+ imagePaths: stagedAttachments.imagePaths,
176
+ runtimeEnvironment: context.runtimeEnvironment
122
177
  });
123
178
  const draftArtifactSlugs = dispatch.artifacts
124
179
  .filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
@@ -133,7 +188,7 @@ export class ProcessBackedProviderCliService {
133
188
  role: "runner",
134
189
  kind: "checkpoint",
135
190
  content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
136
- eventType: "provider_cli_started"
191
+ eventType: "provider_cli_starting"
137
192
  });
138
193
  await context.emit({
139
194
  role: "runner",
@@ -144,6 +199,7 @@ export class ProcessBackedProviderCliService {
144
199
  eventType: "completion_tools_configured"
145
200
  });
146
201
  const outputState = createProviderOutputState(dispatch.execution.provider);
202
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
147
203
  try {
148
204
  const providerCancellation = new AbortController();
149
205
  const abortProvider = () => providerCancellation.abort();
@@ -154,17 +210,23 @@ export class ProcessBackedProviderCliService {
154
210
  context.signal?.addEventListener("abort", abortProvider, { once: true });
155
211
  }
156
212
  try {
157
- const runPromise = this.processRunner.run({
213
+ const runPromise = (context.processRunner ?? this.processRunner).run({
158
214
  command: command.command,
159
215
  args: command.args,
160
216
  cwd: worktree.worktreePath,
161
- env: providerProcessEnv(dispatch),
162
- stdin: pipePromptToStdin ? draftPrompt : undefined
217
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
218
+ stdin: pipePromptToStdin ? providerPrompt : undefined
163
219
  }, {
220
+ onStarted: async () => {
221
+ await context.emit({
222
+ role: "runner",
223
+ kind: "checkpoint",
224
+ content: "Provider CLI Draft is supervised and ready.",
225
+ eventType: "provider_cli_started"
226
+ });
227
+ },
164
228
  onStdoutLine: async (line) => {
165
- for (const event of outputState.consume(line)) {
166
- await context.emit(event);
167
- }
229
+ await toolHeartbeat.consume(line);
168
230
  }
169
231
  }, providerCancellation.signal);
170
232
  const artifactWaitCancellation = new AbortController();
@@ -260,6 +322,7 @@ export class ProcessBackedProviderCliService {
260
322
  }
261
323
  }
262
324
  finally {
325
+ toolHeartbeat.stop();
263
326
  await cleanupCompletionTools(completionTools, context.emit);
264
327
  }
265
328
  }
@@ -278,21 +341,26 @@ export class ProcessBackedProviderCliService {
278
341
  const specPath = path.join(metadataRoot, "spec.md");
279
342
  await fs.writeFile(specPath, specBody, "utf8");
280
343
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
281
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
344
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase, runtimeEnvironment: context.runtimeEnvironment });
345
+ const implementationPrompt = debugCycle
346
+ ? buildDebugCyclePrompt(dispatch, phase)
347
+ : developCycle
348
+ ? buildDevelopCyclePrompt(dispatch, phase)
349
+ : buildImplementationPrompt(dispatch, phase);
350
+ const providerPrompt = buildProviderCliPrompt(dispatch, implementationPrompt, context.runtimeEnvironment);
351
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
282
352
  const command = buildProviderCliCommand(dispatch, {
283
353
  worktreePath: worktree.worktreePath,
284
- prompt: debugCycle
285
- ? buildDebugCyclePrompt(dispatch, phase)
286
- : developCycle
287
- ? buildDevelopCyclePrompt(dispatch, phase)
288
- : buildImplementationPrompt(dispatch, phase),
289
- completionTools
354
+ prompt: implementationPrompt,
355
+ promptFromStdin: pipePromptToStdin,
356
+ completionTools,
357
+ runtimeEnvironment: context.runtimeEnvironment
290
358
  });
291
359
  await context.emit({
292
360
  role: "runner",
293
361
  kind: "checkpoint",
294
362
  content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
295
- eventType: "provider_cli_started"
363
+ eventType: "provider_cli_starting"
296
364
  });
297
365
  await context.emit({
298
366
  role: "runner",
@@ -305,24 +373,33 @@ export class ProcessBackedProviderCliService {
305
373
  eventType: "completion_tools_configured"
306
374
  });
307
375
  const outputState = createProviderOutputState(dispatch.execution.provider);
376
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
308
377
  const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
309
378
  let result;
310
379
  try {
311
380
  liveCompletionEvents.start();
312
- result = await this.processRunner.run({
381
+ result = await (context.processRunner ?? this.processRunner).run({
313
382
  command: command.command,
314
383
  args: command.args,
315
384
  cwd: worktree.worktreePath,
316
- env: providerProcessEnv(dispatch)
385
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
386
+ stdin: pipePromptToStdin ? providerPrompt : undefined
317
387
  }, {
388
+ onStarted: async () => {
389
+ await context.emit({
390
+ role: "runner",
391
+ kind: "checkpoint",
392
+ content: "Provider CLI is supervised and ready.",
393
+ eventType: "provider_cli_started"
394
+ });
395
+ },
318
396
  onStdoutLine: async (line) => {
319
- for (const event of outputState.consume(line)) {
320
- await context.emit(event);
321
- }
397
+ await toolHeartbeat.consume(line);
322
398
  }
323
399
  }, context.signal);
324
400
  }
325
401
  finally {
402
+ toolHeartbeat.stop();
326
403
  await liveCompletionEvents.stop();
327
404
  await cleanupCompletionTools(completionTools, context.emit);
328
405
  }
@@ -338,7 +415,7 @@ export class ProcessBackedProviderCliService {
338
415
  });
339
416
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
340
417
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
341
- if (developCycle || debugCycle || phase === "implementation") {
418
+ if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
342
419
  await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
343
420
  await context.emit({
344
421
  role: "runner",
@@ -397,18 +474,23 @@ export class ProcessBackedProviderCliService {
397
474
  const specPath = path.join(metadataRoot, "spec.md");
398
475
  await fs.writeFile(specPath, specBody, "utf8");
399
476
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
400
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
477
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect", runtimeEnvironment: context.runtimeEnvironment });
478
+ const architectPrompt = buildArchitectPrompt(dispatch);
479
+ const providerPrompt = buildProviderCliPrompt(dispatch, architectPrompt, context.runtimeEnvironment);
480
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
401
481
  const command = buildProviderCliCommand(dispatch, {
402
482
  worktreePath: worktree.worktreePath,
403
- prompt: buildArchitectPrompt(dispatch),
404
- completionTools
483
+ prompt: architectPrompt,
484
+ promptFromStdin: pipePromptToStdin,
485
+ completionTools,
486
+ runtimeEnvironment: context.runtimeEnvironment
405
487
  });
406
488
  const commandText = formatProviderCliCommandForCheckpoint(command);
407
489
  await context.emit({
408
490
  role: "runner",
409
491
  kind: "checkpoint",
410
492
  content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
411
- eventType: "provider_cli_started"
493
+ eventType: "provider_cli_starting"
412
494
  });
413
495
  await context.emit({
414
496
  role: "runner",
@@ -417,24 +499,31 @@ export class ProcessBackedProviderCliService {
417
499
  eventType: "completion_tools_configured"
418
500
  });
419
501
  const outputState = createProviderOutputState(dispatch.execution.provider);
502
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
420
503
  const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
421
504
  let result;
422
505
  try {
423
506
  liveArchitectEvents.start();
424
- result = await this.processRunner.run({
507
+ result = await (context.processRunner ?? this.processRunner).run({
425
508
  command: command.command,
426
509
  args: command.args,
427
510
  cwd: worktree.worktreePath,
428
- env: providerProcessEnv(dispatch)
511
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
512
+ stdin: pipePromptToStdin ? providerPrompt : undefined
429
513
  }, {
430
- onStdoutLine: async (line) => {
431
- for (const event of outputState.consume(line)) {
432
- await context.emit(event);
433
- }
434
- }
514
+ onStarted: async () => {
515
+ await context.emit({
516
+ role: "runner",
517
+ kind: "checkpoint",
518
+ content: "Provider CLI Architect is supervised and ready.",
519
+ eventType: "provider_cli_started"
520
+ });
521
+ },
522
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
435
523
  }, context.signal);
436
524
  }
437
525
  finally {
526
+ toolHeartbeat.stop();
438
527
  await liveArchitectEvents.stop();
439
528
  await cleanupCompletionTools(completionTools, context.emit);
440
529
  }
@@ -488,18 +577,23 @@ export class ProcessBackedProviderCliService {
488
577
  const specPath = path.join(metadataRoot, "spec.md");
489
578
  await fs.writeFile(specPath, specBody, "utf8");
490
579
  await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
491
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
580
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa", runtimeEnvironment: context.runtimeEnvironment });
581
+ const qaPrompt = buildQaPrompt(dispatch, specBody);
582
+ const providerPrompt = buildProviderCliPrompt(dispatch, qaPrompt, context.runtimeEnvironment);
583
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
492
584
  const command = buildProviderCliCommand(dispatch, {
493
585
  worktreePath: worktree.worktreePath,
494
- prompt: buildQaPrompt(dispatch, specBody),
495
- completionTools
586
+ prompt: qaPrompt,
587
+ promptFromStdin: pipePromptToStdin,
588
+ completionTools,
589
+ runtimeEnvironment: context.runtimeEnvironment
496
590
  });
497
591
  const commandText = formatProviderCliCommandForCheckpoint(command);
498
592
  await context.emit({
499
593
  role: "runner",
500
594
  kind: "checkpoint",
501
595
  content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
502
- eventType: "provider_cli_started"
596
+ eventType: "provider_cli_starting"
503
597
  });
504
598
  await context.emit({
505
599
  role: "runner",
@@ -508,24 +602,31 @@ export class ProcessBackedProviderCliService {
508
602
  eventType: "completion_tools_configured"
509
603
  });
510
604
  const outputState = createProviderOutputState(dispatch.execution.provider);
605
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
511
606
  const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
512
607
  let result;
513
608
  try {
514
609
  liveQaEvents.start();
515
- result = await this.processRunner.run({
610
+ result = await (context.processRunner ?? this.processRunner).run({
516
611
  command: command.command,
517
612
  args: command.args,
518
613
  cwd: worktree.worktreePath,
519
- env: providerProcessEnv(dispatch)
614
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
615
+ stdin: pipePromptToStdin ? providerPrompt : undefined
520
616
  }, {
521
- onStdoutLine: async (line) => {
522
- for (const event of outputState.consume(line)) {
523
- await context.emit(event);
524
- }
525
- }
617
+ onStarted: async () => {
618
+ await context.emit({
619
+ role: "runner",
620
+ kind: "checkpoint",
621
+ content: "Provider CLI QA is supervised and ready.",
622
+ eventType: "provider_cli_started"
623
+ });
624
+ },
625
+ onStdoutLine: async (line) => await toolHeartbeat.consume(line)
526
626
  }, context.signal);
527
627
  }
528
628
  finally {
629
+ toolHeartbeat.stop();
529
630
  await liveQaEvents.stop();
530
631
  await cleanupCompletionTools(completionTools, context.emit);
531
632
  }
@@ -547,7 +648,7 @@ export class ProcessBackedProviderCliService {
547
648
  ? "Provider CLI QA completed but did not produce the complete durable QA artifact bundle."
548
649
  : "Provider CLI QA completed but did not call the Forge QA completion tool.");
549
650
  }
550
- enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
651
+ await enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
551
652
  if (!liveQaEvents.hasEmitted()) {
552
653
  await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
553
654
  await emitQaStructuredEvents(context.emit, qaArtifact);
@@ -581,39 +682,114 @@ export class ProcessBackedProviderCliService {
581
682
  const { dispatch, worktree } = context;
582
683
  const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
583
684
  const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
685
+ let repairSpec = null;
686
+ let repairEnvironment = context.runtimeEnvironment;
687
+ let completionTools = null;
688
+ if (repairChecks) {
689
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
690
+ await fs.mkdir(metadataRoot, { recursive: true });
691
+ const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
692
+ if (!laneCommand) {
693
+ throw new Error(`CI repair readiness failed: lane command for '${dispatch.execution.lane}' was not resolved, so the approved spec cannot be materialized.`);
694
+ }
695
+ const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
696
+ const specPath = path.join(metadataRoot, "spec.md");
697
+ await fs.writeFile(specPath, specBody, "utf8");
698
+ await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
699
+ repairSpec = {
700
+ repoPath: worktree.worktreePath,
701
+ path: specPath,
702
+ body: specBody,
703
+ producedBy: "forge-lane-command",
704
+ status: "approved",
705
+ };
706
+ const readiness = await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
707
+ repairEnvironment = readiness.environment;
708
+ completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "ci_repair", runtimeEnvironment: repairEnvironment });
709
+ await context.emit({
710
+ role: "runner",
711
+ kind: "checkpoint",
712
+ 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.`,
713
+ eventType: "ci_repair_readiness_ready",
714
+ });
715
+ await context.emit({
716
+ role: "runner",
717
+ kind: "checkpoint",
718
+ content: "Forge CI repair completion tool configured; provider must call forge.complete_ci_repair before Bridge-owned commit and push.",
719
+ eventType: "completion_tools_configured",
720
+ });
721
+ }
722
+ const repairPrompt = repairChecks
723
+ ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
724
+ : addressComments
725
+ ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
726
+ : buildConflictResolvePrompt(dispatch, worktree.branchName);
727
+ const providerPrompt = buildProviderCliPrompt(dispatch, repairPrompt, repairEnvironment);
728
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
584
729
  const command = buildProviderCliCommand(dispatch, {
585
730
  worktreePath: worktree.worktreePath,
586
- prompt: repairChecks
587
- ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
588
- : addressComments
589
- ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
590
- : buildConflictResolvePrompt(dispatch, worktree.branchName)
731
+ prompt: repairPrompt,
732
+ promptFromStdin: pipePromptToStdin,
733
+ completionTools: completionTools ?? undefined,
734
+ runtimeEnvironment: repairEnvironment
591
735
  });
592
736
  await context.emit({
593
737
  role: "runner",
594
738
  kind: "checkpoint",
595
739
  content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
596
- eventType: "provider_cli_started"
740
+ eventType: "provider_cli_starting"
597
741
  });
598
742
  const outputState = createProviderOutputState(dispatch.execution.provider);
599
- const result = await this.processRunner.run({
600
- command: command.command,
601
- args: command.args,
602
- cwd: worktree.worktreePath,
603
- env: providerProcessEnv(dispatch)
604
- }, {
605
- onStdoutLine: async (line) => {
606
- for (const event of outputState.consume(line)) {
607
- await context.emit(event);
743
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
744
+ let result;
745
+ try {
746
+ result = await (context.processRunner ?? this.processRunner).run({
747
+ command: command.command,
748
+ args: command.args,
749
+ cwd: worktree.worktreePath,
750
+ env: providerProcessEnv(dispatch, repairEnvironment),
751
+ stdin: pipePromptToStdin ? providerPrompt : undefined
752
+ }, {
753
+ onStarted: async () => {
754
+ await context.emit({
755
+ role: "runner",
756
+ kind: "checkpoint",
757
+ content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} is supervised and ready.`,
758
+ eventType: "provider_cli_started"
759
+ });
760
+ },
761
+ onStdoutLine: async (line) => {
762
+ await toolHeartbeat.consume(line);
608
763
  }
764
+ }, context.signal);
765
+ }
766
+ finally {
767
+ toolHeartbeat.stop();
768
+ if (completionTools) {
769
+ await cleanupCompletionTools(completionTools, context.emit);
609
770
  }
610
- }, context.signal);
771
+ }
611
772
  if (result.exitCode !== 0) {
612
773
  const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
613
774
  throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
614
775
  }
776
+ let repairArtifact = null;
777
+ if (repairChecks) {
778
+ repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
779
+ if (!repairArtifact) {
780
+ throw new Error("Provider CLI completed but did not call forge.complete_ci_repair. Forge did not commit or push the repair.");
781
+ }
782
+ if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
783
+ throw new Error(`CI repair completion gate recorded ${repairArtifact.status}; Forge did not commit or push the repair.`);
784
+ }
785
+ await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
786
+ await emitPhaseCompletionEvent(context.emit, repairArtifact);
787
+ await emitPhaseGateEvents(context.emit, repairArtifact);
788
+ }
615
789
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
616
- await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
790
+ if (providerOwnsBranchPublication(context)) {
791
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
792
+ }
617
793
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
618
794
  await context.emit({
619
795
  role: "assistant",
@@ -630,7 +806,10 @@ export class ProcessBackedProviderCliService {
630
806
  ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
631
807
  : addressComments
632
808
  ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
633
- : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
809
+ : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`,
810
+ spec: repairSpec,
811
+ artifact: repairArtifact,
812
+ artifacts: repairArtifact ? [repairArtifact] : [],
634
813
  };
635
814
  }
636
815
  }
@@ -696,8 +875,12 @@ function clipFileName(fileName, maxLength) {
696
875
  return `${stem.slice(0, maxStemLength)}${extension}`;
697
876
  }
698
877
  const providerPromptStdinThreshold = 8_000;
699
- function shouldPipePromptToStdin(provider, prompt) {
700
- return provider.trim().toLowerCase() === "codex" && Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold;
878
+ function shouldPipePromptToStdin(provider, prompt, force = false) {
879
+ return provider.trim().toLowerCase() === "codex"
880
+ && (force || Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold);
881
+ }
882
+ function usesManagedWindowsHost(dispatch) {
883
+ return process.platform === "win32" && dispatch.sandbox?.executionMode === "host";
701
884
  }
702
885
  function isFakeWorkerModel(model) {
703
886
  return model.trim().toLowerCase() === fakeWorkerModel;
@@ -1,14 +1,19 @@
1
1
  import type { RunOnceDispatchPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec } from "../session.types.js";
2
2
  import type { WorktreeHandle } from "../../worktree/worktree.types.js";
3
+ import type { ProviderProcessRunner } from "./provider-cli.process.js";
3
4
  export interface ProviderCliRunContext {
4
5
  readonly dispatch: RunOnceDispatchPacket;
5
6
  readonly worktree: WorktreeHandle;
7
+ readonly runtimeEnvironment?: Readonly<Record<string, string>>;
8
+ readonly processRunner?: ProviderProcessRunner;
6
9
  readonly signal?: AbortSignal;
7
10
  emit(event: RunnerDispatchEvent): Promise<void>;
8
11
  }
9
12
  export interface ProviderCliDraftContext {
10
13
  readonly dispatch: RunOnceDispatchPacket;
11
14
  readonly worktree: WorktreeHandle;
15
+ readonly runtimeEnvironment?: Readonly<Record<string, string>>;
16
+ readonly processRunner?: ProviderProcessRunner;
12
17
  readonly signal?: AbortSignal;
13
18
  emit(event: RunnerDispatchEvent): Promise<void>;
14
19
  }
@@ -5,6 +5,8 @@ export declare function clip(value: string, maxLength: number): string;
5
5
  export interface ProviderOutputState {
6
6
  readonly sessionLabel: string | null;
7
7
  finalMessage: string | null;
8
+ /** True only while the provider has reported a tool/command start without a matching completion. */
9
+ readonly hasActiveToolCall: boolean;
8
10
  /** Accumulated provider-reported token usage for this run, or null when none was reported. */
9
11
  readonly usage: ProviderUsage | null;
10
12
  consume(line: string): readonly RunnerDispatchEvent[];