@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.9445952e33ba

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 (162) hide show
  1. package/README.md +88 -2
  2. package/dist/bridge/bridge-chat.types.d.ts +111 -0
  3. package/dist/bridge/bridge-chat.types.js +401 -0
  4. package/dist/bridge/bridge.base.d.ts +4 -1
  5. package/dist/bridge/bridge.service.d.ts +72 -3
  6. package/dist/bridge/bridge.service.js +1563 -152
  7. package/dist/bridge/bridge.types.d.ts +46 -1
  8. package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
  9. package/dist/bridge/dispatch-result-outbox.js +62 -0
  10. package/dist/bridge/dispatch-result-worker.d.ts +2 -0
  11. package/dist/bridge/dispatch-result-worker.js +122 -0
  12. package/dist/bridge/internals.d.ts +2 -0
  13. package/dist/bridge/internals.js +1 -0
  14. package/dist/bridge/readiness-result-outbox.d.ts +21 -0
  15. package/dist/bridge/readiness-result-outbox.js +54 -0
  16. package/dist/bridge/readiness-result-worker.d.ts +2 -0
  17. package/dist/bridge/readiness-result-worker.js +121 -0
  18. package/dist/bridge/runner-host-capabilities.d.ts +15 -0
  19. package/dist/bridge/runner-host-capabilities.js +93 -0
  20. package/dist/cleanup/cleanup.service.d.ts +1 -0
  21. package/dist/cleanup/cleanup.service.js +70 -9
  22. package/dist/cli/cli.help.js +2 -2
  23. package/dist/cli/cli.service.d.ts +14 -6
  24. package/dist/cli/cli.service.js +75 -16
  25. package/dist/cli/local-config.d.ts +1 -0
  26. package/dist/cli/local-config.js +4 -0
  27. package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
  28. package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
  29. package/dist/runtime/attempt-database-broker.d.ts +89 -0
  30. package/dist/runtime/attempt-database-broker.js +390 -0
  31. package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
  32. package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
  33. package/dist/runtime/attempt-path-operation.d.ts +21 -0
  34. package/dist/runtime/attempt-path-operation.js +94 -0
  35. package/dist/runtime/attempt-private-directory.d.ts +6 -0
  36. package/dist/runtime/attempt-private-directory.js +54 -0
  37. package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
  38. package/dist/runtime/attempt-process-supervisor.js +67 -20
  39. package/dist/runtime/canonical-source-file.d.ts +3 -0
  40. package/dist/runtime/canonical-source-file.js +54 -0
  41. package/dist/runtime/docker-engine-probe.d.ts +90 -0
  42. package/dist/runtime/docker-engine-probe.js +333 -0
  43. package/dist/runtime/generated-output-reset.d.ts +10 -0
  44. package/dist/runtime/generated-output-reset.js +62 -8
  45. package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
  46. package/dist/runtime/host-database-capability-boundary.js +62 -0
  47. package/dist/runtime/host-diagnostics.d.ts +11 -0
  48. package/dist/runtime/host-diagnostics.js +191 -0
  49. package/dist/runtime/host-execution-root.d.ts +4 -0
  50. package/dist/runtime/host-execution-root.js +39 -0
  51. package/dist/runtime/host-listener-inspector.d.ts +8 -1
  52. package/dist/runtime/host-listener-inspector.js +121 -60
  53. package/dist/runtime/host-runtime-contract.d.ts +12 -1
  54. package/dist/runtime/host-runtime-contract.js +168 -55
  55. package/dist/runtime/host-runtime-lease.d.ts +92 -8
  56. package/dist/runtime/host-runtime-lease.js +1141 -114
  57. package/dist/runtime/host-runtime-promotion.d.ts +12 -3
  58. package/dist/runtime/host-runtime-promotion.js +59 -12
  59. package/dist/runtime/memory-capacity.d.ts +70 -0
  60. package/dist/runtime/memory-capacity.js +123 -0
  61. package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
  62. package/dist/runtime/native-test-postgres-fallback.js +91 -0
  63. package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
  64. package/dist/runtime/node-host-runtime-adapter.js +28 -11
  65. package/dist/runtime/pinned-directory-process.d.ts +9 -0
  66. package/dist/runtime/pinned-directory-process.js +61 -0
  67. package/dist/runtime/pinned-source-directory.d.ts +17 -0
  68. package/dist/runtime/pinned-source-directory.js +94 -0
  69. package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
  70. package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
  71. package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
  72. package/dist/runtime/prepared-runtime-identity.js +218 -0
  73. package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
  74. package/dist/runtime/prepared-runtime-readiness.js +285 -0
  75. package/dist/runtime/runtime-isolation-lease.js +4 -1
  76. package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
  77. package/dist/runtime/workspace-resource-estimator.js +174 -0
  78. package/dist/sandbox/preflight.d.ts +31 -1
  79. package/dist/sandbox/preflight.js +237 -51
  80. package/dist/sandbox/repository-lock.d.ts +2 -1
  81. package/dist/sandbox/repository-lock.js +17 -8
  82. package/dist/sandbox/runtime.d.ts +6 -1
  83. package/dist/sandbox/runtime.js +132 -103
  84. package/dist/sandbox/sandbox.types.d.ts +7 -0
  85. package/dist/sandbox/workspace.service.js +2 -0
  86. package/dist/session/managed-attempt-tools.d.ts +17 -1
  87. package/dist/session/managed-attempt-tools.js +485 -31
  88. package/dist/session/managed-build-coordinator.d.ts +5 -0
  89. package/dist/session/managed-build-coordinator.js +105 -0
  90. package/dist/session/managed-build-receipt.d.ts +21 -0
  91. package/dist/session/managed-build-receipt.js +337 -0
  92. package/dist/session/managed-verification-timeouts.d.ts +8 -0
  93. package/dist/session/managed-verification-timeouts.js +17 -0
  94. package/dist/session/phase-completion.artifacts.d.ts +17 -0
  95. package/dist/session/phase-completion.artifacts.js +224 -22
  96. package/dist/session/phase-completion.d.ts +11 -1
  97. package/dist/session/phase-completion.helpers.d.ts +5 -0
  98. package/dist/session/phase-completion.helpers.js +30 -7
  99. package/dist/session/phase-completion.js +14 -3
  100. package/dist/session/phase-completion.prompts.js +17 -18
  101. package/dist/session/phase-completion.runtime.d.ts +3 -1
  102. package/dist/session/phase-completion.runtime.js +126 -17
  103. package/dist/session/phase-completion.schemas.js +49 -2
  104. package/dist/session/phase-completion.validation.d.ts +1 -0
  105. package/dist/session/phase-completion.validation.js +111 -21
  106. package/dist/session/pre-publication-validation.d.ts +75 -0
  107. package/dist/session/pre-publication-validation.js +661 -0
  108. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
  109. package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
  110. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  111. package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
  112. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
  113. package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
  114. package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
  115. package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
  116. package/dist/session/provider-cli/provider-cli.process.js +422 -22
  117. package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
  118. package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
  119. package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
  120. package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
  121. package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
  122. package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
  123. package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
  124. package/dist/session/provider-cli/provider-cli.service.js +773 -69
  125. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  126. package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
  127. package/dist/session/provider-cli/provider-output-state.js +361 -17
  128. package/dist/session/provider-cli/provider-usage.d.ts +7 -0
  129. package/dist/session/provider-cli/provider-usage.js +28 -5
  130. package/dist/session/session-redaction.js +434 -9
  131. package/dist/session/session.service.js +145 -23
  132. package/dist/session/session.types.d.ts +147 -2
  133. package/dist/session/session.types.js +587 -7
  134. package/dist/session/unreferenced-exports.d.ts +12 -0
  135. package/dist/session/unreferenced-exports.js +59 -0
  136. package/dist/shared/format/format-bytes.d.ts +1 -0
  137. package/dist/shared/format/format-bytes.js +9 -0
  138. package/dist/shared/git/git.js +2 -1
  139. package/dist/shared/path/path.d.ts +1 -0
  140. package/dist/shared/path/path.js +18 -0
  141. package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
  142. package/dist/shared/process/dotnet-toolchain.js +307 -0
  143. package/dist/shared/process/host-process-supervisor.d.ts +2 -0
  144. package/dist/shared/process/host-process-supervisor.js +57 -9
  145. package/dist/shared/process/process.d.ts +2 -0
  146. package/dist/shared/process/process.js +12 -7
  147. package/dist/shared/provider-capabilities.d.ts +3 -0
  148. package/dist/shared/provider-capabilities.js +85 -0
  149. package/dist/shared/redaction/redaction.d.ts +1 -0
  150. package/dist/shared/redaction/redaction.js +39 -3
  151. package/dist/version.d.ts +1 -1
  152. package/dist/version.js +1 -1
  153. package/dist/worktree/dependency-warmer.d.ts +41 -9
  154. package/dist/worktree/dependency-warmer.js +497 -22
  155. package/dist/worktree/index.d.ts +1 -1
  156. package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
  157. package/dist/worktree/story-qa-reference-hydrator.js +253 -0
  158. package/dist/worktree/worktree.service.d.ts +7 -0
  159. package/dist/worktree/worktree.service.js +253 -21
  160. package/dist/worktree/worktree.types.d.ts +7 -1
  161. package/package.json +7 -5
  162. package/runtime/windows-job-supervisor.ps1 +380 -14
@@ -1,47 +1,186 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { RepositoryCoordinatorLock } from "../../sandbox/repository-lock.js";
3
4
  import { GitClient } from "../../shared/git/git.js";
4
5
  import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
6
+ import { defaultWorktreeCoordinatorStateRoot } from "../../worktree/worktree-paths.js";
5
7
  import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
6
- import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle, summarizeStderr, waitForCompletionArtifact } from "./provider-cli.artifacts.js";
8
+ import { ForgeFailureError } from "../session.types.js";
9
+ import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle, stagePublishableWorktreeChanges, waitForCompletionArtifact } from "./provider-cli.artifacts.js";
7
10
  import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
8
11
  import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
9
- import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
10
- import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
12
+ import { isTransientCodexProviderNotFoundFailure, recoveringProviderProcessRunner, StreamingProviderProcessRunner } from "./provider-cli.process.js";
13
+ import { emitProviderUsageEvent, prePublicationValidationEnv, providerProcessEnv } from "./provider-cli.runtime.js";
11
14
  import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
12
15
  import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
16
+ import { assertRepairHeadEpoch } from "./provider-cli.repair-epoch.js";
17
+ import { PrePublicationValidationError, PrePublicationValidator, } from "../pre-publication-validation.js";
18
+ const maxPrePublicationRepairTurns = 2;
19
+ const maxQaCompletionRecoveryTurns = 2;
20
+ function assertCommentRepairArtifactMatchesDispatch(dispatch, artifact) {
21
+ const pullRequest = dispatch.pullRequest;
22
+ if (!pullRequest)
23
+ throw new Error("Comment repair completion is missing pull-request context.");
24
+ const payload = JSON.parse(artifact.verdictJson);
25
+ if (payload.repair_head_sha !== pullRequest.headSha || payload.repair_issue_key !== pullRequest.reviewIssueKey) {
26
+ throw new Error("Comment repair completion head or issue key does not match the immutable dispatch snapshot.");
27
+ }
28
+ const expected = new Map(pullRequest.reviewFindings
29
+ .filter(finding => finding.actionable)
30
+ .map(finding => [`${finding.kind}:${finding.claimId}:${finding.bodySha256.toLowerCase()}`, finding]));
31
+ const dispositions = Array.isArray(payload.dispositions) ? payload.dispositions : [];
32
+ const actual = new Set();
33
+ for (const value of dispositions) {
34
+ const record = value;
35
+ const identity = `${String(record.kind ?? "")}:${String(record.claim_id ?? "")}:${String(record.body_sha256 ?? "").toLowerCase()}`;
36
+ if (actual.has(identity))
37
+ throw new Error("Comment repair completion contains a duplicate finding disposition.");
38
+ actual.add(identity);
39
+ const finding = expected.get(identity);
40
+ if (finding && !finding.contentAvailable && record.disposition !== "needs_decision") {
41
+ throw new Error("Comment repair completion cannot resolve or reject a finding whose content was unavailable.");
42
+ }
43
+ if (record.disposition === "owned_elsewhere") {
44
+ const owner = (record.owned_elsewhere ?? {});
45
+ if (!(pullRequest.openWorkOrderStories ?? []).some(story => story.workItemId.toLowerCase() === String(owner.target_work_item_id ?? "").toLowerCase())) {
46
+ throw new Error("Comment repair completion names an owned_elsewhere owner that is not an open Story in this Work Order.");
47
+ }
48
+ }
49
+ }
50
+ if (expected.size !== actual.size || [...expected.keys()].some(identity => !actual.has(identity))) {
51
+ throw new Error("Comment repair completion must disposition every actionable dispatch finding exactly once and no other finding.");
52
+ }
53
+ }
54
+ function assertCommentRepairArtifactMatchesChanges(artifact, actualChangedFiles) {
55
+ const payload = JSON.parse(artifact.verdictJson);
56
+ const dispositions = Array.isArray(payload.dispositions) ? payload.dispositions : [];
57
+ const hasFixedDisposition = dispositions.some(value => {
58
+ const record = value;
59
+ return record.disposition === "fixed";
60
+ });
61
+ const claimedChangedFiles = Array.isArray(payload.changed_files)
62
+ ? payload.changed_files.map(value => {
63
+ const record = value;
64
+ return String(record.path ?? "").trim().replaceAll("\\", "/");
65
+ })
66
+ : [];
67
+ const normalizedActual = actualChangedFiles.map(value => value.trim().replaceAll("\\", "/"));
68
+ const claimed = [...new Set(claimedChangedFiles)].sort();
69
+ const actual = [...new Set(normalizedActual)].sort();
70
+ if (hasFixedDisposition && actual.length === 0) {
71
+ throw new Error("Comment repair fixed dispositions require an actual publishable source diff.");
72
+ }
73
+ if (claimed.length !== actual.length || claimed.some((value, index) => value !== actual[index])) {
74
+ throw new Error("Comment repair changed_files must exactly match the publishable staged source diff.");
75
+ }
76
+ }
77
+ async function finalizeCommentRepairChangedFiles(artifact, baselineChangedFiles, actualChangedFiles) {
78
+ const payload = JSON.parse(artifact.verdictJson);
79
+ const claimedRecords = Array.isArray(payload.changed_files)
80
+ ? payload.changed_files.map(value => value)
81
+ : [];
82
+ const claimedByPath = new Map(claimedRecords.map(record => [
83
+ String(record.path ?? "").trim().replaceAll("\\", "/"),
84
+ record,
85
+ ]));
86
+ const authoritativePaths = [...new Set(actualChangedFiles
87
+ .map(value => value.trim().replaceAll("\\", "/"))
88
+ .filter(Boolean))].sort();
89
+ const authoritative = new Set(authoritativePaths);
90
+ const dispositions = Array.isArray(payload.dispositions) ? payload.dispositions : [];
91
+ const hasFixedDisposition = dispositions.some(value => value.disposition === "fixed");
92
+ if (hasFixedDisposition && authoritativePaths.length === 0) {
93
+ throw new Error("Comment repair fixed dispositions require an actual publishable source diff.");
94
+ }
95
+ for (const baselinePath of baselineChangedFiles.map(value => value.trim().replaceAll("\\", "/"))) {
96
+ if (baselinePath && !authoritative.has(baselinePath)) {
97
+ throw new Error(`Comment repair pre-publication repair removed '${baselinePath}' from the publishable staged source diff.`);
98
+ }
99
+ }
100
+ payload.changed_files = authoritativePaths.map(filePath => {
101
+ const claimed = claimedByPath.get(filePath);
102
+ return claimed
103
+ ? { ...claimed, path: filePath }
104
+ : {
105
+ path: filePath,
106
+ summary: "Updated during bounded Forge pre-publication repair.",
107
+ };
108
+ });
109
+ const verdictJson = `${JSON.stringify(payload, null, 2)}\n`;
110
+ const finalized = { ...artifact, verdictJson };
111
+ assertCommentRepairArtifactMatchesChanges(finalized, authoritativePaths);
112
+ await fs.writeFile(artifact.path, verdictJson, "utf8");
113
+ return finalized;
114
+ }
115
+ export { parseClaudeMcpToolName } from "./provider-output-state.js";
13
116
  export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
14
117
  export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
15
- export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli.process.js";
118
+ export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolveClaudeNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, PROVIDER_OUTPUT_CAPTURE_LIMIT_CHARS, CodexModelsCacheRecoveringProcessRunner, CodexResumeRecoveringProcessRunner, isTransientCodexProviderNotFoundFailure, StreamingProviderProcessRunner } from "./provider-cli.process.js";
16
119
  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;
120
+ async function pushWorktreeBranch(git, worktreePath, branchName, repoPath, coordinator, signal, emit, expectedRemoteHeadSha) {
121
+ return await coordinator.withLock(repoPath, async () => {
122
+ if (expectedRemoteHeadSha) {
123
+ const expected = expectedRemoteHeadSha.trim().toLowerCase();
124
+ const remoteListing = await git.run(worktreePath, ["ls-remote", "--heads", "origin", `refs/heads/${branchName}`]);
125
+ const observed = remoteListing.trim().split(/\s+/u)[0]?.toLowerCase() ?? "";
126
+ if (observed !== expected) {
127
+ throw new Error(`stale_head: repair publication expected remote ${branchName} at ${expected}, but observed ${observed || "no branch"}; refresh provider state before retrying.`);
128
+ }
129
+ try {
130
+ await git.run(worktreePath, [
131
+ "push",
132
+ "--porcelain",
133
+ "origin",
134
+ `HEAD:refs/heads/${branchName}`,
135
+ `--force-with-lease=refs/heads/${branchName}:${expected}`,
136
+ ]);
137
+ }
138
+ catch (error) {
139
+ throw new Error(`stale_head: repair publication for ${branchName} lost its expected-head lease; Forge did not rebase or publish the stale repair: ${errorMessage(error)}`, { cause: error });
140
+ }
141
+ return (await git.run(worktreePath, ["rev-parse", "HEAD"])).trim();
25
142
  }
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
143
  try {
33
- await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
34
- await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
35
144
  await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
145
+ return (await git.run(worktreePath, ["rev-parse", "HEAD"])).trim();
36
146
  }
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 });
147
+ catch (err) {
148
+ if (!isNonFastForwardPushError(err)) {
149
+ throw err;
150
+ }
151
+ await emit?.({
152
+ role: "runner",
153
+ kind: "checkpoint",
154
+ content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
155
+ eventType: "worktree_branch_push_rebase"
156
+ });
157
+ try {
158
+ await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
159
+ await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
160
+ await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
161
+ }
162
+ catch (retryErr) {
163
+ 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 });
164
+ }
39
165
  }
40
- }
166
+ return (await git.run(worktreePath, ["rev-parse", "HEAD"])).trim();
167
+ }, signal);
41
168
  }
42
169
  function providerOwnsBranchPublication(context) {
43
170
  return context.dispatch.sandbox?.executionMode !== "docker";
44
171
  }
172
+ function prePublicationExecution(context, environment, defaultRunner, baselineRevision) {
173
+ if (context.processRunner)
174
+ return { baselineRevision, environment, processRunner: context.processRunner };
175
+ if (context.dispatch.sandbox?.executionMode !== "docker")
176
+ return { baselineRevision, environment };
177
+ const expectedSandboxId = context.dispatch.sandbox.dispatchAttemptId;
178
+ const observedSandboxId = context.runtimeEnvironment?.FORGE_SANDBOX_ID ?? process.env.FORGE_SANDBOX_ID;
179
+ if (observedSandboxId !== expectedSandboxId) {
180
+ throw new ForgeFailureError("prepublication_execution_scope_invalid", `prepublication_execution_scope_invalid: Docker attempt ${expectedSandboxId} has no runner proven to execute inside that sandbox.`, "bridge");
181
+ }
182
+ return { baselineRevision, environment, processRunner: defaultRunner };
183
+ }
45
184
  function remoteTrackingRefspec(branchName) {
46
185
  return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
47
186
  }
@@ -56,14 +195,111 @@ function isNonFastForwardPushError(err) {
56
195
  function errorMessage(err) {
57
196
  return err instanceof Error ? err.message : String(err);
58
197
  }
198
+ function providerCliExitFailure(command, result) {
199
+ const output = [result.stderr.trim(), result.stdout.trim()]
200
+ .filter(value => value.length > 0)
201
+ .join("\n");
202
+ const detail = clipTail(output, 1200);
203
+ const suffix = detail ? `: ${detail}` : "";
204
+ if (isProviderConfigurationFailure(command, result)) {
205
+ return new ForgeFailureError("provider_configuration_invalid", `${command} rejected its model configuration; correct the configuration before retrying${suffix}`, "provider");
206
+ }
207
+ if (isTransientCodexProviderNotFoundFailure(command, result)) {
208
+ return new ForgeFailureError("provider_request_not_found", `${command} received a transient not-found response from its provider service${suffix}`, "provider");
209
+ }
210
+ if (isProviderCapacityFailure(result)) {
211
+ return new ForgeFailureError("provider_capacity_unavailable", `${command} was deferred because the selected provider model is temporarily at capacity${suffix}`, "provider");
212
+ }
213
+ if (isProviderContextWindowExhausted(command, result)) {
214
+ return new ForgeFailureError("provider_context_window_exhausted", `${command} exhausted the provider context window and must continue in a fresh session${suffix}`, "provider");
215
+ }
216
+ return new Error(`${command} exited ${result.exitCode}${suffix}`);
217
+ }
218
+ function clipTail(value, maximumLength) {
219
+ if (value.length <= maximumLength)
220
+ return value;
221
+ return `...${value.slice(-(maximumLength - 3))}`;
222
+ }
223
+ /**
224
+ * Capacity is trusted only when a nonzero provider process writes one of the
225
+ * narrow service-level messages to stderr. Stdout can contain arbitrary agent
226
+ * command output and account usage-limit messages remain ordinary failures.
227
+ */
228
+ export function isProviderCapacityFailure(result) {
229
+ if (result.exitCode === 0)
230
+ return false;
231
+ const capacityMessages = new Set([
232
+ "the selected model is at capacity. please try again later.",
233
+ "the selected model is currently at capacity. please try again later.",
234
+ "the model is currently overloaded. please try again later.",
235
+ "the provider is currently overloaded. please try again later.",
236
+ ]);
237
+ return result.stderr
238
+ .split(/\r?\n/u)
239
+ .map(line => line.trim().toLowerCase().replace(/^error:\s*/u, ""))
240
+ .some(line => capacityMessages.has(line));
241
+ }
242
+ /** Only a nonzero Codex service diagnostic can classify a configuration rejection.
243
+ * JSONL turn.failed is a provider envelope; item/tool output is not retry authority. */
244
+ export function isProviderConfigurationFailure(command, result) {
245
+ const names = [path.posix.parse(command).name, path.win32.parse(command).name];
246
+ if (result.exitCode === 0 || !names.some(name => name.toLowerCase() === "codex"))
247
+ return false;
248
+ const reasoningRejected = /Unsupported value: '[^'\r\n]+' is not supported with the '[^'\r\n]+' model\./u.test(result.stderr)
249
+ && result.stderr.includes("reasoning.effort")
250
+ && result.stderr.includes("invalid_request_error")
251
+ && result.stderr.includes("unsupported_value");
252
+ if (reasoningRejected)
253
+ return true;
254
+ return [result.stderr, result.stdout].some(output => output.split(/\r?\n/u).some(line => {
255
+ try {
256
+ const event = JSON.parse(line);
257
+ if (event.type !== "turn.failed")
258
+ return false;
259
+ const error = event.error;
260
+ if (typeof error?.message !== "string")
261
+ return false;
262
+ const service = JSON.parse(error.message);
263
+ return service.type === "error" && service.status === 400
264
+ && service.error?.type === "invalid_request_error"
265
+ && typeof service.error.message === "string"
266
+ && /^The '[^'\r\n]+' model is not supported when using Codex with a ChatGPT account\.$/u.test(service.error.message);
267
+ }
268
+ catch {
269
+ return false;
270
+ }
271
+ }));
272
+ }
273
+ /**
274
+ * A context limit is trusted only at the provider process boundary. The exact
275
+ * Codex message can be embedded in its JSONL stderr envelope, so substring
276
+ * matching is intentional but remains narrow and stderr-only.
277
+ */
278
+ export function isProviderContextWindowExhausted(command, result) {
279
+ const executableNames = [path.posix.parse(command).name, path.win32.parse(command).name]
280
+ .map(name => name.toLowerCase());
281
+ if (result.exitCode === 0 || !executableNames.includes("codex"))
282
+ return false;
283
+ return result.stderr.toLowerCase().includes("codex ran out of room in the model's context window");
284
+ }
59
285
  const defaultProviderToolHeartbeatIntervalMs = 60_000;
60
286
  const defaultManagedVerificationProgressIntervalMs = 30_000;
287
+ function compareManagedVerificationSnapshots(left, right) {
288
+ const leftOrder = managedVerificationStartedAt(left.startedAt) ?? Number.MAX_SAFE_INTEGER;
289
+ const rightOrder = managedVerificationStartedAt(right.startedAt) ?? Number.MAX_SAFE_INTEGER;
290
+ return leftOrder - rightOrder || left.commandId.localeCompare(right.commandId);
291
+ }
292
+ function managedVerificationStartedAt(value) {
293
+ const parsed = Date.parse(value ?? "");
294
+ return Number.isFinite(parsed) ? parsed : null;
295
+ }
61
296
  export class ManagedVerificationProgressEmitter {
62
297
  logDirectory;
63
298
  emit;
64
299
  intervalMs;
65
300
  timer = null;
66
- lastSignature = null;
301
+ observedStatuses = new Map();
302
+ completedCommands = new Set();
67
303
  constructor(logDirectory, emit, intervalMs = defaultManagedVerificationProgressIntervalMs) {
68
304
  this.logDirectory = logDirectory;
69
305
  this.emit = emit;
@@ -94,40 +330,67 @@ export class ManagedVerificationProgressEmitter {
94
330
  .map(async (name) => parseManagedVerificationSnapshot(path.join(directory, name))));
95
331
  const current = snapshots
96
332
  .filter((entry) => entry !== null)
97
- .sort((left, right) => right.elapsedMs - left.elapsedMs)[0];
98
- if (!current)
333
+ .sort(compareManagedVerificationSnapshots);
334
+ if (current.some(snapshot => isManagedVerificationNonterminal(snapshot.status))) {
335
+ // Durable managed-command receipts are authoritative evidence that Forge
336
+ // still supervises work even when the provider is not inside an MCP tool
337
+ // call. Refresh the API idle deadline without inflating the transcript.
338
+ await this.emit({
339
+ role: "runner",
340
+ kind: "status",
341
+ content: "A supervised managed verification command is still running.",
342
+ eventType: "provider_tool_heartbeat",
343
+ });
344
+ }
345
+ for (const snapshot of current) {
346
+ await this.emitTransition(snapshot);
347
+ }
348
+ }
349
+ async emitTransition(current) {
350
+ const terminal = ["pass", "fail", "timed_out", "timeout_termination_failed", "runner_error"].includes(current.status);
351
+ if (terminal && this.completedCommands.has(current.commandId))
99
352
  return;
100
- const elapsedMs = current.startedAt
101
- ? Math.max(current.elapsedMs ?? 0, Date.now() - Date.parse(current.startedAt))
102
- : current.elapsedMs;
103
- const progressBucket = elapsedMs === null ? "none" : Math.floor(elapsedMs / this.intervalMs);
104
- const signature = `${current.commandId}:${current.status}:${current.outputRevision ?? "none"}:${progressBucket}`;
105
- if (signature === this.lastSignature)
353
+ const previousStatus = this.observedStatuses.get(current.commandId);
354
+ if (previousStatus === current.status)
106
355
  return;
107
- const firstObservation = this.lastSignature === null;
108
- this.lastSignature = signature;
109
- const terminal = ["pass", "fail", "timed_out", "timeout_termination_failed", "runner_error"].includes(current.status);
356
+ this.observedStatuses.set(current.commandId, current.status);
357
+ if (terminal)
358
+ this.completedCommands.add(current.commandId);
359
+ // Starting and running are one externally meaningful state. The receipt is
360
+ // still refreshed on disk for UI telemetry, but repeated output/elapsed
361
+ // changes must not inflate the provider transcript or session history.
362
+ if (!terminal && previousStatus === "starting" && current.status === "running")
363
+ return;
364
+ const startedAt = managedVerificationStartedAt(current.startedAt);
365
+ const elapsedMs = startedAt !== null
366
+ ? Math.max(current.elapsedMs ?? 0, Date.now() - startedAt)
367
+ : current.elapsedMs;
110
368
  const elapsed = elapsedMs === null ? "an unknown duration" : formatElapsed(elapsedMs);
111
- const outputTail = current.stderrTail || current.stdoutTail;
369
+ const outputTail = current.lastOutputStream === "stderr"
370
+ ? current.stderrTail || current.stdoutTail
371
+ : current.stdoutTail || current.stderrTail;
372
+ const recovered = previousStatus === undefined && terminal;
112
373
  await this.emit({
113
374
  role: "runner",
114
375
  kind: terminal && current.status !== "pass" ? "error" : "status",
115
376
  content: terminal
116
- ? `Managed verification ${current.status} after ${elapsed}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`
117
- : firstObservation
118
- ? `QA is waiting on managed verification ${current.commandId}; Forge will keep supervising it while the provider waits.`
119
- : `Managed verification is still running after ${elapsed}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`,
120
- eventType: terminal ? "verification_completed" : firstObservation ? "verification_waiting" : "verification_progress",
377
+ ? `Managed verification ${current.status} after ${elapsed}${recovered ? " (recovered from its durable receipt)" : ""}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`
378
+ : `QA started managed verification ${current.commandId}. Forge is supervising it; durable progress remains available in the UI while the provider continues independent work.`,
379
+ eventType: terminal ? "verification_completed" : "verification_waiting",
121
380
  data: JSON.stringify({
122
381
  command_id: current.commandId,
123
382
  status: current.status,
124
383
  elapsed_ms: elapsedMs,
125
384
  output_revision: current.outputRevision,
126
385
  last_output_at: current.lastOutputAt,
386
+ recovered,
127
387
  }),
128
388
  });
129
389
  }
130
390
  }
391
+ function isManagedVerificationNonterminal(status) {
392
+ return status === "starting" || status === "running" || status === "waiting_for_build_slot";
393
+ }
131
394
  async function parseManagedVerificationSnapshot(receiptPath) {
132
395
  try {
133
396
  const receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
@@ -142,6 +405,9 @@ async function parseManagedVerificationSnapshot(receiptPath) {
142
405
  startedAt: typeof receipt.started_at === "string" ? receipt.started_at : null,
143
406
  outputRevision: typeof receipt.output_revision === "number" ? receipt.output_revision : null,
144
407
  lastOutputAt: typeof receipt.last_output_at === "string" ? receipt.last_output_at : null,
408
+ lastOutputStream: receipt.last_output_stream === "stdout" || receipt.last_output_stream === "stderr"
409
+ ? receipt.last_output_stream
410
+ : null,
145
411
  stdoutTail: tail(typeof receipt.stdout === "string" ? receipt.stdout : ""),
146
412
  stderrTail: tail(typeof receipt.stderr === "string" ? receipt.stderr : ""),
147
413
  };
@@ -246,10 +512,14 @@ export class ProcessBackedProviderCliService {
246
512
  processRunner;
247
513
  git;
248
514
  providerToolHeartbeatIntervalMs;
249
- constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs) {
515
+ publicationCoordinator;
516
+ prePublicationValidator;
517
+ constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs, publicationCoordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot()), prePublicationValidator = new PrePublicationValidator()) {
250
518
  this.processRunner = processRunner;
251
519
  this.git = git;
252
520
  this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
521
+ this.publicationCoordinator = publicationCoordinator;
522
+ this.prePublicationValidator = prePublicationValidator;
253
523
  }
254
524
  async runDraft(context) {
255
525
  const { dispatch } = context;
@@ -311,7 +581,7 @@ export class ProcessBackedProviderCliService {
311
581
  context.signal?.addEventListener("abort", abortProvider, { once: true });
312
582
  }
313
583
  try {
314
- const runPromise = (context.processRunner ?? this.processRunner).run({
584
+ const runPromise = recoveringProviderProcessRunner(context.processRunner ?? this.processRunner).run({
315
585
  command: command.command,
316
586
  args: command.args,
317
587
  cwd: worktree.worktreePath,
@@ -362,7 +632,7 @@ export class ProcessBackedProviderCliService {
362
632
  context.signal?.removeEventListener("abort", abortArtifactWait);
363
633
  }
364
634
  if (result.exitCode !== 0 && !earlyProposalArtifact) {
365
- throw new Error(`Provider CLI exited with code ${result.exitCode}${summarizeStderr(result.stderr)}.`);
635
+ throw providerCliExitFailure(command.command, result);
366
636
  }
367
637
  await context.emit({
368
638
  role: "runner",
@@ -429,6 +699,7 @@ export class ProcessBackedProviderCliService {
429
699
  }
430
700
  async runImplementation(context) {
431
701
  const { dispatch, worktree } = context;
702
+ const validationBaselineRevision = (await this.git.run(worktree.worktreePath, ["rev-parse", "HEAD"])).trim();
432
703
  const phase = completionPhaseFromDispatch(dispatch);
433
704
  const developCycle = usesDevelopCycleCompletion(dispatch);
434
705
  const debugCycle = usesDebugCycleCompletion(dispatch);
@@ -479,7 +750,7 @@ export class ProcessBackedProviderCliService {
479
750
  let result;
480
751
  try {
481
752
  liveCompletionEvents.start();
482
- result = await (context.processRunner ?? this.processRunner).run({
753
+ result = await recoveringProviderProcessRunner(context.processRunner ?? this.processRunner).run({
483
754
  command: command.command,
484
755
  args: command.args,
485
756
  cwd: worktree.worktreePath,
@@ -505,8 +776,7 @@ export class ProcessBackedProviderCliService {
505
776
  await cleanupCompletionTools(completionTools, context.emit);
506
777
  }
507
778
  if (result.exitCode !== 0) {
508
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
509
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
779
+ throw providerCliExitFailure(command.command, result);
510
780
  }
511
781
  await context.emit({
512
782
  role: "runner",
@@ -515,9 +785,20 @@ export class ProcessBackedProviderCliService {
515
785
  eventType: "provider_cli_completed"
516
786
  });
517
787
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
788
+ if (developCycle || phase === "implementation") {
789
+ await this.validatePrePublicationWithRecovery({
790
+ context,
791
+ validationProfile: "implementation",
792
+ repairEnvironment: context.runtimeEnvironment,
793
+ outputState,
794
+ preparedHeadSha: validationBaselineRevision,
795
+ repairScope: "implementation",
796
+ });
797
+ }
518
798
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
799
+ let publishedCommitSha = null;
519
800
  if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
520
- await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
801
+ publishedCommitSha = await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit);
521
802
  await context.emit({
522
803
  role: "runner",
523
804
  kind: "checkpoint",
@@ -551,7 +832,9 @@ export class ProcessBackedProviderCliService {
551
832
  });
552
833
  }
553
834
  return {
554
- summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${debugCycle ? "debug cycle" : developCycle ? "develop cycle" : phase}, ${committed.commitSha.slice(0, 12)}).`,
835
+ summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${debugCycle ? "debug cycle" : developCycle ? "develop cycle" : phase}, ${(publishedCommitSha ?? committed.commitSha).slice(0, 12)}).`,
836
+ resultCommitSha: publishedCommitSha ?? committed.commitSha,
837
+ publishedCommitSha: publishedCommitSha ?? undefined,
555
838
  spec: {
556
839
  repoPath: worktree.worktreePath,
557
840
  path: specPath,
@@ -605,7 +888,7 @@ export class ProcessBackedProviderCliService {
605
888
  let result;
606
889
  try {
607
890
  liveArchitectEvents.start();
608
- result = await (context.processRunner ?? this.processRunner).run({
891
+ result = await recoveringProviderProcessRunner(context.processRunner ?? this.processRunner).run({
609
892
  command: command.command,
610
893
  args: command.args,
611
894
  cwd: worktree.worktreePath,
@@ -629,8 +912,7 @@ export class ProcessBackedProviderCliService {
629
912
  await cleanupCompletionTools(completionTools, context.emit);
630
913
  }
631
914
  if (result.exitCode !== 0) {
632
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
633
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
915
+ throw providerCliExitFailure(command.command, result);
634
916
  }
635
917
  await context.emit({
636
918
  role: "runner",
@@ -710,7 +992,7 @@ export class ProcessBackedProviderCliService {
710
992
  try {
711
993
  liveQaEvents.start();
712
994
  verificationProgress.start();
713
- result = await (context.processRunner ?? this.processRunner).run({
995
+ result = await recoveringProviderProcessRunner(context.processRunner ?? this.processRunner).run({
714
996
  command: command.command,
715
997
  args: command.args,
716
998
  cwd: worktree.worktreePath,
@@ -735,8 +1017,7 @@ export class ProcessBackedProviderCliService {
735
1017
  await cleanupCompletionTools(completionTools, context.emit);
736
1018
  }
737
1019
  if (result.exitCode !== 0) {
738
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
739
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
1020
+ throw providerCliExitFailure(command.command, result);
740
1021
  }
741
1022
  await context.emit({
742
1023
  role: "runner",
@@ -745,7 +1026,12 @@ export class ProcessBackedProviderCliService {
745
1026
  eventType: "provider_cli_completed"
746
1027
  });
747
1028
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
748
- const qaArtifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId);
1029
+ let qaArtifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId);
1030
+ if (!qaArtifact && !(await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "qa"))) {
1031
+ // A headless provider that ends its turn to "wait for a background watcher"
1032
+ // exits for good. Resume the same thread instead of failing the session.
1033
+ qaArtifact = await this.recoverMissingQaCompletion({ context, outputState });
1034
+ }
749
1035
  if (!qaArtifact) {
750
1036
  const verdictOnly = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "qa");
751
1037
  throw new Error(verdictOnly
@@ -784,6 +1070,7 @@ export class ProcessBackedProviderCliService {
784
1070
  }
785
1071
  async runPullRequestRepair(context, route) {
786
1072
  const { dispatch, worktree } = context;
1073
+ const { expectedRepairHeadSha, preparedHeadSha } = await assertRepairHeadEpoch(context, this.git);
787
1074
  const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
788
1075
  const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
789
1076
  let repairSpec = null;
@@ -823,6 +1110,15 @@ export class ProcessBackedProviderCliService {
823
1110
  eventType: "completion_tools_configured",
824
1111
  });
825
1112
  }
1113
+ if (addressComments) {
1114
+ completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "comment_repair", runtimeEnvironment: repairEnvironment });
1115
+ await context.emit({
1116
+ role: "runner",
1117
+ kind: "checkpoint",
1118
+ content: "Forge comment-repair completion tool configured; provider must record an evidence-backed disposition for every actionable snapshot finding.",
1119
+ eventType: "completion_tools_configured",
1120
+ });
1121
+ }
826
1122
  const repairPrompt = repairChecks
827
1123
  ? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
828
1124
  : addressComments
@@ -846,8 +1142,9 @@ export class ProcessBackedProviderCliService {
846
1142
  const outputState = createProviderOutputState(dispatch.execution.provider);
847
1143
  const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
848
1144
  let result;
1145
+ let providerStarted = false;
849
1146
  try {
850
- result = await (context.processRunner ?? this.processRunner).run({
1147
+ result = await recoveringProviderProcessRunner(context.processRunner ?? this.processRunner).run({
851
1148
  command: command.command,
852
1149
  args: command.args,
853
1150
  cwd: worktree.worktreePath,
@@ -861,6 +1158,7 @@ export class ProcessBackedProviderCliService {
861
1158
  content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} is supervised and ready.`,
862
1159
  eventType: "provider_cli_started"
863
1160
  });
1161
+ providerStarted = true;
864
1162
  },
865
1163
  onStdoutLine: async (line) => {
866
1164
  await toolHeartbeat.consume(line);
@@ -869,13 +1167,28 @@ export class ProcessBackedProviderCliService {
869
1167
  }
870
1168
  finally {
871
1169
  toolHeartbeat.stop();
872
- if (completionTools) {
873
- await cleanupCompletionTools(completionTools, context.emit);
1170
+ try {
1171
+ if (providerStarted) {
1172
+ await context.emit({
1173
+ role: "runner",
1174
+ kind: "checkpoint",
1175
+ content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} turn exited.`,
1176
+ eventType: "provider_cli_completed"
1177
+ });
1178
+ }
1179
+ }
1180
+ finally {
1181
+ if (completionTools) {
1182
+ await cleanupCompletionTools(completionTools, context.emit);
1183
+ }
874
1184
  }
875
1185
  }
876
1186
  if (result.exitCode !== 0) {
877
- const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
878
- throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
1187
+ throw providerCliExitFailure(command.command, result);
1188
+ }
1189
+ const postProviderHeadSha = (await this.git.run(worktree.worktreePath, ["rev-parse", "HEAD"])).trim();
1190
+ if (postProviderHeadSha.toLowerCase() !== preparedHeadSha.toLowerCase()) {
1191
+ throw new Error(`provider_publication_boundary_violation: PR repair changed HEAD from ${preparedHeadSha} to ${postProviderHeadSha}; Forge requires provider changes to remain uncommitted and unpushed until validation passes.`);
879
1192
  }
880
1193
  let repairArtifact = null;
881
1194
  if (repairChecks) {
@@ -886,14 +1199,57 @@ export class ProcessBackedProviderCliService {
886
1199
  if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
887
1200
  throw new Error(`CI repair completion gate recorded ${repairArtifact.status}; Forge did not commit or push the repair.`);
888
1201
  }
1202
+ }
1203
+ if (addressComments) {
1204
+ repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "comment_repair");
1205
+ if (!repairArtifact) {
1206
+ repairArtifact = await this.recoverMissingCommentRepairCompletion({
1207
+ context,
1208
+ repairEnvironment,
1209
+ outputState,
1210
+ preparedHeadSha,
1211
+ });
1212
+ }
1213
+ if (!repairArtifact) {
1214
+ throw new Error("Provider CLI completed but did not call forge.complete_comment_repair. Forge did not publish or resolve review findings.");
1215
+ }
1216
+ if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
1217
+ throw new Error(`Comment repair completion gate recorded ${repairArtifact.status}; Forge did not publish or resolve review findings.`);
1218
+ }
1219
+ assertCommentRepairArtifactMatchesDispatch(dispatch, repairArtifact);
1220
+ }
1221
+ let commentRepairBaselineChangedFiles = null;
1222
+ if (addressComments && repairArtifact) {
1223
+ commentRepairBaselineChangedFiles = await stagePublishableWorktreeChanges(this.git, worktree.worktreePath);
1224
+ assertCommentRepairArtifactMatchesChanges(repairArtifact, commentRepairBaselineChangedFiles);
1225
+ }
1226
+ const validationProfile = repairChecks
1227
+ ? "ci_repair"
1228
+ : addressComments
1229
+ ? "comment_repair"
1230
+ : "conflict_repair";
1231
+ await this.validatePrePublicationWithRecovery({
1232
+ context,
1233
+ validationProfile,
1234
+ repairEnvironment,
1235
+ outputState,
1236
+ preparedHeadSha,
1237
+ repairScope: "pull_request",
1238
+ });
1239
+ const stagedChangedFiles = await stagePublishableWorktreeChanges(this.git, worktree.worktreePath);
1240
+ if (addressComments && repairArtifact && commentRepairBaselineChangedFiles) {
1241
+ repairArtifact = await finalizeCommentRepairChangedFiles(repairArtifact, commentRepairBaselineChangedFiles, stagedChangedFiles);
1242
+ }
1243
+ const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id, stagedChangedFiles);
1244
+ const publishedByProvider = providerOwnsBranchPublication(context);
1245
+ const publishedCommitSha = publishedByProvider
1246
+ ? await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit, expectedRepairHeadSha)
1247
+ : null;
1248
+ if (repairArtifact) {
889
1249
  await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
890
1250
  await emitPhaseCompletionEvent(context.emit, repairArtifact);
891
1251
  await emitPhaseGateEvents(context.emit, repairArtifact);
892
1252
  }
893
- const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
894
- if (providerOwnsBranchPublication(context)) {
895
- await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
896
- }
897
1253
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
898
1254
  await context.emit({
899
1255
  role: "assistant",
@@ -907,15 +1263,359 @@ export class ProcessBackedProviderCliService {
907
1263
  });
908
1264
  return {
909
1265
  summary: repairChecks
910
- ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
1266
+ ? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${(publishedCommitSha ?? committed.commitSha).slice(0, 12)}).`
911
1267
  : addressComments
912
- ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
913
- : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`,
1268
+ ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${(publishedCommitSha ?? committed.commitSha).slice(0, 12)}).`
1269
+ : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${(publishedCommitSha ?? committed.commitSha).slice(0, 12)}).`,
1270
+ resultCommitSha: publishedCommitSha ?? committed.commitSha,
1271
+ publishedCommitSha,
914
1272
  spec: repairSpec,
915
1273
  artifact: repairArtifact,
916
1274
  artifacts: repairArtifact ? [repairArtifact] : [],
917
1275
  };
918
1276
  }
1277
+ async recoverMissingQaCompletion(options) {
1278
+ const { context, outputState } = options;
1279
+ const { dispatch, worktree } = context;
1280
+ const normalizedProvider = dispatch.execution.provider.trim().toLowerCase();
1281
+ const providerThreadId = outputState.providerThreadId?.trim();
1282
+ if ((normalizedProvider !== "codex" && normalizedProvider !== "cursor" && normalizedProvider !== "claude")
1283
+ || !providerThreadId) {
1284
+ await context.emit({
1285
+ role: "runner",
1286
+ kind: "error",
1287
+ content: `Forge could not resume QA for the missing verdict because provider '${dispatch.execution.provider}' did not report a resumable thread.`,
1288
+ eventType: "completion_tool_recovery_unavailable",
1289
+ data: JSON.stringify({ phase: "qa", provider: normalizedProvider }),
1290
+ });
1291
+ return null;
1292
+ }
1293
+ const recoveryDispatch = {
1294
+ ...dispatch,
1295
+ session: { ...dispatch.session, resumeProviderThreadId: providerThreadId },
1296
+ };
1297
+ const recoveryPrompt = [
1298
+ "Your previous turn ended before Forge received the QA verdict, so the provider process exited.",
1299
+ "Background watchers, monitors and notifications from that turn did not survive and will never report back.",
1300
+ "Resume QA exactly where it stopped. Do not repeat gates you already recorded.",
1301
+ "Check each managed command with forge.get_managed_command, restart any that were interrupted,",
1302
+ "and block on long commands with forge.wait_managed_command, calling it again while command_continues is true.",
1303
+ "Do not end your turn until you have called every remaining QA gate through forge.complete_qa_verdict.",
1304
+ "If a gate cannot get evidence, record it as blocked with the reason instead of ending the turn.",
1305
+ ].join(" ");
1306
+ for (let turn = 1; turn <= maxQaCompletionRecoveryTurns; turn += 1) {
1307
+ const providerPrompt = buildProviderCliPrompt(recoveryDispatch, recoveryPrompt, context.runtimeEnvironment);
1308
+ const pipePromptToStdin = shouldPipePromptToStdin(recoveryDispatch.execution.provider, providerPrompt, usesManagedWindowsHost(recoveryDispatch));
1309
+ const completionTools = await prepareCompletionToolRuntime({
1310
+ dispatch: recoveryDispatch,
1311
+ worktree,
1312
+ phase: "qa",
1313
+ runtimeEnvironment: context.runtimeEnvironment,
1314
+ });
1315
+ const command = buildProviderCliCommand(recoveryDispatch, {
1316
+ worktreePath: worktree.worktreePath,
1317
+ prompt: recoveryPrompt,
1318
+ promptFromStdin: pipePromptToStdin,
1319
+ completionTools,
1320
+ runtimeEnvironment: context.runtimeEnvironment,
1321
+ });
1322
+ await context.emit({
1323
+ role: "runner",
1324
+ kind: "checkpoint",
1325
+ content: `QA ended without a verdict. Resuming ${dispatch.execution.provider} ${outputState.sessionLabel ?? "provider thread"} (turn ${turn}/${maxQaCompletionRecoveryTurns}) to finish the remaining QA gates.`,
1326
+ eventType: "completion_tool_recovery_started",
1327
+ data: JSON.stringify({ phase: "qa", providerThreadId, turn }),
1328
+ });
1329
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
1330
+ let result;
1331
+ try {
1332
+ result = await recoveringProviderProcessRunner(context.processRunner ?? this.processRunner).run({
1333
+ command: command.command,
1334
+ args: command.args,
1335
+ cwd: worktree.worktreePath,
1336
+ env: providerProcessEnv(recoveryDispatch, context.runtimeEnvironment),
1337
+ stdin: pipePromptToStdin ? providerPrompt : undefined,
1338
+ }, { onStdoutLine: async (line) => await toolHeartbeat.consume(line) }, context.signal);
1339
+ }
1340
+ finally {
1341
+ toolHeartbeat.stop();
1342
+ await cleanupCompletionTools(completionTools, context.emit);
1343
+ }
1344
+ if (result.exitCode !== 0)
1345
+ throw providerCliExitFailure(command.command, result);
1346
+ const artifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId);
1347
+ if (artifact || await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "qa")) {
1348
+ await context.emit({
1349
+ role: "runner",
1350
+ kind: "checkpoint",
1351
+ content: `Forge resumed the QA thread and received its completion record on recovery turn ${turn}.`,
1352
+ eventType: "completion_tool_recovery_completed",
1353
+ data: JSON.stringify({ phase: "qa", providerThreadId, turn }),
1354
+ });
1355
+ return artifact;
1356
+ }
1357
+ }
1358
+ await context.emit({
1359
+ role: "runner",
1360
+ kind: "error",
1361
+ content: `The resumed QA thread still did not call the Forge QA completion tool after ${maxQaCompletionRecoveryTurns} recovery turns.`,
1362
+ eventType: "completion_tool_recovery_exhausted",
1363
+ data: JSON.stringify({ phase: "qa", providerThreadId }),
1364
+ });
1365
+ return null;
1366
+ }
1367
+ async recoverMissingCommentRepairCompletion(options) {
1368
+ const { context, repairEnvironment, outputState, preparedHeadSha } = options;
1369
+ const { dispatch, worktree } = context;
1370
+ const normalizedProvider = dispatch.execution.provider.trim().toLowerCase();
1371
+ if (normalizedProvider !== "codex" && normalizedProvider !== "cursor" && normalizedProvider !== "claude") {
1372
+ await context.emit({
1373
+ role: "runner",
1374
+ kind: "error",
1375
+ content: `Forge could not recover the missing comment-repair completion record because provider '${dispatch.execution.provider}' has no declared thread-resume contract. The dirty worktree remains preserved.`,
1376
+ eventType: "completion_tool_recovery_unavailable",
1377
+ data: JSON.stringify({
1378
+ phase: "comment_repair",
1379
+ provider: normalizedProvider,
1380
+ reason: "thread_resume_unsupported",
1381
+ }),
1382
+ });
1383
+ return null;
1384
+ }
1385
+ const providerThreadId = outputState.providerThreadId?.trim();
1386
+ if (!providerThreadId) {
1387
+ await context.emit({
1388
+ role: "runner",
1389
+ kind: "error",
1390
+ content: "Forge could not recover the missing comment-repair completion record because the provider did not report a resumable thread id. The dirty worktree remains preserved.",
1391
+ eventType: "completion_tool_recovery_unavailable",
1392
+ });
1393
+ return null;
1394
+ }
1395
+ const recoveryDispatch = {
1396
+ ...dispatch,
1397
+ session: { ...dispatch.session, resumeProviderThreadId: providerThreadId },
1398
+ };
1399
+ const recoveryPrompt = [
1400
+ "Forge did not receive the required forge.complete_comment_repair call from your previous turn.",
1401
+ "Resume the existing work exactly where it is. Do not reimplement or discard the prepared changes.",
1402
+ "Review the current diff and the durable review snapshot, preserve the validation evidence already gathered,",
1403
+ "then call forge.complete_comment_repair with an evidence-backed disposition for every actionable finding.",
1404
+ "Do not commit or push; Forge owns publication after the completion gate succeeds.",
1405
+ ].join(" ");
1406
+ const providerPrompt = buildProviderCliPrompt(recoveryDispatch, recoveryPrompt, repairEnvironment);
1407
+ const pipePromptToStdin = shouldPipePromptToStdin(recoveryDispatch.execution.provider, providerPrompt, usesManagedWindowsHost(recoveryDispatch));
1408
+ const completionTools = await prepareCompletionToolRuntime({
1409
+ dispatch: recoveryDispatch,
1410
+ worktree,
1411
+ phase: "comment_repair",
1412
+ runtimeEnvironment: repairEnvironment,
1413
+ });
1414
+ const command = buildProviderCliCommand(recoveryDispatch, {
1415
+ worktreePath: worktree.worktreePath,
1416
+ prompt: recoveryPrompt,
1417
+ promptFromStdin: pipePromptToStdin,
1418
+ completionTools,
1419
+ runtimeEnvironment: repairEnvironment,
1420
+ });
1421
+ await context.emit({
1422
+ role: "runner",
1423
+ kind: "checkpoint",
1424
+ content: `Resuming ${dispatch.execution.provider} ${outputState.sessionLabel ?? "provider thread"} once to record the missing comment-repair completion artifact without repeating implementation work.`,
1425
+ eventType: "completion_tool_recovery_started",
1426
+ data: JSON.stringify({ phase: "comment_repair", providerThreadId }),
1427
+ });
1428
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
1429
+ let result;
1430
+ let providerStarted = false;
1431
+ try {
1432
+ result = await recoveringProviderProcessRunner(context.processRunner ?? this.processRunner).run({
1433
+ command: command.command,
1434
+ args: command.args,
1435
+ cwd: worktree.worktreePath,
1436
+ env: providerProcessEnv(recoveryDispatch, repairEnvironment),
1437
+ stdin: pipePromptToStdin ? providerPrompt : undefined,
1438
+ }, {
1439
+ onStarted: async () => {
1440
+ providerStarted = true;
1441
+ await context.emit({
1442
+ role: "runner",
1443
+ kind: "checkpoint",
1444
+ content: "Provider completion-tool recovery turn is supervised and ready.",
1445
+ eventType: "provider_cli_started",
1446
+ });
1447
+ },
1448
+ onStdoutLine: async (line) => {
1449
+ await toolHeartbeat.consume(line);
1450
+ },
1451
+ }, context.signal);
1452
+ }
1453
+ finally {
1454
+ toolHeartbeat.stop();
1455
+ try {
1456
+ if (providerStarted) {
1457
+ await context.emit({
1458
+ role: "runner",
1459
+ kind: "checkpoint",
1460
+ content: "Provider completion-tool recovery turn exited.",
1461
+ eventType: "provider_cli_completed",
1462
+ });
1463
+ }
1464
+ }
1465
+ finally {
1466
+ await cleanupCompletionTools(completionTools, context.emit);
1467
+ }
1468
+ }
1469
+ if (result.exitCode !== 0)
1470
+ throw providerCliExitFailure(command.command, result);
1471
+ const postRecoveryHeadSha = (await this.git.run(worktree.worktreePath, ["rev-parse", "HEAD"])).trim();
1472
+ if (postRecoveryHeadSha.toLowerCase() !== preparedHeadSha.toLowerCase()) {
1473
+ throw new Error(`provider_publication_boundary_violation: completion-tool recovery changed HEAD from ${preparedHeadSha} to ${postRecoveryHeadSha}; Forge requires provider changes to remain uncommitted and unpushed until validation passes.`);
1474
+ }
1475
+ const artifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "comment_repair");
1476
+ await context.emit({
1477
+ role: "runner",
1478
+ kind: artifact ? "checkpoint" : "error",
1479
+ content: artifact
1480
+ ? "Forge recovered the missing comment-repair completion artifact from the same provider thread."
1481
+ : "The resumed provider thread still did not call forge.complete_comment_repair. The dirty worktree remains preserved and unpublished.",
1482
+ eventType: artifact ? "completion_tool_recovery_completed" : "completion_tool_recovery_exhausted",
1483
+ data: JSON.stringify({ phase: "comment_repair", providerThreadId }),
1484
+ });
1485
+ return artifact;
1486
+ }
1487
+ async validatePrePublicationWithRecovery(options) {
1488
+ const { context, validationProfile, repairEnvironment, outputState, preparedHeadSha, repairScope } = options;
1489
+ const { dispatch, worktree } = context;
1490
+ const processRunner = recoveringProviderProcessRunner(context.processRunner ?? this.processRunner);
1491
+ const environment = providerProcessEnv(dispatch, repairEnvironment);
1492
+ const validationEnvironment = prePublicationValidationEnv(dispatch, repairEnvironment);
1493
+ for (let repairTurn = 0;; repairTurn += 1) {
1494
+ try {
1495
+ await this.prePublicationValidator.validate(worktree.worktreePath, validationProfile, publicationCapabilities(context, repairEnvironment), context.emit, context.signal, prePublicationExecution(context, validationEnvironment, this.processRunner, preparedHeadSha));
1496
+ if (repairTurn > 0) {
1497
+ await context.emit({
1498
+ role: "runner",
1499
+ kind: "checkpoint",
1500
+ content: `Pre-publication validation passed after ${repairTurn} in-place repair turn${repairTurn === 1 ? "" : "s"}.`,
1501
+ eventType: "prepublication_repair_completed",
1502
+ data: JSON.stringify({ repairTurn, maxRepairTurns: maxPrePublicationRepairTurns }),
1503
+ });
1504
+ }
1505
+ return;
1506
+ }
1507
+ catch (error) {
1508
+ if (!(error instanceof PrePublicationValidationError))
1509
+ throw error;
1510
+ if (repairTurn >= maxPrePublicationRepairTurns) {
1511
+ await context.emit({
1512
+ role: "runner",
1513
+ kind: "error",
1514
+ content: `Pre-publication validation still fails after ${maxPrePublicationRepairTurns} in-place repair turns: ${clip(error.message, 4000)}`,
1515
+ eventType: "prepublication_repair_exhausted",
1516
+ data: JSON.stringify({ checkId: error.checkId, repairTurns: repairTurn, maxRepairTurns: maxPrePublicationRepairTurns }),
1517
+ });
1518
+ throw error;
1519
+ }
1520
+ const providerThreadId = outputState.providerThreadId?.trim();
1521
+ if (!providerThreadId) {
1522
+ throw new Error(`prepublication_repair_unavailable: ${error.message}; provider did not report a resumable thread id, so Forge preserved the dirty worktree without starting a blind replacement run.`, { cause: error });
1523
+ }
1524
+ const nextRepairTurn = repairTurn + 1;
1525
+ const focusedPrompt = buildPrePublicationRepairPrompt(error, nextRepairTurn, repairScope);
1526
+ const repairDispatch = {
1527
+ ...dispatch,
1528
+ session: { ...dispatch.session, resumeProviderThreadId: providerThreadId },
1529
+ };
1530
+ const providerPrompt = buildProviderCliPrompt(repairDispatch, focusedPrompt, repairEnvironment);
1531
+ const pipePromptToStdin = shouldPipePromptToStdin(repairDispatch.execution.provider, providerPrompt, usesManagedWindowsHost(repairDispatch));
1532
+ const command = buildProviderCliCommand(repairDispatch, {
1533
+ worktreePath: worktree.worktreePath,
1534
+ prompt: focusedPrompt,
1535
+ promptFromStdin: pipePromptToStdin,
1536
+ runtimeEnvironment: repairEnvironment,
1537
+ });
1538
+ await context.emit({
1539
+ role: "runner",
1540
+ kind: "checkpoint",
1541
+ content: `Resuming ${dispatch.execution.provider} ${outputState.sessionLabel ?? "provider thread"} for bounded pre-publication repair ${nextRepairTurn}/${maxPrePublicationRepairTurns} after '${error.checkId}' failed.`,
1542
+ eventType: "prepublication_repair_started",
1543
+ data: JSON.stringify({ checkId: error.checkId, repairTurn: nextRepairTurn, maxRepairTurns: maxPrePublicationRepairTurns }),
1544
+ });
1545
+ const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
1546
+ let result;
1547
+ let providerStarted = false;
1548
+ try {
1549
+ result = await processRunner.run({
1550
+ command: command.command,
1551
+ args: command.args,
1552
+ cwd: worktree.worktreePath,
1553
+ env: environment,
1554
+ stdin: pipePromptToStdin ? providerPrompt : undefined,
1555
+ }, {
1556
+ onStarted: async () => {
1557
+ await context.emit({
1558
+ role: "runner",
1559
+ kind: "checkpoint",
1560
+ content: `Provider pre-publication repair turn ${nextRepairTurn}/${maxPrePublicationRepairTurns} is supervised and ready.`,
1561
+ eventType: "provider_cli_started",
1562
+ });
1563
+ providerStarted = true;
1564
+ },
1565
+ onStdoutLine: async (line) => {
1566
+ await toolHeartbeat.consume(line);
1567
+ },
1568
+ }, context.signal);
1569
+ }
1570
+ finally {
1571
+ toolHeartbeat.stop();
1572
+ if (providerStarted) {
1573
+ await context.emit({
1574
+ role: "runner",
1575
+ kind: "checkpoint",
1576
+ content: `Provider pre-publication repair turn ${nextRepairTurn}/${maxPrePublicationRepairTurns} exited.`,
1577
+ eventType: "provider_cli_completed",
1578
+ });
1579
+ }
1580
+ }
1581
+ if (result.exitCode !== 0)
1582
+ throw providerCliExitFailure(command.command, result);
1583
+ const postRepairHeadSha = (await this.git.run(worktree.worktreePath, ["rev-parse", "HEAD"])).trim();
1584
+ if (postRepairHeadSha.toLowerCase() !== preparedHeadSha.toLowerCase()) {
1585
+ throw new Error(`provider_publication_boundary_violation: pre-publication repair changed HEAD from ${preparedHeadSha} to ${postRepairHeadSha}; Forge requires provider changes to remain uncommitted and unpushed until validation passes.`, { cause: error });
1586
+ }
1587
+ }
1588
+ }
1589
+ }
1590
+ }
1591
+ function buildPrePublicationRepairPrompt(error, repairTurn, repairScope) {
1592
+ const invocation = [error.command, ...error.args].join(" ");
1593
+ const priorWork = repairScope === "implementation"
1594
+ ? "Keep the completed implementation and its Forge-owned completion artifacts intact."
1595
+ : "Keep the existing PR-comment or CI repairs intact.";
1596
+ return [
1597
+ `Forge pre-publication validation failed after your ${repairScope === "implementation" ? "implementation" : "PR repair"} turn.`,
1598
+ `This is bounded in-place repair turn ${repairTurn}/${maxPrePublicationRepairTurns}.`,
1599
+ `Failed check: ${error.checkId}`,
1600
+ `Command: ${invocation}`,
1601
+ `Exit code: ${error.exitCode}`,
1602
+ "Diagnostics:",
1603
+ error.detail || "The validation command returned no diagnostic output.",
1604
+ "Fix the root cause in the current worktree and run the focused failing check.",
1605
+ `${priorWork} Do not commit, push, rebase, or modify Forge-owned protocol artifacts.`,
1606
+ "When the focused check passes, stop; Bridge will rerun the complete required pre-publication profile and owns commit/push.",
1607
+ ].join("\n\n");
1608
+ }
1609
+ function publicationCapabilities(context, runtimeEnvironment) {
1610
+ const capabilities = new Set();
1611
+ const launchTarget = context.dispatch.qaBrowser?.launchTarget;
1612
+ if (runtimeEnvironment?.FORGE_PLAYWRIGHT_CONFIG
1613
+ || launchTarget?.url
1614
+ || launchTarget?.command
1615
+ || launchTarget?.managed_runtime_port) {
1616
+ capabilities.add("browser_launch");
1617
+ }
1618
+ return capabilities;
919
1619
  }
920
1620
  async function stageDraftAttachments(dispatch, metadataRoot) {
921
1621
  const attachments = dispatch.draft?.attachments ?? [];
@@ -980,7 +1680,11 @@ function clipFileName(fileName, maxLength) {
980
1680
  }
981
1681
  const providerPromptStdinThreshold = 8_000;
982
1682
  function shouldPipePromptToStdin(provider, prompt, force = false) {
983
- return provider.trim().toLowerCase() === "codex"
1683
+ const normalized = provider.trim().toLowerCase();
1684
+ if (normalized === "claude") {
1685
+ return true;
1686
+ }
1687
+ return normalized === "codex"
984
1688
  && (force || Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold);
985
1689
  }
986
1690
  function usesManagedWindowsHost(dispatch) {