@loucompanion/forge-bridge 0.1.3-dev.d1ca67076aa9 → 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 (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 +962 -22
  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 +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/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 +350 -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 +19 -5
  82. package/dist/session/phase-completion.runtime.d.ts +12 -1
  83. package/dist/session/phase-completion.runtime.js +127 -7
  84. package/dist/session/phase-completion.schemas.js +108 -3
  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 +16 -2
  97. package/dist/session/provider-cli/provider-cli.prompts.js +202 -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 +7 -3
  102. package/dist/session/provider-cli/provider-cli.service.js +399 -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 +107 -1
  112. package/dist/session/session.types.js +226 -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,107 @@
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
+ 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
+ }
17
105
  const fakeWorkerModel = "server-local-fake-worker";
18
106
  export class RoutingProviderCliService {
19
107
  realProvider;
@@ -32,6 +120,11 @@ export class RoutingProviderCliService {
32
120
  ? await this.deterministicProvider.runArchitect(context)
33
121
  : await this.realProvider.runArchitect(context);
34
122
  }
123
+ async runCiRepair(context) {
124
+ return isFakeWorkerModel(context.dispatch.execution.model)
125
+ ? await this.deterministicProvider.runCiRepair(context)
126
+ : await this.realProvider.runCiRepair(context);
127
+ }
35
128
  async runConflictResolve(context) {
36
129
  return isFakeWorkerModel(context.dispatch.execution.model)
37
130
  ? await this.deterministicProvider.runConflictResolve(context)
@@ -51,34 +144,36 @@ export class RoutingProviderCliService {
51
144
  export class ProcessBackedProviderCliService {
52
145
  processRunner;
53
146
  git;
54
- constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient()) {
147
+ providerToolHeartbeatIntervalMs;
148
+ constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs) {
55
149
  this.processRunner = processRunner;
56
150
  this.git = git;
151
+ this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
57
152
  }
58
153
  async runDraft(context) {
59
154
  const { dispatch } = context;
60
155
  if (!dispatch.draft) {
61
156
  throw new Error("Draft dispatch context is missing.");
62
157
  }
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);
158
+ const { worktree } = context;
159
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
72
160
  await fs.mkdir(metadataRoot, { recursive: true });
73
- await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(dispatch.draft, null, 2) + "\n", "utf8");
161
+ await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(durableDraftInput(dispatch), null, 2) + "\n", "utf8");
74
162
  await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
163
+ const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
75
164
  const decomposeDraft = isDecomposeDraftDispatch(dispatch);
76
- const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree: draftWorktree, phase: decomposeDraft ? "decompose" : "draft" });
165
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
166
+ const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
167
+ const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
168
+ const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
77
169
  const command = buildProviderCliCommand(dispatch, {
78
- worktreePath: tempDir,
79
- prompt: buildDraftPrompt(dispatch),
170
+ worktreePath: worktree.worktreePath,
171
+ prompt: draftPrompt,
172
+ promptFromStdin: pipePromptToStdin,
80
173
  completionTools: completionTools ?? undefined,
81
- skipGitRepoCheck: true
174
+ skipGitRepoCheck: false,
175
+ imagePaths: stagedAttachments.imagePaths,
176
+ runtimeEnvironment: context.runtimeEnvironment
82
177
  });
83
178
  const draftArtifactSlugs = dispatch.artifacts
84
179
  .filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
@@ -92,8 +187,8 @@ export class ProcessBackedProviderCliService {
92
187
  await context.emit({
93
188
  role: "runner",
94
189
  kind: "checkpoint",
95
- content: `Starting provider CLI Draft: ${formatProviderCliCommand(command)}`,
96
- eventType: "provider_cli_started"
190
+ content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
191
+ eventType: "provider_cli_starting"
97
192
  });
98
193
  await context.emit({
99
194
  role: "runner",
@@ -104,6 +199,7 @@ export class ProcessBackedProviderCliService {
104
199
  eventType: "completion_tools_configured"
105
200
  });
106
201
  const outputState = createProviderOutputState(dispatch.execution.provider);
202
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
107
203
  try {
108
204
  const providerCancellation = new AbortController();
109
205
  const abortProvider = () => providerCancellation.abort();
@@ -114,16 +210,23 @@ export class ProcessBackedProviderCliService {
114
210
  context.signal?.addEventListener("abort", abortProvider, { once: true });
115
211
  }
116
212
  try {
117
- const runPromise = this.processRunner.run({
213
+ const runPromise = (context.processRunner ?? this.processRunner).run({
118
214
  command: command.command,
119
215
  args: command.args,
120
- cwd: tempDir,
121
- env: providerProcessEnv(dispatch)
216
+ cwd: worktree.worktreePath,
217
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
218
+ stdin: pipePromptToStdin ? providerPrompt : undefined
122
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
+ },
123
228
  onStdoutLine: async (line) => {
124
- for (const event of outputState.consume(line)) {
125
- await context.emit(event);
126
- }
229
+ await toolHeartbeat.consume(line);
127
230
  }
128
231
  }, providerCancellation.signal);
129
232
  const artifactWaitCancellation = new AbortController();
@@ -134,7 +237,7 @@ export class ProcessBackedProviderCliService {
134
237
  else {
135
238
  context.signal?.addEventListener("abort", abortArtifactWait, { once: true });
136
239
  }
137
- const artifactWaitPromise = waitForCompletionArtifact(tempDir, dispatch.workItem.id, decomposeDraft ? "decompose" : "draft", artifactWaitCancellation.signal);
240
+ const artifactWaitPromise = waitForCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, decomposeDraft ? "decompose" : "draft", artifactWaitCancellation.signal);
138
241
  let result;
139
242
  let earlyProposalArtifact = null;
140
243
  try {
@@ -168,8 +271,8 @@ export class ProcessBackedProviderCliService {
168
271
  });
169
272
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
170
273
  if (decomposeDraft) {
171
- await emitDecomposeGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
172
- const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "decompose");
274
+ await emitDecomposeGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
275
+ const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "decompose");
173
276
  if (!proposalArtifact) {
174
277
  throw new Error("Provider CLI Decompose completed but did not call forge.complete_decompose_proposal.");
175
278
  }
@@ -191,8 +294,8 @@ export class ProcessBackedProviderCliService {
191
294
  artifacts: [proposalArtifact]
192
295
  };
193
296
  }
194
- await emitDraftGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
195
- const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(tempDir, dispatch.workItem.id, "draft");
297
+ await emitDraftGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
298
+ const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "draft");
196
299
  if (!proposalArtifact) {
197
300
  throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
198
301
  }
@@ -219,8 +322,8 @@ export class ProcessBackedProviderCliService {
219
322
  }
220
323
  }
221
324
  finally {
325
+ toolHeartbeat.stop();
222
326
  await cleanupCompletionTools(completionTools, context.emit);
223
- await fs.rm(tempDir, { recursive: true, force: true });
224
327
  }
225
328
  }
226
329
  async runImplementation(context) {
@@ -238,21 +341,26 @@ export class ProcessBackedProviderCliService {
238
341
  const specPath = path.join(metadataRoot, "spec.md");
239
342
  await fs.writeFile(specPath, specBody, "utf8");
240
343
  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 });
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));
242
352
  const command = buildProviderCliCommand(dispatch, {
243
353
  worktreePath: worktree.worktreePath,
244
- prompt: debugCycle
245
- ? buildDebugCyclePrompt(dispatch, phase)
246
- : developCycle
247
- ? buildDevelopCyclePrompt(dispatch, phase)
248
- : buildImplementationPrompt(dispatch, phase),
249
- completionTools
354
+ prompt: implementationPrompt,
355
+ promptFromStdin: pipePromptToStdin,
356
+ completionTools,
357
+ runtimeEnvironment: context.runtimeEnvironment
250
358
  });
251
359
  await context.emit({
252
360
  role: "runner",
253
361
  kind: "checkpoint",
254
- content: `Starting provider CLI: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
255
- eventType: "provider_cli_started"
362
+ content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
363
+ eventType: "provider_cli_starting"
256
364
  });
257
365
  await context.emit({
258
366
  role: "runner",
@@ -265,24 +373,33 @@ export class ProcessBackedProviderCliService {
265
373
  eventType: "completion_tools_configured"
266
374
  });
267
375
  const outputState = createProviderOutputState(dispatch.execution.provider);
376
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
268
377
  const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
269
378
  let result;
270
379
  try {
271
380
  liveCompletionEvents.start();
272
- result = await this.processRunner.run({
381
+ result = await (context.processRunner ?? this.processRunner).run({
273
382
  command: command.command,
274
383
  args: command.args,
275
384
  cwd: worktree.worktreePath,
276
- env: providerProcessEnv(dispatch)
385
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
386
+ stdin: pipePromptToStdin ? providerPrompt : undefined
277
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
+ },
278
396
  onStdoutLine: async (line) => {
279
- for (const event of outputState.consume(line)) {
280
- await context.emit(event);
281
- }
397
+ await toolHeartbeat.consume(line);
282
398
  }
283
399
  }, context.signal);
284
400
  }
285
401
  finally {
402
+ toolHeartbeat.stop();
286
403
  await liveCompletionEvents.stop();
287
404
  await cleanupCompletionTools(completionTools, context.emit);
288
405
  }
@@ -298,8 +415,8 @@ export class ProcessBackedProviderCliService {
298
415
  });
299
416
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
300
417
  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}`]);
418
+ if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
419
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
303
420
  await context.emit({
304
421
  role: "runner",
305
422
  kind: "checkpoint",
@@ -357,18 +474,23 @@ export class ProcessBackedProviderCliService {
357
474
  const specPath = path.join(metadataRoot, "spec.md");
358
475
  await fs.writeFile(specPath, specBody, "utf8");
359
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");
360
- 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));
361
481
  const command = buildProviderCliCommand(dispatch, {
362
482
  worktreePath: worktree.worktreePath,
363
- prompt: buildArchitectPrompt(dispatch),
364
- completionTools
483
+ prompt: architectPrompt,
484
+ promptFromStdin: pipePromptToStdin,
485
+ completionTools,
486
+ runtimeEnvironment: context.runtimeEnvironment
365
487
  });
366
- const commandText = formatProviderCliCommand(command);
488
+ const commandText = formatProviderCliCommandForCheckpoint(command);
367
489
  await context.emit({
368
490
  role: "runner",
369
491
  kind: "checkpoint",
370
492
  content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
371
- eventType: "provider_cli_started"
493
+ eventType: "provider_cli_starting"
372
494
  });
373
495
  await context.emit({
374
496
  role: "runner",
@@ -377,24 +499,31 @@ export class ProcessBackedProviderCliService {
377
499
  eventType: "completion_tools_configured"
378
500
  });
379
501
  const outputState = createProviderOutputState(dispatch.execution.provider);
502
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
380
503
  const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
381
504
  let result;
382
505
  try {
383
506
  liveArchitectEvents.start();
384
- result = await this.processRunner.run({
507
+ result = await (context.processRunner ?? this.processRunner).run({
385
508
  command: command.command,
386
509
  args: command.args,
387
510
  cwd: worktree.worktreePath,
388
- env: providerProcessEnv(dispatch)
511
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
512
+ stdin: pipePromptToStdin ? providerPrompt : undefined
389
513
  }, {
390
- onStdoutLine: async (line) => {
391
- for (const event of outputState.consume(line)) {
392
- await context.emit(event);
393
- }
394
- }
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)
395
523
  }, context.signal);
396
524
  }
397
525
  finally {
526
+ toolHeartbeat.stop();
398
527
  await liveArchitectEvents.stop();
399
528
  await cleanupCompletionTools(completionTools, context.emit);
400
529
  }
@@ -448,18 +577,23 @@ export class ProcessBackedProviderCliService {
448
577
  const specPath = path.join(metadataRoot, "spec.md");
449
578
  await fs.writeFile(specPath, specBody, "utf8");
450
579
  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" });
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));
452
584
  const command = buildProviderCliCommand(dispatch, {
453
585
  worktreePath: worktree.worktreePath,
454
- prompt: buildQaPrompt(dispatch, specBody),
455
- completionTools
586
+ prompt: qaPrompt,
587
+ promptFromStdin: pipePromptToStdin,
588
+ completionTools,
589
+ runtimeEnvironment: context.runtimeEnvironment
456
590
  });
457
- const commandText = formatProviderCliCommand(command);
591
+ const commandText = formatProviderCliCommandForCheckpoint(command);
458
592
  await context.emit({
459
593
  role: "runner",
460
594
  kind: "checkpoint",
461
595
  content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
462
- eventType: "provider_cli_started"
596
+ eventType: "provider_cli_starting"
463
597
  });
464
598
  await context.emit({
465
599
  role: "runner",
@@ -468,24 +602,31 @@ export class ProcessBackedProviderCliService {
468
602
  eventType: "completion_tools_configured"
469
603
  });
470
604
  const outputState = createProviderOutputState(dispatch.execution.provider);
605
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
471
606
  const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
472
607
  let result;
473
608
  try {
474
609
  liveQaEvents.start();
475
- result = await this.processRunner.run({
610
+ result = await (context.processRunner ?? this.processRunner).run({
476
611
  command: command.command,
477
612
  args: command.args,
478
613
  cwd: worktree.worktreePath,
479
- env: providerProcessEnv(dispatch)
614
+ env: providerProcessEnv(dispatch, context.runtimeEnvironment),
615
+ stdin: pipePromptToStdin ? providerPrompt : undefined
480
616
  }, {
481
- onStdoutLine: async (line) => {
482
- for (const event of outputState.consume(line)) {
483
- await context.emit(event);
484
- }
485
- }
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)
486
626
  }, context.signal);
487
627
  }
488
628
  finally {
629
+ toolHeartbeat.stop();
489
630
  await liveQaEvents.stop();
490
631
  await cleanupCompletionTools(completionTools, context.emit);
491
632
  }
@@ -507,7 +648,7 @@ export class ProcessBackedProviderCliService {
507
648
  ? "Provider CLI QA completed but did not produce the complete durable QA artifact bundle."
508
649
  : "Provider CLI QA completed but did not call the Forge QA completion tool.");
509
650
  }
510
- enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
651
+ await enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
511
652
  if (!liveQaEvents.hasEmitted()) {
512
653
  await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
513
654
  await emitQaStructuredEvents(context.emit, qaArtifact);
@@ -531,56 +672,216 @@ export class ProcessBackedProviderCliService {
531
672
  artifacts: [qaArtifact]
532
673
  };
533
674
  }
675
+ async runCiRepair(context) {
676
+ return await this.runPullRequestRepair(context, "ci_repair");
677
+ }
534
678
  async runConflictResolve(context) {
679
+ return await this.runPullRequestRepair(context, "conflict_resolve");
680
+ }
681
+ async runPullRequestRepair(context, route) {
535
682
  const { dispatch, worktree } = context;
536
- const addressComments = isPullRequestCommentsDispatch(dispatch);
683
+ const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
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));
537
729
  const command = buildProviderCliCommand(dispatch, {
538
730
  worktreePath: worktree.worktreePath,
539
- prompt: addressComments
540
- ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
541
- : buildConflictResolvePrompt(dispatch, worktree.branchName)
731
+ prompt: repairPrompt,
732
+ promptFromStdin: pipePromptToStdin,
733
+ completionTools: completionTools ?? undefined,
734
+ runtimeEnvironment: repairEnvironment
542
735
  });
543
736
  await context.emit({
544
737
  role: "runner",
545
738
  kind: "checkpoint",
546
- content: `Starting provider CLI ${addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
547
- eventType: "provider_cli_started"
739
+ content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
740
+ eventType: "provider_cli_starting"
548
741
  });
549
742
  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);
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);
559
763
  }
764
+ }, context.signal);
765
+ }
766
+ finally {
767
+ toolHeartbeat.stop();
768
+ if (completionTools) {
769
+ await cleanupCompletionTools(completionTools, context.emit);
560
770
  }
561
- }, context.signal);
771
+ }
562
772
  if (result.exitCode !== 0) {
563
773
  const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
564
774
  throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
565
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
+ }
566
789
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
567
- await this.git.run(worktree.worktreePath, ["push", "origin", `HEAD:${worktree.branchName}`]);
790
+ if (providerOwnsBranchPublication(context)) {
791
+ await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
792
+ }
568
793
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
569
794
  await context.emit({
570
795
  role: "assistant",
571
796
  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"
797
+ content: outputState.finalMessage || (repairChecks
798
+ ? `${dispatch.execution.provider} CLI repaired pull request checks for ${worktree.branchName}.`
799
+ : addressComments
800
+ ? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
801
+ : `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
802
+ eventType: repairChecks ? "pr_checks_complete" : addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
576
803
  });
577
804
  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)}).`
805
+ summary: repairChecks
806
+ ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
807
+ : addressComments
808
+ ? `${dispatch.execution.provider} CLI addressed PR comments 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] : [],
581
813
  };
582
814
  }
583
815
  }
816
+ async function stageDraftAttachments(dispatch, metadataRoot) {
817
+ const attachments = dispatch.draft?.attachments ?? [];
818
+ const stageable = attachments.filter((attachment) => (attachment.kind === "text" && typeof attachment.textContent === "string")
819
+ || (attachment.kind === "image" && typeof attachment.contentBase64 === "string"));
820
+ if (stageable.length === 0) {
821
+ return { textReferences: [], imagePaths: [] };
822
+ }
823
+ const attachmentRoot = path.join(metadataRoot, "draft-attachments");
824
+ const worktreeRoot = path.resolve(metadataRoot, "../../..");
825
+ await fs.mkdir(attachmentRoot, { recursive: true });
826
+ const textReferences = [];
827
+ const imagePaths = [];
828
+ for (const attachment of stageable) {
829
+ const fileName = `${String(attachment.index + 1).padStart(2, "0")}-${safeAttachmentFileName(attachment.fileName)}`;
830
+ const absolutePath = path.join(attachmentRoot, fileName);
831
+ if (attachment.kind === "text") {
832
+ await fs.writeFile(absolutePath, attachment.textContent ?? "", { encoding: "utf8", mode: 0o600 });
833
+ textReferences.push({ index: attachment.index, path: path.relative(worktreeRoot, absolutePath).replaceAll(path.sep, "/") });
834
+ continue;
835
+ }
836
+ const imageContent = decodeDraftImageAttachment(attachment.contentBase64 ?? "", attachment.fileName, attachment.index);
837
+ await fs.writeFile(absolutePath, imageContent, { mode: 0o600 });
838
+ imagePaths.push(absolutePath);
839
+ }
840
+ return { textReferences, imagePaths };
841
+ }
842
+ function decodeDraftImageAttachment(contentBase64, fileName, index) {
843
+ const normalized = contentBase64.replace(/\s+/gu, "");
844
+ if (normalized.length === 0 || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(normalized)) {
845
+ throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
846
+ }
847
+ const decoded = Buffer.from(normalized, "base64");
848
+ if (decoded.length === 0 || decoded.toString("base64") !== normalized) {
849
+ throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
850
+ }
851
+ return decoded;
852
+ }
853
+ function safeAttachmentFileName(fileName) {
854
+ const sanitized = path.basename(fileName || "attachment.txt")
855
+ .replace(/[^a-zA-Z0-9._-]/gu, "_")
856
+ .replace(/[. ]+$/gu, "") || "attachment.txt";
857
+ const maxFileNameLength = 120;
858
+ const clipped = clipFileName(sanitized, maxFileNameLength);
859
+ const extension = path.extname(clipped);
860
+ const stem = path.basename(clipped, extension).replace(/[. ]+$/gu, "") || "attachment";
861
+ const windowsReservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
862
+ const safeStem = stem === "." || stem === ".." || windowsReservedName.test(stem) ? `attachment-${stem.replaceAll(".", "_")}` : stem;
863
+ return clipFileName(`${safeStem}${extension}`, maxFileNameLength);
864
+ }
865
+ function clipFileName(fileName, maxLength) {
866
+ if (fileName.length <= maxLength) {
867
+ return fileName;
868
+ }
869
+ const originalExtension = path.extname(fileName);
870
+ const extension = originalExtension.length > maxLength - 1
871
+ ? originalExtension.slice(0, maxLength - 1)
872
+ : originalExtension;
873
+ const stem = path.basename(fileName, originalExtension);
874
+ const maxStemLength = Math.max(1, maxLength - extension.length);
875
+ return `${stem.slice(0, maxStemLength)}${extension}`;
876
+ }
877
+ const providerPromptStdinThreshold = 8_000;
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";
884
+ }
584
885
  function isFakeWorkerModel(model) {
585
886
  return model.trim().toLowerCase() === fakeWorkerModel;
586
887
  }