@loucompanion/forge-bridge 0.1.3-dev.d1ca67076aa9 → 0.1.3-dev.eeefdb0d19c1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +962 -22
- 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 +343 -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 +350 -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 +19 -5
- package/dist/session/phase-completion.runtime.d.ts +12 -1
- package/dist/session/phase-completion.runtime.js +127 -7
- package/dist/session/phase-completion.schemas.js +108 -3
- 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 +16 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +202 -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 +7 -3
- package/dist/session/provider-cli/provider-cli.service.js +399 -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 +107 -1
- package/dist/session/session.types.js +226 -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,107 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { GitClient } from "../../shared/git/git.js";
|
|
5
4
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
6
5
|
import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
|
|
7
6
|
import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle, summarizeStderr, waitForCompletionArtifact } from "./provider-cli.artifacts.js";
|
|
8
7
|
import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
|
|
9
|
-
import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, buildPullRequestCommentsPrompt, buildQaPrompt,
|
|
8
|
+
import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
|
|
10
9
|
import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
11
10
|
import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
|
|
12
11
|
import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
|
|
12
|
+
import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
|
|
13
13
|
export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
|
|
14
|
-
export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand } from "./provider-cli.prompts.js";
|
|
15
|
-
export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
14
|
+
export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
|
|
15
|
+
export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
16
16
|
export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
|
|
17
|
+
async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
|
|
18
|
+
try {
|
|
19
|
+
await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (!isNonFastForwardPushError(err)) {
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
await emit?.({
|
|
27
|
+
role: "runner",
|
|
28
|
+
kind: "checkpoint",
|
|
29
|
+
content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
|
|
30
|
+
eventType: "worktree_branch_push_rebase"
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
|
|
34
|
+
await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
|
|
35
|
+
await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
|
|
36
|
+
}
|
|
37
|
+
catch (retryErr) {
|
|
38
|
+
throw new Error(`git push origin HEAD:${branchName} was rejected because the remote branch moved, and automatic fetch/rebase failed: ${errorMessage(retryErr)}`, { cause: retryErr });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function providerOwnsBranchPublication(context) {
|
|
43
|
+
return context.dispatch.sandbox?.executionMode !== "docker";
|
|
44
|
+
}
|
|
45
|
+
function remoteTrackingRefspec(branchName) {
|
|
46
|
+
return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
|
|
47
|
+
}
|
|
48
|
+
function isNonFastForwardPushError(err) {
|
|
49
|
+
const message = errorMessage(err).toLowerCase();
|
|
50
|
+
return message.includes("non-fast-forward")
|
|
51
|
+
|| message.includes("fetch first")
|
|
52
|
+
|| message.includes("tip of your current branch is behind")
|
|
53
|
+
|| message.includes("updates were rejected because the tip of your current branch is behind")
|
|
54
|
+
|| message.includes("stale info");
|
|
55
|
+
}
|
|
56
|
+
function errorMessage(err) {
|
|
57
|
+
return err instanceof Error ? err.message : String(err);
|
|
58
|
+
}
|
|
59
|
+
const defaultProviderToolHeartbeatIntervalMs = 60_000;
|
|
60
|
+
class ProviderToolActivityHeartbeat {
|
|
61
|
+
outputState;
|
|
62
|
+
emit;
|
|
63
|
+
intervalMs;
|
|
64
|
+
timer = null;
|
|
65
|
+
constructor(outputState, emit, intervalMs) {
|
|
66
|
+
this.outputState = outputState;
|
|
67
|
+
this.emit = emit;
|
|
68
|
+
this.intervalMs = intervalMs;
|
|
69
|
+
}
|
|
70
|
+
async consume(line) {
|
|
71
|
+
const events = this.outputState.consume(line);
|
|
72
|
+
this.sync();
|
|
73
|
+
for (const event of events) {
|
|
74
|
+
await this.emit(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
stop() {
|
|
78
|
+
if (this.timer) {
|
|
79
|
+
clearInterval(this.timer);
|
|
80
|
+
this.timer = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
sync() {
|
|
84
|
+
if (!this.outputState.hasActiveToolCall) {
|
|
85
|
+
this.stop();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (this.timer) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.timer = setInterval(() => {
|
|
92
|
+
if (!this.outputState.hasActiveToolCall) {
|
|
93
|
+
this.stop();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
void this.emit({
|
|
97
|
+
role: "runner",
|
|
98
|
+
kind: "status",
|
|
99
|
+
content: "A supervised provider tool is still running.",
|
|
100
|
+
eventType: "provider_tool_heartbeat",
|
|
101
|
+
}).catch(() => undefined);
|
|
102
|
+
}, Math.max(1, this.intervalMs));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
17
105
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
18
106
|
export class RoutingProviderCliService {
|
|
19
107
|
realProvider;
|
|
@@ -32,6 +120,11 @@ export class RoutingProviderCliService {
|
|
|
32
120
|
? await this.deterministicProvider.runArchitect(context)
|
|
33
121
|
: await this.realProvider.runArchitect(context);
|
|
34
122
|
}
|
|
123
|
+
async runCiRepair(context) {
|
|
124
|
+
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
125
|
+
? await this.deterministicProvider.runCiRepair(context)
|
|
126
|
+
: await this.realProvider.runCiRepair(context);
|
|
127
|
+
}
|
|
35
128
|
async runConflictResolve(context) {
|
|
36
129
|
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
37
130
|
? await this.deterministicProvider.runConflictResolve(context)
|
|
@@ -51,34 +144,36 @@ export class RoutingProviderCliService {
|
|
|
51
144
|
export class ProcessBackedProviderCliService {
|
|
52
145
|
processRunner;
|
|
53
146
|
git;
|
|
54
|
-
|
|
147
|
+
providerToolHeartbeatIntervalMs;
|
|
148
|
+
constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs) {
|
|
55
149
|
this.processRunner = processRunner;
|
|
56
150
|
this.git = git;
|
|
151
|
+
this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
|
|
57
152
|
}
|
|
58
153
|
async runDraft(context) {
|
|
59
154
|
const { dispatch } = context;
|
|
60
155
|
if (!dispatch.draft) {
|
|
61
156
|
throw new Error("Draft dispatch context is missing.");
|
|
62
157
|
}
|
|
63
|
-
const
|
|
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);
|
|
158
|
+
const { worktree } = context;
|
|
159
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
72
160
|
await fs.mkdir(metadataRoot, { recursive: true });
|
|
73
|
-
await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(dispatch
|
|
161
|
+
await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(durableDraftInput(dispatch), null, 2) + "\n", "utf8");
|
|
74
162
|
await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
|
|
163
|
+
const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
|
|
75
164
|
const decomposeDraft = isDecomposeDraftDispatch(dispatch);
|
|
76
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree
|
|
165
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
|
|
166
|
+
const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
|
|
167
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
|
|
168
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
77
169
|
const command = buildProviderCliCommand(dispatch, {
|
|
78
|
-
worktreePath:
|
|
79
|
-
prompt:
|
|
170
|
+
worktreePath: worktree.worktreePath,
|
|
171
|
+
prompt: draftPrompt,
|
|
172
|
+
promptFromStdin: pipePromptToStdin,
|
|
80
173
|
completionTools: completionTools ?? undefined,
|
|
81
|
-
skipGitRepoCheck:
|
|
174
|
+
skipGitRepoCheck: false,
|
|
175
|
+
imagePaths: stagedAttachments.imagePaths,
|
|
176
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
82
177
|
});
|
|
83
178
|
const draftArtifactSlugs = dispatch.artifacts
|
|
84
179
|
.filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
|
|
@@ -92,8 +187,8 @@ export class ProcessBackedProviderCliService {
|
|
|
92
187
|
await context.emit({
|
|
93
188
|
role: "runner",
|
|
94
189
|
kind: "checkpoint",
|
|
95
|
-
content: `Starting provider CLI Draft: ${
|
|
96
|
-
eventType: "
|
|
190
|
+
content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
|
|
191
|
+
eventType: "provider_cli_starting"
|
|
97
192
|
});
|
|
98
193
|
await context.emit({
|
|
99
194
|
role: "runner",
|
|
@@ -104,6 +199,7 @@ export class ProcessBackedProviderCliService {
|
|
|
104
199
|
eventType: "completion_tools_configured"
|
|
105
200
|
});
|
|
106
201
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
202
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
107
203
|
try {
|
|
108
204
|
const providerCancellation = new AbortController();
|
|
109
205
|
const abortProvider = () => providerCancellation.abort();
|
|
@@ -114,16 +210,23 @@ export class ProcessBackedProviderCliService {
|
|
|
114
210
|
context.signal?.addEventListener("abort", abortProvider, { once: true });
|
|
115
211
|
}
|
|
116
212
|
try {
|
|
117
|
-
const runPromise = this.processRunner.run({
|
|
213
|
+
const runPromise = (context.processRunner ?? this.processRunner).run({
|
|
118
214
|
command: command.command,
|
|
119
215
|
args: command.args,
|
|
120
|
-
cwd:
|
|
121
|
-
env: providerProcessEnv(dispatch)
|
|
216
|
+
cwd: worktree.worktreePath,
|
|
217
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
218
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
122
219
|
}, {
|
|
220
|
+
onStarted: async () => {
|
|
221
|
+
await context.emit({
|
|
222
|
+
role: "runner",
|
|
223
|
+
kind: "checkpoint",
|
|
224
|
+
content: "Provider CLI Draft is supervised and ready.",
|
|
225
|
+
eventType: "provider_cli_started"
|
|
226
|
+
});
|
|
227
|
+
},
|
|
123
228
|
onStdoutLine: async (line) => {
|
|
124
|
-
|
|
125
|
-
await context.emit(event);
|
|
126
|
-
}
|
|
229
|
+
await toolHeartbeat.consume(line);
|
|
127
230
|
}
|
|
128
231
|
}, providerCancellation.signal);
|
|
129
232
|
const artifactWaitCancellation = new AbortController();
|
|
@@ -134,7 +237,7 @@ export class ProcessBackedProviderCliService {
|
|
|
134
237
|
else {
|
|
135
238
|
context.signal?.addEventListener("abort", abortArtifactWait, { once: true });
|
|
136
239
|
}
|
|
137
|
-
const artifactWaitPromise = waitForCompletionArtifact(
|
|
240
|
+
const artifactWaitPromise = waitForCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, decomposeDraft ? "decompose" : "draft", artifactWaitCancellation.signal);
|
|
138
241
|
let result;
|
|
139
242
|
let earlyProposalArtifact = null;
|
|
140
243
|
try {
|
|
@@ -168,8 +271,8 @@ export class ProcessBackedProviderCliService {
|
|
|
168
271
|
});
|
|
169
272
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
170
273
|
if (decomposeDraft) {
|
|
171
|
-
await emitDecomposeGateArtifacts(context.emit,
|
|
172
|
-
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(
|
|
274
|
+
await emitDecomposeGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
|
|
275
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "decompose");
|
|
173
276
|
if (!proposalArtifact) {
|
|
174
277
|
throw new Error("Provider CLI Decompose completed but did not call forge.complete_decompose_proposal.");
|
|
175
278
|
}
|
|
@@ -191,8 +294,8 @@ export class ProcessBackedProviderCliService {
|
|
|
191
294
|
artifacts: [proposalArtifact]
|
|
192
295
|
};
|
|
193
296
|
}
|
|
194
|
-
await emitDraftGateArtifacts(context.emit,
|
|
195
|
-
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(
|
|
297
|
+
await emitDraftGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
|
|
298
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "draft");
|
|
196
299
|
if (!proposalArtifact) {
|
|
197
300
|
throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
|
|
198
301
|
}
|
|
@@ -219,8 +322,8 @@ export class ProcessBackedProviderCliService {
|
|
|
219
322
|
}
|
|
220
323
|
}
|
|
221
324
|
finally {
|
|
325
|
+
toolHeartbeat.stop();
|
|
222
326
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
223
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
224
327
|
}
|
|
225
328
|
}
|
|
226
329
|
async runImplementation(context) {
|
|
@@ -238,21 +341,26 @@ export class ProcessBackedProviderCliService {
|
|
|
238
341
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
239
342
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
240
343
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
241
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
|
|
344
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase, runtimeEnvironment: context.runtimeEnvironment });
|
|
345
|
+
const implementationPrompt = debugCycle
|
|
346
|
+
? buildDebugCyclePrompt(dispatch, phase)
|
|
347
|
+
: developCycle
|
|
348
|
+
? buildDevelopCyclePrompt(dispatch, phase)
|
|
349
|
+
: buildImplementationPrompt(dispatch, phase);
|
|
350
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, implementationPrompt, context.runtimeEnvironment);
|
|
351
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
242
352
|
const command = buildProviderCliCommand(dispatch, {
|
|
243
353
|
worktreePath: worktree.worktreePath,
|
|
244
|
-
prompt:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
: buildImplementationPrompt(dispatch, phase),
|
|
249
|
-
completionTools
|
|
354
|
+
prompt: implementationPrompt,
|
|
355
|
+
promptFromStdin: pipePromptToStdin,
|
|
356
|
+
completionTools,
|
|
357
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
250
358
|
});
|
|
251
359
|
await context.emit({
|
|
252
360
|
role: "runner",
|
|
253
361
|
kind: "checkpoint",
|
|
254
|
-
content: `Starting provider CLI: ${
|
|
255
|
-
eventType: "
|
|
362
|
+
content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
|
|
363
|
+
eventType: "provider_cli_starting"
|
|
256
364
|
});
|
|
257
365
|
await context.emit({
|
|
258
366
|
role: "runner",
|
|
@@ -265,24 +373,33 @@ export class ProcessBackedProviderCliService {
|
|
|
265
373
|
eventType: "completion_tools_configured"
|
|
266
374
|
});
|
|
267
375
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
376
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
268
377
|
const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
|
|
269
378
|
let result;
|
|
270
379
|
try {
|
|
271
380
|
liveCompletionEvents.start();
|
|
272
|
-
result = await this.processRunner.run({
|
|
381
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
273
382
|
command: command.command,
|
|
274
383
|
args: command.args,
|
|
275
384
|
cwd: worktree.worktreePath,
|
|
276
|
-
env: providerProcessEnv(dispatch)
|
|
385
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
386
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
277
387
|
}, {
|
|
388
|
+
onStarted: async () => {
|
|
389
|
+
await context.emit({
|
|
390
|
+
role: "runner",
|
|
391
|
+
kind: "checkpoint",
|
|
392
|
+
content: "Provider CLI is supervised and ready.",
|
|
393
|
+
eventType: "provider_cli_started"
|
|
394
|
+
});
|
|
395
|
+
},
|
|
278
396
|
onStdoutLine: async (line) => {
|
|
279
|
-
|
|
280
|
-
await context.emit(event);
|
|
281
|
-
}
|
|
397
|
+
await toolHeartbeat.consume(line);
|
|
282
398
|
}
|
|
283
399
|
}, context.signal);
|
|
284
400
|
}
|
|
285
401
|
finally {
|
|
402
|
+
toolHeartbeat.stop();
|
|
286
403
|
await liveCompletionEvents.stop();
|
|
287
404
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
288
405
|
}
|
|
@@ -298,8 +415,8 @@ export class ProcessBackedProviderCliService {
|
|
|
298
415
|
});
|
|
299
416
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
300
417
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
301
|
-
if (developCycle || debugCycle || phase === "implementation") {
|
|
302
|
-
await this.git
|
|
418
|
+
if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
|
|
419
|
+
await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
|
|
303
420
|
await context.emit({
|
|
304
421
|
role: "runner",
|
|
305
422
|
kind: "checkpoint",
|
|
@@ -357,18 +474,23 @@ export class ProcessBackedProviderCliService {
|
|
|
357
474
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
358
475
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
359
476
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
360
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
|
|
477
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect", runtimeEnvironment: context.runtimeEnvironment });
|
|
478
|
+
const architectPrompt = buildArchitectPrompt(dispatch);
|
|
479
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, architectPrompt, context.runtimeEnvironment);
|
|
480
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
361
481
|
const command = buildProviderCliCommand(dispatch, {
|
|
362
482
|
worktreePath: worktree.worktreePath,
|
|
363
|
-
prompt:
|
|
364
|
-
|
|
483
|
+
prompt: architectPrompt,
|
|
484
|
+
promptFromStdin: pipePromptToStdin,
|
|
485
|
+
completionTools,
|
|
486
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
365
487
|
});
|
|
366
|
-
const commandText =
|
|
488
|
+
const commandText = formatProviderCliCommandForCheckpoint(command);
|
|
367
489
|
await context.emit({
|
|
368
490
|
role: "runner",
|
|
369
491
|
kind: "checkpoint",
|
|
370
492
|
content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
|
|
371
|
-
eventType: "
|
|
493
|
+
eventType: "provider_cli_starting"
|
|
372
494
|
});
|
|
373
495
|
await context.emit({
|
|
374
496
|
role: "runner",
|
|
@@ -377,24 +499,31 @@ export class ProcessBackedProviderCliService {
|
|
|
377
499
|
eventType: "completion_tools_configured"
|
|
378
500
|
});
|
|
379
501
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
502
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
380
503
|
const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
|
|
381
504
|
let result;
|
|
382
505
|
try {
|
|
383
506
|
liveArchitectEvents.start();
|
|
384
|
-
result = await this.processRunner.run({
|
|
507
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
385
508
|
command: command.command,
|
|
386
509
|
args: command.args,
|
|
387
510
|
cwd: worktree.worktreePath,
|
|
388
|
-
env: providerProcessEnv(dispatch)
|
|
511
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
512
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
389
513
|
}, {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
514
|
+
onStarted: async () => {
|
|
515
|
+
await context.emit({
|
|
516
|
+
role: "runner",
|
|
517
|
+
kind: "checkpoint",
|
|
518
|
+
content: "Provider CLI Architect is supervised and ready.",
|
|
519
|
+
eventType: "provider_cli_started"
|
|
520
|
+
});
|
|
521
|
+
},
|
|
522
|
+
onStdoutLine: async (line) => await toolHeartbeat.consume(line)
|
|
395
523
|
}, context.signal);
|
|
396
524
|
}
|
|
397
525
|
finally {
|
|
526
|
+
toolHeartbeat.stop();
|
|
398
527
|
await liveArchitectEvents.stop();
|
|
399
528
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
400
529
|
}
|
|
@@ -448,18 +577,23 @@ export class ProcessBackedProviderCliService {
|
|
|
448
577
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
449
578
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
450
579
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
451
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
|
|
580
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa", runtimeEnvironment: context.runtimeEnvironment });
|
|
581
|
+
const qaPrompt = buildQaPrompt(dispatch, specBody);
|
|
582
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, qaPrompt, context.runtimeEnvironment);
|
|
583
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
452
584
|
const command = buildProviderCliCommand(dispatch, {
|
|
453
585
|
worktreePath: worktree.worktreePath,
|
|
454
|
-
prompt:
|
|
455
|
-
|
|
586
|
+
prompt: qaPrompt,
|
|
587
|
+
promptFromStdin: pipePromptToStdin,
|
|
588
|
+
completionTools,
|
|
589
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
456
590
|
});
|
|
457
|
-
const commandText =
|
|
591
|
+
const commandText = formatProviderCliCommandForCheckpoint(command);
|
|
458
592
|
await context.emit({
|
|
459
593
|
role: "runner",
|
|
460
594
|
kind: "checkpoint",
|
|
461
595
|
content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
|
|
462
|
-
eventType: "
|
|
596
|
+
eventType: "provider_cli_starting"
|
|
463
597
|
});
|
|
464
598
|
await context.emit({
|
|
465
599
|
role: "runner",
|
|
@@ -468,24 +602,31 @@ export class ProcessBackedProviderCliService {
|
|
|
468
602
|
eventType: "completion_tools_configured"
|
|
469
603
|
});
|
|
470
604
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
605
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
471
606
|
const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
|
|
472
607
|
let result;
|
|
473
608
|
try {
|
|
474
609
|
liveQaEvents.start();
|
|
475
|
-
result = await this.processRunner.run({
|
|
610
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
476
611
|
command: command.command,
|
|
477
612
|
args: command.args,
|
|
478
613
|
cwd: worktree.worktreePath,
|
|
479
|
-
env: providerProcessEnv(dispatch)
|
|
614
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
615
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
480
616
|
}, {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
617
|
+
onStarted: async () => {
|
|
618
|
+
await context.emit({
|
|
619
|
+
role: "runner",
|
|
620
|
+
kind: "checkpoint",
|
|
621
|
+
content: "Provider CLI QA is supervised and ready.",
|
|
622
|
+
eventType: "provider_cli_started"
|
|
623
|
+
});
|
|
624
|
+
},
|
|
625
|
+
onStdoutLine: async (line) => await toolHeartbeat.consume(line)
|
|
486
626
|
}, context.signal);
|
|
487
627
|
}
|
|
488
628
|
finally {
|
|
629
|
+
toolHeartbeat.stop();
|
|
489
630
|
await liveQaEvents.stop();
|
|
490
631
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
491
632
|
}
|
|
@@ -507,7 +648,7 @@ export class ProcessBackedProviderCliService {
|
|
|
507
648
|
? "Provider CLI QA completed but did not produce the complete durable QA artifact bundle."
|
|
508
649
|
: "Provider CLI QA completed but did not call the Forge QA completion tool.");
|
|
509
650
|
}
|
|
510
|
-
enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
|
|
651
|
+
await enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
|
|
511
652
|
if (!liveQaEvents.hasEmitted()) {
|
|
512
653
|
await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
|
|
513
654
|
await emitQaStructuredEvents(context.emit, qaArtifact);
|
|
@@ -531,56 +672,216 @@ export class ProcessBackedProviderCliService {
|
|
|
531
672
|
artifacts: [qaArtifact]
|
|
532
673
|
};
|
|
533
674
|
}
|
|
675
|
+
async runCiRepair(context) {
|
|
676
|
+
return await this.runPullRequestRepair(context, "ci_repair");
|
|
677
|
+
}
|
|
534
678
|
async runConflictResolve(context) {
|
|
679
|
+
return await this.runPullRequestRepair(context, "conflict_resolve");
|
|
680
|
+
}
|
|
681
|
+
async runPullRequestRepair(context, route) {
|
|
535
682
|
const { dispatch, worktree } = context;
|
|
536
|
-
const
|
|
683
|
+
const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
|
|
684
|
+
const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
|
|
685
|
+
let repairSpec = null;
|
|
686
|
+
let repairEnvironment = context.runtimeEnvironment;
|
|
687
|
+
let completionTools = null;
|
|
688
|
+
if (repairChecks) {
|
|
689
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
690
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
691
|
+
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
692
|
+
if (!laneCommand) {
|
|
693
|
+
throw new Error(`CI repair readiness failed: lane command for '${dispatch.execution.lane}' was not resolved, so the approved spec cannot be materialized.`);
|
|
694
|
+
}
|
|
695
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
|
|
696
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
697
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
698
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
|
|
699
|
+
repairSpec = {
|
|
700
|
+
repoPath: worktree.worktreePath,
|
|
701
|
+
path: specPath,
|
|
702
|
+
body: specBody,
|
|
703
|
+
producedBy: "forge-lane-command",
|
|
704
|
+
status: "approved",
|
|
705
|
+
};
|
|
706
|
+
const readiness = await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
|
|
707
|
+
repairEnvironment = readiness.environment;
|
|
708
|
+
completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "ci_repair", runtimeEnvironment: repairEnvironment });
|
|
709
|
+
await context.emit({
|
|
710
|
+
role: "runner",
|
|
711
|
+
kind: "checkpoint",
|
|
712
|
+
content: `CI repair readiness contract prepared at ${path.relative(worktree.worktreePath, readiness.path).replaceAll(path.sep, "/")}; approved spec, attempt URLs, browser readiness, and local-equivalence policy are available before provider start.`,
|
|
713
|
+
eventType: "ci_repair_readiness_ready",
|
|
714
|
+
});
|
|
715
|
+
await context.emit({
|
|
716
|
+
role: "runner",
|
|
717
|
+
kind: "checkpoint",
|
|
718
|
+
content: "Forge CI repair completion tool configured; provider must call forge.complete_ci_repair before Bridge-owned commit and push.",
|
|
719
|
+
eventType: "completion_tools_configured",
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
const repairPrompt = repairChecks
|
|
723
|
+
? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
|
|
724
|
+
: addressComments
|
|
725
|
+
? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
|
|
726
|
+
: buildConflictResolvePrompt(dispatch, worktree.branchName);
|
|
727
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, repairPrompt, repairEnvironment);
|
|
728
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
537
729
|
const command = buildProviderCliCommand(dispatch, {
|
|
538
730
|
worktreePath: worktree.worktreePath,
|
|
539
|
-
prompt:
|
|
540
|
-
|
|
541
|
-
|
|
731
|
+
prompt: repairPrompt,
|
|
732
|
+
promptFromStdin: pipePromptToStdin,
|
|
733
|
+
completionTools: completionTools ?? undefined,
|
|
734
|
+
runtimeEnvironment: repairEnvironment
|
|
542
735
|
});
|
|
543
736
|
await context.emit({
|
|
544
737
|
role: "runner",
|
|
545
738
|
kind: "checkpoint",
|
|
546
|
-
content: `Starting provider CLI ${addressComments ? "PR comment address" : "conflict resolve"}: ${
|
|
547
|
-
eventType: "
|
|
739
|
+
content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
|
|
740
|
+
eventType: "provider_cli_starting"
|
|
548
741
|
});
|
|
549
742
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
743
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
744
|
+
let result;
|
|
745
|
+
try {
|
|
746
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
747
|
+
command: command.command,
|
|
748
|
+
args: command.args,
|
|
749
|
+
cwd: worktree.worktreePath,
|
|
750
|
+
env: providerProcessEnv(dispatch, repairEnvironment),
|
|
751
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
752
|
+
}, {
|
|
753
|
+
onStarted: async () => {
|
|
754
|
+
await context.emit({
|
|
755
|
+
role: "runner",
|
|
756
|
+
kind: "checkpoint",
|
|
757
|
+
content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} is supervised and ready.`,
|
|
758
|
+
eventType: "provider_cli_started"
|
|
759
|
+
});
|
|
760
|
+
},
|
|
761
|
+
onStdoutLine: async (line) => {
|
|
762
|
+
await toolHeartbeat.consume(line);
|
|
559
763
|
}
|
|
764
|
+
}, context.signal);
|
|
765
|
+
}
|
|
766
|
+
finally {
|
|
767
|
+
toolHeartbeat.stop();
|
|
768
|
+
if (completionTools) {
|
|
769
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
560
770
|
}
|
|
561
|
-
}
|
|
771
|
+
}
|
|
562
772
|
if (result.exitCode !== 0) {
|
|
563
773
|
const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
|
|
564
774
|
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
565
775
|
}
|
|
776
|
+
let repairArtifact = null;
|
|
777
|
+
if (repairChecks) {
|
|
778
|
+
repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
|
|
779
|
+
if (!repairArtifact) {
|
|
780
|
+
throw new Error("Provider CLI completed but did not call forge.complete_ci_repair. Forge did not commit or push the repair.");
|
|
781
|
+
}
|
|
782
|
+
if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
|
|
783
|
+
throw new Error(`CI repair completion gate recorded ${repairArtifact.status}; Forge did not commit or push the repair.`);
|
|
784
|
+
}
|
|
785
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
|
|
786
|
+
await emitPhaseCompletionEvent(context.emit, repairArtifact);
|
|
787
|
+
await emitPhaseGateEvents(context.emit, repairArtifact);
|
|
788
|
+
}
|
|
566
789
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
567
|
-
|
|
790
|
+
if (providerOwnsBranchPublication(context)) {
|
|
791
|
+
await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
|
|
792
|
+
}
|
|
568
793
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
569
794
|
await context.emit({
|
|
570
795
|
role: "assistant",
|
|
571
796
|
kind: "message",
|
|
572
|
-
content: outputState.finalMessage || (
|
|
573
|
-
? `${dispatch.execution.provider} CLI
|
|
574
|
-
:
|
|
575
|
-
|
|
797
|
+
content: outputState.finalMessage || (repairChecks
|
|
798
|
+
? `${dispatch.execution.provider} CLI repaired pull request checks for ${worktree.branchName}.`
|
|
799
|
+
: addressComments
|
|
800
|
+
? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
|
|
801
|
+
: `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
|
|
802
|
+
eventType: repairChecks ? "pr_checks_complete" : addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
|
|
576
803
|
});
|
|
577
804
|
return {
|
|
578
|
-
summary:
|
|
579
|
-
? `${dispatch.execution.provider} CLI
|
|
580
|
-
:
|
|
805
|
+
summary: repairChecks
|
|
806
|
+
? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
|
|
807
|
+
: addressComments
|
|
808
|
+
? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
|
|
809
|
+
: `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`,
|
|
810
|
+
spec: repairSpec,
|
|
811
|
+
artifact: repairArtifact,
|
|
812
|
+
artifacts: repairArtifact ? [repairArtifact] : [],
|
|
581
813
|
};
|
|
582
814
|
}
|
|
583
815
|
}
|
|
816
|
+
async function stageDraftAttachments(dispatch, metadataRoot) {
|
|
817
|
+
const attachments = dispatch.draft?.attachments ?? [];
|
|
818
|
+
const stageable = attachments.filter((attachment) => (attachment.kind === "text" && typeof attachment.textContent === "string")
|
|
819
|
+
|| (attachment.kind === "image" && typeof attachment.contentBase64 === "string"));
|
|
820
|
+
if (stageable.length === 0) {
|
|
821
|
+
return { textReferences: [], imagePaths: [] };
|
|
822
|
+
}
|
|
823
|
+
const attachmentRoot = path.join(metadataRoot, "draft-attachments");
|
|
824
|
+
const worktreeRoot = path.resolve(metadataRoot, "../../..");
|
|
825
|
+
await fs.mkdir(attachmentRoot, { recursive: true });
|
|
826
|
+
const textReferences = [];
|
|
827
|
+
const imagePaths = [];
|
|
828
|
+
for (const attachment of stageable) {
|
|
829
|
+
const fileName = `${String(attachment.index + 1).padStart(2, "0")}-${safeAttachmentFileName(attachment.fileName)}`;
|
|
830
|
+
const absolutePath = path.join(attachmentRoot, fileName);
|
|
831
|
+
if (attachment.kind === "text") {
|
|
832
|
+
await fs.writeFile(absolutePath, attachment.textContent ?? "", { encoding: "utf8", mode: 0o600 });
|
|
833
|
+
textReferences.push({ index: attachment.index, path: path.relative(worktreeRoot, absolutePath).replaceAll(path.sep, "/") });
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const imageContent = decodeDraftImageAttachment(attachment.contentBase64 ?? "", attachment.fileName, attachment.index);
|
|
837
|
+
await fs.writeFile(absolutePath, imageContent, { mode: 0o600 });
|
|
838
|
+
imagePaths.push(absolutePath);
|
|
839
|
+
}
|
|
840
|
+
return { textReferences, imagePaths };
|
|
841
|
+
}
|
|
842
|
+
function decodeDraftImageAttachment(contentBase64, fileName, index) {
|
|
843
|
+
const normalized = contentBase64.replace(/\s+/gu, "");
|
|
844
|
+
if (normalized.length === 0 || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(normalized)) {
|
|
845
|
+
throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
|
|
846
|
+
}
|
|
847
|
+
const decoded = Buffer.from(normalized, "base64");
|
|
848
|
+
if (decoded.length === 0 || decoded.toString("base64") !== normalized) {
|
|
849
|
+
throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
|
|
850
|
+
}
|
|
851
|
+
return decoded;
|
|
852
|
+
}
|
|
853
|
+
function safeAttachmentFileName(fileName) {
|
|
854
|
+
const sanitized = path.basename(fileName || "attachment.txt")
|
|
855
|
+
.replace(/[^a-zA-Z0-9._-]/gu, "_")
|
|
856
|
+
.replace(/[. ]+$/gu, "") || "attachment.txt";
|
|
857
|
+
const maxFileNameLength = 120;
|
|
858
|
+
const clipped = clipFileName(sanitized, maxFileNameLength);
|
|
859
|
+
const extension = path.extname(clipped);
|
|
860
|
+
const stem = path.basename(clipped, extension).replace(/[. ]+$/gu, "") || "attachment";
|
|
861
|
+
const windowsReservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
|
|
862
|
+
const safeStem = stem === "." || stem === ".." || windowsReservedName.test(stem) ? `attachment-${stem.replaceAll(".", "_")}` : stem;
|
|
863
|
+
return clipFileName(`${safeStem}${extension}`, maxFileNameLength);
|
|
864
|
+
}
|
|
865
|
+
function clipFileName(fileName, maxLength) {
|
|
866
|
+
if (fileName.length <= maxLength) {
|
|
867
|
+
return fileName;
|
|
868
|
+
}
|
|
869
|
+
const originalExtension = path.extname(fileName);
|
|
870
|
+
const extension = originalExtension.length > maxLength - 1
|
|
871
|
+
? originalExtension.slice(0, maxLength - 1)
|
|
872
|
+
: originalExtension;
|
|
873
|
+
const stem = path.basename(fileName, originalExtension);
|
|
874
|
+
const maxStemLength = Math.max(1, maxLength - extension.length);
|
|
875
|
+
return `${stem.slice(0, maxStemLength)}${extension}`;
|
|
876
|
+
}
|
|
877
|
+
const providerPromptStdinThreshold = 8_000;
|
|
878
|
+
function shouldPipePromptToStdin(provider, prompt, force = false) {
|
|
879
|
+
return provider.trim().toLowerCase() === "codex"
|
|
880
|
+
&& (force || Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold);
|
|
881
|
+
}
|
|
882
|
+
function usesManagedWindowsHost(dispatch) {
|
|
883
|
+
return process.platform === "win32" && dispatch.sandbox?.executionMode === "host";
|
|
884
|
+
}
|
|
584
885
|
function isFakeWorkerModel(model) {
|
|
585
886
|
return model.trim().toLowerCase() === fakeWorkerModel;
|
|
586
887
|
}
|