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