@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.e7b62d8fdd33
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 +83 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +55 -3
- package/dist/bridge/bridge.service.js +834 -87
- package/dist/bridge/bridge.types.d.ts +89 -1
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +14 -2
- package/dist/cleanup/cleanup.service.js +496 -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 +37 -3
- package/dist/cli/cli.service.d.ts +33 -2
- package/dist/cli/cli.service.js +209 -27
- package/dist/cli/local-config.d.ts +5 -0
- package/dist/cli/local-config.js +45 -1
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +340 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +105 -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 +97 -0
- package/dist/runtime/attempt-process-supervisor.js +386 -0
- package/dist/runtime/docker-engine-probe.d.ts +83 -0
- package/dist/runtime/docker-engine-probe.js +300 -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 +46 -0
- package/dist/runtime/host-listener-inspector.js +349 -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 +197 -0
- package/dist/runtime/host-runtime-lease.js +2203 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/memory-capacity.d.ts +68 -0
- package/dist/runtime/memory-capacity.js +121 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +54 -0
- package/dist/runtime/node-host-runtime-adapter.js +284 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +21 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +231 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +45 -1
- package/dist/sandbox/preflight.js +299 -34
- package/dist/sandbox/process.js +125 -40
- package/dist/sandbox/repository-lock.js +41 -3
- package/dist/sandbox/runtime.d.ts +66 -3
- package/dist/sandbox/runtime.js +670 -105
- package/dist/sandbox/sandbox.types.d.ts +10 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.js +1 -1
- package/dist/sandbox/worker.js +2 -5
- package/dist/sandbox/workspace.service.d.ts +1 -0
- package/dist/sandbox/workspace.service.js +28 -5
- package/dist/session/managed-attempt-tools.d.ts +21 -0
- package/dist/session/managed-attempt-tools.js +912 -0
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +105 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +335 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +233 -24
- package/dist/session/phase-completion.d.ts +12 -2
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +51 -10
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +24 -8
- package/dist/session/phase-completion.runtime.d.ts +13 -1
- package/dist/session/phase-completion.runtime.js +189 -8
- package/dist/session/phase-completion.schemas.js +117 -4
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +153 -22
- package/dist/session/pre-publication-validation.d.ts +46 -0
- package/dist/session/pre-publication-validation.js +208 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +16 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +110 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +3 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +65 -7
- package/dist/session/provider-cli/provider-cli.deterministic.js +49 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +25 -0
- package/dist/session/provider-cli/provider-cli.process.js +463 -34
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.prompts.js +108 -25
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +6 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +22 -2
- package/dist/session/provider-cli/provider-cli.service.js +524 -100
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +91 -16
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +38 -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 +522 -41
- package/dist/session/session.types.d.ts +72 -0
- package/dist/session/session.types.js +74 -3
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +21 -1
- package/dist/shared/process/dotnet-toolchain.d.ts +14 -0
- package/dist/shared/process/dotnet-toolchain.js +179 -0
- 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 +59 -0
- package/dist/shared/process/host-process-supervisor.js +330 -0
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/redaction/redaction.js +12 -2
- 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 +28 -1
- package/dist/worktree/dependency-warmer.js +308 -9
- package/dist/worktree/index.d.ts +2 -1
- 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 +13 -2
- package/dist/worktree/worktree.service.js +238 -47
- package/dist/worktree/worktree.types.d.ts +5 -1
- package/package.json +5 -3
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -1,42 +1,85 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { RepositoryCoordinatorLock } from "../../sandbox/repository-lock.js";
|
|
3
4
|
import { GitClient } from "../../shared/git/git.js";
|
|
4
5
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
6
|
+
import { defaultWorktreeCoordinatorStateRoot } from "../../worktree/worktree-paths.js";
|
|
5
7
|
import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
|
|
6
|
-
import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle,
|
|
8
|
+
import { cleanupCompletionTools, commitWorktreeChanges, completionPhaseFromArtifact, emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitDecomposeGateArtifacts, emitDraftGateArtifacts, emitPhaseCompletionEvent, emitPhaseGateEvents, emitQaArtifactRecorded, emitQaStructuredEvents, enforceQaBrowserLaunchTargetGate, LiveArchitectArtifactEmitter, LiveCompletionArtifactEmitter, LiveQaArtifactEmitter, readCompletionArtifactsForRun, readQaCompletionArtifactBundle, waitForCompletionArtifact } from "./provider-cli.artifacts.js";
|
|
7
9
|
import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
|
|
8
|
-
import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
|
|
10
|
+
import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
|
|
9
11
|
import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
10
12
|
import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
|
|
11
13
|
import { clip, createProviderOutputState, parseJsonLine } from "./provider-output-state.js";
|
|
14
|
+
import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
|
|
15
|
+
import { assertRepairHeadEpoch } from "./provider-cli.repair-epoch.js";
|
|
16
|
+
import { PrePublicationValidator, } from "../pre-publication-validation.js";
|
|
12
17
|
export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
|
|
13
18
|
export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
|
|
14
|
-
export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
19
|
+
export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, PROVIDER_OUTPUT_CAPTURE_LIMIT_CHARS, StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
15
20
|
export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
|
|
16
|
-
async function pushWorktreeBranch(git, worktreePath, branchName, emit) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
async function pushWorktreeBranch(git, worktreePath, branchName, repoPath, coordinator, signal, emit, expectedRemoteHeadSha) {
|
|
22
|
+
await coordinator.withLock(repoPath, async () => {
|
|
23
|
+
if (expectedRemoteHeadSha) {
|
|
24
|
+
const expected = expectedRemoteHeadSha.trim().toLowerCase();
|
|
25
|
+
const remoteListing = await git.run(worktreePath, ["ls-remote", "--heads", "origin", `refs/heads/${branchName}`]);
|
|
26
|
+
const observed = remoteListing.trim().split(/\s+/u)[0]?.toLowerCase() ?? "";
|
|
27
|
+
if (observed !== expected) {
|
|
28
|
+
throw new Error(`stale_head: repair publication expected remote ${branchName} at ${expected}, but observed ${observed || "no branch"}; refresh provider state before retrying.`);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await git.run(worktreePath, [
|
|
32
|
+
"push",
|
|
33
|
+
"--porcelain",
|
|
34
|
+
"origin",
|
|
35
|
+
`HEAD:refs/heads/${branchName}`,
|
|
36
|
+
`--force-with-lease=refs/heads/${branchName}:${expected}`,
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`stale_head: repair publication for ${branchName} lost its expected-head lease; Forge did not rebase or publish the stale repair: ${errorMessage(error)}`, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
24
43
|
}
|
|
25
|
-
await emit?.({
|
|
26
|
-
role: "runner",
|
|
27
|
-
kind: "checkpoint",
|
|
28
|
-
content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
|
|
29
|
-
eventType: "worktree_branch_push_rebase"
|
|
30
|
-
});
|
|
31
44
|
try {
|
|
32
|
-
await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
|
|
33
|
-
await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
|
|
34
45
|
await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
|
|
46
|
+
return;
|
|
35
47
|
}
|
|
36
|
-
catch (
|
|
37
|
-
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (!isNonFastForwardPushError(err)) {
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
await emit?.({
|
|
53
|
+
role: "runner",
|
|
54
|
+
kind: "checkpoint",
|
|
55
|
+
content: `Remote branch ${branchName} moved before push; fetching latest and rebasing local changes.`,
|
|
56
|
+
eventType: "worktree_branch_push_rebase"
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
await git.run(worktreePath, ["fetch", "origin", remoteTrackingRefspec(branchName)]);
|
|
60
|
+
await git.run(worktreePath, ["rebase", `refs/remotes/origin/${branchName}`]);
|
|
61
|
+
await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
|
|
62
|
+
}
|
|
63
|
+
catch (retryErr) {
|
|
64
|
+
throw new Error(`worktree_branch_integration_conflict: git push origin HEAD:${branchName} was rejected because the remote branch moved, and automatic fetch/rebase failed: ${errorMessage(retryErr)}`, { cause: retryErr });
|
|
65
|
+
}
|
|
38
66
|
}
|
|
67
|
+
}, signal);
|
|
68
|
+
}
|
|
69
|
+
function providerOwnsBranchPublication(context) {
|
|
70
|
+
return context.dispatch.sandbox?.executionMode !== "docker";
|
|
71
|
+
}
|
|
72
|
+
function prePublicationExecution(context, environment, defaultRunner) {
|
|
73
|
+
if (context.processRunner)
|
|
74
|
+
return { environment, processRunner: context.processRunner };
|
|
75
|
+
if (context.dispatch.sandbox?.executionMode !== "docker")
|
|
76
|
+
return { environment };
|
|
77
|
+
const expectedSandboxId = context.dispatch.sandbox.dispatchAttemptId;
|
|
78
|
+
const observedSandboxId = context.runtimeEnvironment?.FORGE_SANDBOX_ID ?? process.env.FORGE_SANDBOX_ID;
|
|
79
|
+
if (observedSandboxId !== expectedSandboxId) {
|
|
80
|
+
throw new Error(`prepublication_execution_scope_invalid: Docker attempt ${expectedSandboxId} has no runner proven to execute inside that sandbox.`);
|
|
39
81
|
}
|
|
82
|
+
return { environment, processRunner: defaultRunner };
|
|
40
83
|
}
|
|
41
84
|
function remoteTrackingRefspec(branchName) {
|
|
42
85
|
return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
|
|
@@ -52,6 +95,226 @@ function isNonFastForwardPushError(err) {
|
|
|
52
95
|
function errorMessage(err) {
|
|
53
96
|
return err instanceof Error ? err.message : String(err);
|
|
54
97
|
}
|
|
98
|
+
function providerCliExitFailure(command, result) {
|
|
99
|
+
const output = [result.stderr.trim(), result.stdout.trim()]
|
|
100
|
+
.filter(value => value.length > 0)
|
|
101
|
+
.join("\n");
|
|
102
|
+
const detail = clip(output, 1200);
|
|
103
|
+
const suffix = detail ? `: ${detail}` : "";
|
|
104
|
+
if (isProviderNetworkFailure(output)) {
|
|
105
|
+
return new Error(`provider_network_unavailable: ${command} could not reach its provider service${suffix}`);
|
|
106
|
+
}
|
|
107
|
+
return new Error(`${command} exited ${result.exitCode}${suffix}`);
|
|
108
|
+
}
|
|
109
|
+
export function isProviderNetworkFailure(output) {
|
|
110
|
+
const normalized = output.trim().toLowerCase();
|
|
111
|
+
if (!normalized) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return [
|
|
115
|
+
"eai_again",
|
|
116
|
+
"enotfound",
|
|
117
|
+
"could not resolve host",
|
|
118
|
+
"failed to lookup address",
|
|
119
|
+
"name or service not known",
|
|
120
|
+
"no such host is known",
|
|
121
|
+
"temporary failure in name resolution",
|
|
122
|
+
"network is unreachable",
|
|
123
|
+
"no route to host",
|
|
124
|
+
"error sending request for url",
|
|
125
|
+
"client error (connect)",
|
|
126
|
+
"connection refused",
|
|
127
|
+
"connection reset by peer",
|
|
128
|
+
"stream disconnected before completion",
|
|
129
|
+
].some(marker => normalized.includes(marker));
|
|
130
|
+
}
|
|
131
|
+
const defaultProviderToolHeartbeatIntervalMs = 60_000;
|
|
132
|
+
const defaultManagedVerificationProgressIntervalMs = 30_000;
|
|
133
|
+
function compareManagedVerificationSnapshots(left, right) {
|
|
134
|
+
const leftOrder = managedVerificationStartedAt(left.startedAt) ?? Number.MAX_SAFE_INTEGER;
|
|
135
|
+
const rightOrder = managedVerificationStartedAt(right.startedAt) ?? Number.MAX_SAFE_INTEGER;
|
|
136
|
+
return leftOrder - rightOrder || left.commandId.localeCompare(right.commandId);
|
|
137
|
+
}
|
|
138
|
+
function managedVerificationStartedAt(value) {
|
|
139
|
+
const parsed = Date.parse(value ?? "");
|
|
140
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
141
|
+
}
|
|
142
|
+
export class ManagedVerificationProgressEmitter {
|
|
143
|
+
logDirectory;
|
|
144
|
+
emit;
|
|
145
|
+
intervalMs;
|
|
146
|
+
timer = null;
|
|
147
|
+
observedStatuses = new Map();
|
|
148
|
+
completedCommands = new Set();
|
|
149
|
+
constructor(logDirectory, emit, intervalMs = defaultManagedVerificationProgressIntervalMs) {
|
|
150
|
+
this.logDirectory = logDirectory;
|
|
151
|
+
this.emit = emit;
|
|
152
|
+
this.intervalMs = intervalMs;
|
|
153
|
+
}
|
|
154
|
+
start() {
|
|
155
|
+
if (!this.logDirectory?.trim() || this.timer)
|
|
156
|
+
return;
|
|
157
|
+
this.timer = setInterval(() => {
|
|
158
|
+
void this.scan().catch(() => undefined);
|
|
159
|
+
}, Math.max(1, this.intervalMs));
|
|
160
|
+
void this.scan().catch(() => undefined);
|
|
161
|
+
}
|
|
162
|
+
async stop() {
|
|
163
|
+
if (this.timer) {
|
|
164
|
+
clearInterval(this.timer);
|
|
165
|
+
this.timer = null;
|
|
166
|
+
}
|
|
167
|
+
await this.scan().catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
async scan() {
|
|
170
|
+
if (!this.logDirectory?.trim())
|
|
171
|
+
return;
|
|
172
|
+
const directory = path.join(this.logDirectory, "managed-commands");
|
|
173
|
+
const names = await fs.readdir(directory).catch(() => []);
|
|
174
|
+
const snapshots = await Promise.all(names
|
|
175
|
+
.filter(name => name.endsWith(".receipt.json"))
|
|
176
|
+
.map(async (name) => parseManagedVerificationSnapshot(path.join(directory, name))));
|
|
177
|
+
const current = snapshots
|
|
178
|
+
.filter((entry) => entry !== null)
|
|
179
|
+
.sort(compareManagedVerificationSnapshots);
|
|
180
|
+
if (current.some(snapshot => isManagedVerificationNonterminal(snapshot.status))) {
|
|
181
|
+
// Durable managed-command receipts are authoritative evidence that Forge
|
|
182
|
+
// still supervises work even when the provider is not inside an MCP tool
|
|
183
|
+
// call. Refresh the API idle deadline without inflating the transcript.
|
|
184
|
+
await this.emit({
|
|
185
|
+
role: "runner",
|
|
186
|
+
kind: "status",
|
|
187
|
+
content: "A supervised managed verification command is still running.",
|
|
188
|
+
eventType: "provider_tool_heartbeat",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
for (const snapshot of current) {
|
|
192
|
+
await this.emitTransition(snapshot);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async emitTransition(current) {
|
|
196
|
+
const terminal = ["pass", "fail", "timed_out", "timeout_termination_failed", "runner_error"].includes(current.status);
|
|
197
|
+
if (terminal && this.completedCommands.has(current.commandId))
|
|
198
|
+
return;
|
|
199
|
+
const previousStatus = this.observedStatuses.get(current.commandId);
|
|
200
|
+
if (previousStatus === current.status)
|
|
201
|
+
return;
|
|
202
|
+
this.observedStatuses.set(current.commandId, current.status);
|
|
203
|
+
if (terminal)
|
|
204
|
+
this.completedCommands.add(current.commandId);
|
|
205
|
+
// Starting and running are one externally meaningful state. The receipt is
|
|
206
|
+
// still refreshed on disk for UI telemetry, but repeated output/elapsed
|
|
207
|
+
// changes must not inflate the provider transcript or session history.
|
|
208
|
+
if (!terminal && previousStatus === "starting" && current.status === "running")
|
|
209
|
+
return;
|
|
210
|
+
const startedAt = managedVerificationStartedAt(current.startedAt);
|
|
211
|
+
const elapsedMs = startedAt !== null
|
|
212
|
+
? Math.max(current.elapsedMs ?? 0, Date.now() - startedAt)
|
|
213
|
+
: current.elapsedMs;
|
|
214
|
+
const elapsed = elapsedMs === null ? "an unknown duration" : formatElapsed(elapsedMs);
|
|
215
|
+
const outputTail = current.lastOutputStream === "stderr"
|
|
216
|
+
? current.stderrTail || current.stdoutTail
|
|
217
|
+
: current.stdoutTail || current.stderrTail;
|
|
218
|
+
const recovered = previousStatus === undefined && terminal;
|
|
219
|
+
await this.emit({
|
|
220
|
+
role: "runner",
|
|
221
|
+
kind: terminal && current.status !== "pass" ? "error" : "status",
|
|
222
|
+
content: terminal
|
|
223
|
+
? `Managed verification ${current.status} after ${elapsed}${recovered ? " (recovered from its durable receipt)" : ""}${outputTail ? `: ${clip(outputTail, 600)}` : "."}`
|
|
224
|
+
: `QA started managed verification ${current.commandId}. Forge is supervising it; durable progress remains available in the UI while the provider continues independent work.`,
|
|
225
|
+
eventType: terminal ? "verification_completed" : "verification_waiting",
|
|
226
|
+
data: JSON.stringify({
|
|
227
|
+
command_id: current.commandId,
|
|
228
|
+
status: current.status,
|
|
229
|
+
elapsed_ms: elapsedMs,
|
|
230
|
+
output_revision: current.outputRevision,
|
|
231
|
+
last_output_at: current.lastOutputAt,
|
|
232
|
+
recovered,
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function isManagedVerificationNonterminal(status) {
|
|
238
|
+
return status === "starting" || status === "running" || status === "waiting_for_build_slot";
|
|
239
|
+
}
|
|
240
|
+
async function parseManagedVerificationSnapshot(receiptPath) {
|
|
241
|
+
try {
|
|
242
|
+
const receipt = JSON.parse(await fs.readFile(receiptPath, "utf8"));
|
|
243
|
+
const commandId = typeof receipt.command_id === "string" ? receipt.command_id : null;
|
|
244
|
+
const status = typeof receipt.status === "string" ? receipt.status : null;
|
|
245
|
+
if (!commandId || !status)
|
|
246
|
+
return null;
|
|
247
|
+
return {
|
|
248
|
+
commandId,
|
|
249
|
+
status,
|
|
250
|
+
elapsedMs: typeof receipt.elapsed_ms === "number" ? receipt.elapsed_ms : null,
|
|
251
|
+
startedAt: typeof receipt.started_at === "string" ? receipt.started_at : null,
|
|
252
|
+
outputRevision: typeof receipt.output_revision === "number" ? receipt.output_revision : null,
|
|
253
|
+
lastOutputAt: typeof receipt.last_output_at === "string" ? receipt.last_output_at : null,
|
|
254
|
+
lastOutputStream: receipt.last_output_stream === "stdout" || receipt.last_output_stream === "stderr"
|
|
255
|
+
? receipt.last_output_stream
|
|
256
|
+
: null,
|
|
257
|
+
stdoutTail: tail(typeof receipt.stdout === "string" ? receipt.stdout : ""),
|
|
258
|
+
stderrTail: tail(typeof receipt.stderr === "string" ? receipt.stderr : ""),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function tail(value) {
|
|
266
|
+
return value.trim().split(/\r?\n/u).slice(-4).join("\n");
|
|
267
|
+
}
|
|
268
|
+
function formatElapsed(milliseconds) {
|
|
269
|
+
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
|
270
|
+
const minutes = Math.floor(seconds / 60);
|
|
271
|
+
return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
|
|
272
|
+
}
|
|
273
|
+
class ProviderToolActivityHeartbeat {
|
|
274
|
+
outputState;
|
|
275
|
+
emit;
|
|
276
|
+
intervalMs;
|
|
277
|
+
timer = null;
|
|
278
|
+
constructor(outputState, emit, intervalMs) {
|
|
279
|
+
this.outputState = outputState;
|
|
280
|
+
this.emit = emit;
|
|
281
|
+
this.intervalMs = intervalMs;
|
|
282
|
+
}
|
|
283
|
+
async consume(line) {
|
|
284
|
+
const events = this.outputState.consume(line);
|
|
285
|
+
this.sync();
|
|
286
|
+
for (const event of events) {
|
|
287
|
+
await this.emit(event);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
stop() {
|
|
291
|
+
if (this.timer) {
|
|
292
|
+
clearInterval(this.timer);
|
|
293
|
+
this.timer = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
sync() {
|
|
297
|
+
if (!this.outputState.hasActiveToolCall) {
|
|
298
|
+
this.stop();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (this.timer) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.timer = setInterval(() => {
|
|
305
|
+
if (!this.outputState.hasActiveToolCall) {
|
|
306
|
+
this.stop();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
void this.emit({
|
|
310
|
+
role: "runner",
|
|
311
|
+
kind: "status",
|
|
312
|
+
content: "A supervised provider tool is still running.",
|
|
313
|
+
eventType: "provider_tool_heartbeat",
|
|
314
|
+
}).catch(() => undefined);
|
|
315
|
+
}, Math.max(1, this.intervalMs));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
55
318
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
56
319
|
export class RoutingProviderCliService {
|
|
57
320
|
realProvider;
|
|
@@ -94,9 +357,15 @@ export class RoutingProviderCliService {
|
|
|
94
357
|
export class ProcessBackedProviderCliService {
|
|
95
358
|
processRunner;
|
|
96
359
|
git;
|
|
97
|
-
|
|
360
|
+
providerToolHeartbeatIntervalMs;
|
|
361
|
+
publicationCoordinator;
|
|
362
|
+
prePublicationValidator;
|
|
363
|
+
constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs, publicationCoordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot()), prePublicationValidator = new PrePublicationValidator()) {
|
|
98
364
|
this.processRunner = processRunner;
|
|
99
365
|
this.git = git;
|
|
366
|
+
this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
|
|
367
|
+
this.publicationCoordinator = publicationCoordinator;
|
|
368
|
+
this.prePublicationValidator = prePublicationValidator;
|
|
100
369
|
}
|
|
101
370
|
async runDraft(context) {
|
|
102
371
|
const { dispatch } = context;
|
|
@@ -110,15 +379,18 @@ export class ProcessBackedProviderCliService {
|
|
|
110
379
|
await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
|
|
111
380
|
const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
|
|
112
381
|
const decomposeDraft = isDecomposeDraftDispatch(dispatch);
|
|
113
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft" });
|
|
382
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
|
|
114
383
|
const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
|
|
115
|
-
const
|
|
384
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
|
|
385
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
116
386
|
const command = buildProviderCliCommand(dispatch, {
|
|
117
387
|
worktreePath: worktree.worktreePath,
|
|
118
|
-
prompt:
|
|
388
|
+
prompt: draftPrompt,
|
|
389
|
+
promptFromStdin: pipePromptToStdin,
|
|
119
390
|
completionTools: completionTools ?? undefined,
|
|
120
391
|
skipGitRepoCheck: false,
|
|
121
|
-
imagePaths: stagedAttachments.imagePaths
|
|
392
|
+
imagePaths: stagedAttachments.imagePaths,
|
|
393
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
122
394
|
});
|
|
123
395
|
const draftArtifactSlugs = dispatch.artifacts
|
|
124
396
|
.filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
|
|
@@ -133,7 +405,7 @@ export class ProcessBackedProviderCliService {
|
|
|
133
405
|
role: "runner",
|
|
134
406
|
kind: "checkpoint",
|
|
135
407
|
content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
|
|
136
|
-
eventType: "
|
|
408
|
+
eventType: "provider_cli_starting"
|
|
137
409
|
});
|
|
138
410
|
await context.emit({
|
|
139
411
|
role: "runner",
|
|
@@ -144,6 +416,7 @@ export class ProcessBackedProviderCliService {
|
|
|
144
416
|
eventType: "completion_tools_configured"
|
|
145
417
|
});
|
|
146
418
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
419
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
147
420
|
try {
|
|
148
421
|
const providerCancellation = new AbortController();
|
|
149
422
|
const abortProvider = () => providerCancellation.abort();
|
|
@@ -154,17 +427,23 @@ export class ProcessBackedProviderCliService {
|
|
|
154
427
|
context.signal?.addEventListener("abort", abortProvider, { once: true });
|
|
155
428
|
}
|
|
156
429
|
try {
|
|
157
|
-
const runPromise = this.processRunner.run({
|
|
430
|
+
const runPromise = (context.processRunner ?? this.processRunner).run({
|
|
158
431
|
command: command.command,
|
|
159
432
|
args: command.args,
|
|
160
433
|
cwd: worktree.worktreePath,
|
|
161
|
-
env: providerProcessEnv(dispatch),
|
|
162
|
-
stdin: pipePromptToStdin ?
|
|
434
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
435
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
163
436
|
}, {
|
|
437
|
+
onStarted: async () => {
|
|
438
|
+
await context.emit({
|
|
439
|
+
role: "runner",
|
|
440
|
+
kind: "checkpoint",
|
|
441
|
+
content: "Provider CLI Draft is supervised and ready.",
|
|
442
|
+
eventType: "provider_cli_started"
|
|
443
|
+
});
|
|
444
|
+
},
|
|
164
445
|
onStdoutLine: async (line) => {
|
|
165
|
-
|
|
166
|
-
await context.emit(event);
|
|
167
|
-
}
|
|
446
|
+
await toolHeartbeat.consume(line);
|
|
168
447
|
}
|
|
169
448
|
}, providerCancellation.signal);
|
|
170
449
|
const artifactWaitCancellation = new AbortController();
|
|
@@ -199,7 +478,7 @@ export class ProcessBackedProviderCliService {
|
|
|
199
478
|
context.signal?.removeEventListener("abort", abortArtifactWait);
|
|
200
479
|
}
|
|
201
480
|
if (result.exitCode !== 0 && !earlyProposalArtifact) {
|
|
202
|
-
throw
|
|
481
|
+
throw providerCliExitFailure(command.command, result);
|
|
203
482
|
}
|
|
204
483
|
await context.emit({
|
|
205
484
|
role: "runner",
|
|
@@ -260,6 +539,7 @@ export class ProcessBackedProviderCliService {
|
|
|
260
539
|
}
|
|
261
540
|
}
|
|
262
541
|
finally {
|
|
542
|
+
toolHeartbeat.stop();
|
|
263
543
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
264
544
|
}
|
|
265
545
|
}
|
|
@@ -278,21 +558,26 @@ export class ProcessBackedProviderCliService {
|
|
|
278
558
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
279
559
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
280
560
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
281
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
|
|
561
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase, runtimeEnvironment: context.runtimeEnvironment });
|
|
562
|
+
const implementationPrompt = debugCycle
|
|
563
|
+
? buildDebugCyclePrompt(dispatch, phase)
|
|
564
|
+
: developCycle
|
|
565
|
+
? buildDevelopCyclePrompt(dispatch, phase)
|
|
566
|
+
: buildImplementationPrompt(dispatch, phase);
|
|
567
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, implementationPrompt, context.runtimeEnvironment);
|
|
568
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
282
569
|
const command = buildProviderCliCommand(dispatch, {
|
|
283
570
|
worktreePath: worktree.worktreePath,
|
|
284
|
-
prompt:
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
: buildImplementationPrompt(dispatch, phase),
|
|
289
|
-
completionTools
|
|
571
|
+
prompt: implementationPrompt,
|
|
572
|
+
promptFromStdin: pipePromptToStdin,
|
|
573
|
+
completionTools,
|
|
574
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
290
575
|
});
|
|
291
576
|
await context.emit({
|
|
292
577
|
role: "runner",
|
|
293
578
|
kind: "checkpoint",
|
|
294
579
|
content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
|
|
295
|
-
eventType: "
|
|
580
|
+
eventType: "provider_cli_starting"
|
|
296
581
|
});
|
|
297
582
|
await context.emit({
|
|
298
583
|
role: "runner",
|
|
@@ -305,30 +590,38 @@ export class ProcessBackedProviderCliService {
|
|
|
305
590
|
eventType: "completion_tools_configured"
|
|
306
591
|
});
|
|
307
592
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
593
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
308
594
|
const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
|
|
309
595
|
let result;
|
|
310
596
|
try {
|
|
311
597
|
liveCompletionEvents.start();
|
|
312
|
-
result = await this.processRunner.run({
|
|
598
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
313
599
|
command: command.command,
|
|
314
600
|
args: command.args,
|
|
315
601
|
cwd: worktree.worktreePath,
|
|
316
|
-
env: providerProcessEnv(dispatch)
|
|
602
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
603
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
317
604
|
}, {
|
|
605
|
+
onStarted: async () => {
|
|
606
|
+
await context.emit({
|
|
607
|
+
role: "runner",
|
|
608
|
+
kind: "checkpoint",
|
|
609
|
+
content: "Provider CLI is supervised and ready.",
|
|
610
|
+
eventType: "provider_cli_started"
|
|
611
|
+
});
|
|
612
|
+
},
|
|
318
613
|
onStdoutLine: async (line) => {
|
|
319
|
-
|
|
320
|
-
await context.emit(event);
|
|
321
|
-
}
|
|
614
|
+
await toolHeartbeat.consume(line);
|
|
322
615
|
}
|
|
323
616
|
}, context.signal);
|
|
324
617
|
}
|
|
325
618
|
finally {
|
|
619
|
+
toolHeartbeat.stop();
|
|
326
620
|
await liveCompletionEvents.stop();
|
|
327
621
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
328
622
|
}
|
|
329
623
|
if (result.exitCode !== 0) {
|
|
330
|
-
|
|
331
|
-
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
624
|
+
throw providerCliExitFailure(command.command, result);
|
|
332
625
|
}
|
|
333
626
|
await context.emit({
|
|
334
627
|
role: "runner",
|
|
@@ -337,9 +630,12 @@ export class ProcessBackedProviderCliService {
|
|
|
337
630
|
eventType: "provider_cli_completed"
|
|
338
631
|
});
|
|
339
632
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
633
|
+
if (developCycle || phase === "implementation") {
|
|
634
|
+
await this.prePublicationValidator.validate(worktree.worktreePath, "implementation", publicationCapabilities(context, context.runtimeEnvironment), context.emit, context.signal, prePublicationExecution(context, providerProcessEnv(dispatch, context.runtimeEnvironment), this.processRunner));
|
|
635
|
+
}
|
|
340
636
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
341
|
-
if (developCycle || debugCycle || phase === "implementation") {
|
|
342
|
-
await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, context.emit);
|
|
637
|
+
if ((developCycle || debugCycle || phase === "implementation") && providerOwnsBranchPublication(context)) {
|
|
638
|
+
await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit);
|
|
343
639
|
await context.emit({
|
|
344
640
|
role: "runner",
|
|
345
641
|
kind: "checkpoint",
|
|
@@ -397,18 +693,23 @@ export class ProcessBackedProviderCliService {
|
|
|
397
693
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
398
694
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
399
695
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
400
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
|
|
696
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect", runtimeEnvironment: context.runtimeEnvironment });
|
|
697
|
+
const architectPrompt = buildArchitectPrompt(dispatch);
|
|
698
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, architectPrompt, context.runtimeEnvironment);
|
|
699
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
401
700
|
const command = buildProviderCliCommand(dispatch, {
|
|
402
701
|
worktreePath: worktree.worktreePath,
|
|
403
|
-
prompt:
|
|
404
|
-
|
|
702
|
+
prompt: architectPrompt,
|
|
703
|
+
promptFromStdin: pipePromptToStdin,
|
|
704
|
+
completionTools,
|
|
705
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
405
706
|
});
|
|
406
707
|
const commandText = formatProviderCliCommandForCheckpoint(command);
|
|
407
708
|
await context.emit({
|
|
408
709
|
role: "runner",
|
|
409
710
|
kind: "checkpoint",
|
|
410
711
|
content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
|
|
411
|
-
eventType: "
|
|
712
|
+
eventType: "provider_cli_starting"
|
|
412
713
|
});
|
|
413
714
|
await context.emit({
|
|
414
715
|
role: "runner",
|
|
@@ -417,30 +718,36 @@ export class ProcessBackedProviderCliService {
|
|
|
417
718
|
eventType: "completion_tools_configured"
|
|
418
719
|
});
|
|
419
720
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
721
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
420
722
|
const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
|
|
421
723
|
let result;
|
|
422
724
|
try {
|
|
423
725
|
liveArchitectEvents.start();
|
|
424
|
-
result = await this.processRunner.run({
|
|
726
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
425
727
|
command: command.command,
|
|
426
728
|
args: command.args,
|
|
427
729
|
cwd: worktree.worktreePath,
|
|
428
|
-
env: providerProcessEnv(dispatch)
|
|
730
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
731
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
429
732
|
}, {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
733
|
+
onStarted: async () => {
|
|
734
|
+
await context.emit({
|
|
735
|
+
role: "runner",
|
|
736
|
+
kind: "checkpoint",
|
|
737
|
+
content: "Provider CLI Architect is supervised and ready.",
|
|
738
|
+
eventType: "provider_cli_started"
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
onStdoutLine: async (line) => await toolHeartbeat.consume(line)
|
|
435
742
|
}, context.signal);
|
|
436
743
|
}
|
|
437
744
|
finally {
|
|
745
|
+
toolHeartbeat.stop();
|
|
438
746
|
await liveArchitectEvents.stop();
|
|
439
747
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
440
748
|
}
|
|
441
749
|
if (result.exitCode !== 0) {
|
|
442
|
-
|
|
443
|
-
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
750
|
+
throw providerCliExitFailure(command.command, result);
|
|
444
751
|
}
|
|
445
752
|
await context.emit({
|
|
446
753
|
role: "runner",
|
|
@@ -488,18 +795,23 @@ export class ProcessBackedProviderCliService {
|
|
|
488
795
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
489
796
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
490
797
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
491
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
|
|
798
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa", runtimeEnvironment: context.runtimeEnvironment });
|
|
799
|
+
const qaPrompt = buildQaPrompt(dispatch, specBody);
|
|
800
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, qaPrompt, context.runtimeEnvironment);
|
|
801
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
492
802
|
const command = buildProviderCliCommand(dispatch, {
|
|
493
803
|
worktreePath: worktree.worktreePath,
|
|
494
|
-
prompt:
|
|
495
|
-
|
|
804
|
+
prompt: qaPrompt,
|
|
805
|
+
promptFromStdin: pipePromptToStdin,
|
|
806
|
+
completionTools,
|
|
807
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
496
808
|
});
|
|
497
809
|
const commandText = formatProviderCliCommandForCheckpoint(command);
|
|
498
810
|
await context.emit({
|
|
499
811
|
role: "runner",
|
|
500
812
|
kind: "checkpoint",
|
|
501
813
|
content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
|
|
502
|
-
eventType: "
|
|
814
|
+
eventType: "provider_cli_starting"
|
|
503
815
|
});
|
|
504
816
|
await context.emit({
|
|
505
817
|
role: "runner",
|
|
@@ -508,30 +820,39 @@ export class ProcessBackedProviderCliService {
|
|
|
508
820
|
eventType: "completion_tools_configured"
|
|
509
821
|
});
|
|
510
822
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
823
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
824
|
+
const verificationProgress = new ManagedVerificationProgressEmitter(context.runtimeEnvironment?.FORGE_LOG_DIR, context.emit);
|
|
511
825
|
const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
|
|
512
826
|
let result;
|
|
513
827
|
try {
|
|
514
828
|
liveQaEvents.start();
|
|
515
|
-
|
|
829
|
+
verificationProgress.start();
|
|
830
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
516
831
|
command: command.command,
|
|
517
832
|
args: command.args,
|
|
518
833
|
cwd: worktree.worktreePath,
|
|
519
|
-
env: providerProcessEnv(dispatch)
|
|
834
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
835
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
520
836
|
}, {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
837
|
+
onStarted: async () => {
|
|
838
|
+
await context.emit({
|
|
839
|
+
role: "runner",
|
|
840
|
+
kind: "checkpoint",
|
|
841
|
+
content: "Provider CLI QA is supervised and ready.",
|
|
842
|
+
eventType: "provider_cli_started"
|
|
843
|
+
});
|
|
844
|
+
},
|
|
845
|
+
onStdoutLine: async (line) => await toolHeartbeat.consume(line)
|
|
526
846
|
}, context.signal);
|
|
527
847
|
}
|
|
528
848
|
finally {
|
|
849
|
+
toolHeartbeat.stop();
|
|
850
|
+
await verificationProgress.stop();
|
|
529
851
|
await liveQaEvents.stop();
|
|
530
852
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
531
853
|
}
|
|
532
854
|
if (result.exitCode !== 0) {
|
|
533
|
-
|
|
534
|
-
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
855
|
+
throw providerCliExitFailure(command.command, result);
|
|
535
856
|
}
|
|
536
857
|
await context.emit({
|
|
537
858
|
role: "runner",
|
|
@@ -547,7 +868,7 @@ export class ProcessBackedProviderCliService {
|
|
|
547
868
|
? "Provider CLI QA completed but did not produce the complete durable QA artifact bundle."
|
|
548
869
|
: "Provider CLI QA completed but did not call the Forge QA completion tool.");
|
|
549
870
|
}
|
|
550
|
-
enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
|
|
871
|
+
await enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
|
|
551
872
|
if (!liveQaEvents.hasEmitted()) {
|
|
552
873
|
await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
|
|
553
874
|
await emitQaStructuredEvents(context.emit, qaArtifact);
|
|
@@ -579,41 +900,126 @@ export class ProcessBackedProviderCliService {
|
|
|
579
900
|
}
|
|
580
901
|
async runPullRequestRepair(context, route) {
|
|
581
902
|
const { dispatch, worktree } = context;
|
|
903
|
+
const { expectedRepairHeadSha, preparedHeadSha } = await assertRepairHeadEpoch(context, this.git);
|
|
582
904
|
const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
|
|
583
905
|
const addressComments = !repairChecks && isPullRequestCommentsDispatch(dispatch);
|
|
906
|
+
let repairSpec = null;
|
|
907
|
+
let repairEnvironment = context.runtimeEnvironment;
|
|
908
|
+
let completionTools = null;
|
|
909
|
+
if (repairChecks) {
|
|
910
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
911
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
912
|
+
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
913
|
+
if (!laneCommand) {
|
|
914
|
+
throw new Error(`CI repair readiness failed: lane command for '${dispatch.execution.lane}' was not resolved, so the approved spec cannot be materialized.`);
|
|
915
|
+
}
|
|
916
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
|
|
917
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
918
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
919
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
|
|
920
|
+
repairSpec = {
|
|
921
|
+
repoPath: worktree.worktreePath,
|
|
922
|
+
path: specPath,
|
|
923
|
+
body: specBody,
|
|
924
|
+
producedBy: "forge-lane-command",
|
|
925
|
+
status: "approved",
|
|
926
|
+
};
|
|
927
|
+
const readiness = await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
|
|
928
|
+
repairEnvironment = readiness.environment;
|
|
929
|
+
completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "ci_repair", runtimeEnvironment: repairEnvironment });
|
|
930
|
+
await context.emit({
|
|
931
|
+
role: "runner",
|
|
932
|
+
kind: "checkpoint",
|
|
933
|
+
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.`,
|
|
934
|
+
eventType: "ci_repair_readiness_ready",
|
|
935
|
+
});
|
|
936
|
+
await context.emit({
|
|
937
|
+
role: "runner",
|
|
938
|
+
kind: "checkpoint",
|
|
939
|
+
content: "Forge CI repair completion tool configured; provider must call forge.complete_ci_repair before Bridge-owned commit and push.",
|
|
940
|
+
eventType: "completion_tools_configured",
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const repairPrompt = repairChecks
|
|
944
|
+
? buildPullRequestCheckRepairPrompt(dispatch, worktree.branchName)
|
|
945
|
+
: addressComments
|
|
946
|
+
? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
|
|
947
|
+
: buildConflictResolvePrompt(dispatch, worktree.branchName);
|
|
948
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, repairPrompt, repairEnvironment);
|
|
949
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
584
950
|
const command = buildProviderCliCommand(dispatch, {
|
|
585
951
|
worktreePath: worktree.worktreePath,
|
|
586
|
-
prompt:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
: buildConflictResolvePrompt(dispatch, worktree.branchName)
|
|
952
|
+
prompt: repairPrompt,
|
|
953
|
+
promptFromStdin: pipePromptToStdin,
|
|
954
|
+
completionTools: completionTools ?? undefined,
|
|
955
|
+
runtimeEnvironment: repairEnvironment
|
|
591
956
|
});
|
|
592
957
|
await context.emit({
|
|
593
958
|
role: "runner",
|
|
594
959
|
kind: "checkpoint",
|
|
595
960
|
content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
|
|
596
|
-
eventType: "
|
|
961
|
+
eventType: "provider_cli_starting"
|
|
597
962
|
});
|
|
598
963
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
964
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
965
|
+
let result;
|
|
966
|
+
try {
|
|
967
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
968
|
+
command: command.command,
|
|
969
|
+
args: command.args,
|
|
970
|
+
cwd: worktree.worktreePath,
|
|
971
|
+
env: providerProcessEnv(dispatch, repairEnvironment),
|
|
972
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
973
|
+
}, {
|
|
974
|
+
onStarted: async () => {
|
|
975
|
+
await context.emit({
|
|
976
|
+
role: "runner",
|
|
977
|
+
kind: "checkpoint",
|
|
978
|
+
content: `Provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"} is supervised and ready.`,
|
|
979
|
+
eventType: "provider_cli_started"
|
|
980
|
+
});
|
|
981
|
+
},
|
|
982
|
+
onStdoutLine: async (line) => {
|
|
983
|
+
await toolHeartbeat.consume(line);
|
|
608
984
|
}
|
|
985
|
+
}, context.signal);
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
toolHeartbeat.stop();
|
|
989
|
+
if (completionTools) {
|
|
990
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
609
991
|
}
|
|
610
|
-
}
|
|
992
|
+
}
|
|
611
993
|
if (result.exitCode !== 0) {
|
|
612
|
-
|
|
613
|
-
|
|
994
|
+
throw providerCliExitFailure(command.command, result);
|
|
995
|
+
}
|
|
996
|
+
const postProviderHeadSha = (await this.git.run(worktree.worktreePath, ["rev-parse", "HEAD"])).trim();
|
|
997
|
+
if (postProviderHeadSha.toLowerCase() !== preparedHeadSha.toLowerCase()) {
|
|
998
|
+
throw new Error(`provider_publication_boundary_violation: PR repair changed HEAD from ${preparedHeadSha} to ${postProviderHeadSha}; Forge requires provider changes to remain uncommitted and unpushed until validation passes.`);
|
|
614
999
|
}
|
|
1000
|
+
let repairArtifact = null;
|
|
1001
|
+
if (repairChecks) {
|
|
1002
|
+
repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
|
|
1003
|
+
if (!repairArtifact) {
|
|
1004
|
+
throw new Error("Provider CLI completed but did not call forge.complete_ci_repair. Forge did not commit or push the repair.");
|
|
1005
|
+
}
|
|
1006
|
+
if ((repairArtifact.status ?? "").trim().toLowerCase() !== "pass") {
|
|
1007
|
+
throw new Error(`CI repair completion gate recorded ${repairArtifact.status}; Forge did not commit or push the repair.`);
|
|
1008
|
+
}
|
|
1009
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
|
|
1010
|
+
await emitPhaseCompletionEvent(context.emit, repairArtifact);
|
|
1011
|
+
await emitPhaseGateEvents(context.emit, repairArtifact);
|
|
1012
|
+
}
|
|
1013
|
+
const validationProfile = repairChecks
|
|
1014
|
+
? "ci_repair"
|
|
1015
|
+
: addressComments
|
|
1016
|
+
? "comment_repair"
|
|
1017
|
+
: "conflict_repair";
|
|
1018
|
+
await this.prePublicationValidator.validate(worktree.worktreePath, validationProfile, publicationCapabilities(context, repairEnvironment), context.emit, context.signal, prePublicationExecution(context, providerProcessEnv(dispatch, repairEnvironment), this.processRunner));
|
|
615
1019
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
616
|
-
|
|
1020
|
+
if (providerOwnsBranchPublication(context)) {
|
|
1021
|
+
await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit, expectedRepairHeadSha);
|
|
1022
|
+
}
|
|
617
1023
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
618
1024
|
await context.emit({
|
|
619
1025
|
role: "assistant",
|
|
@@ -630,10 +1036,24 @@ export class ProcessBackedProviderCliService {
|
|
|
630
1036
|
? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
|
|
631
1037
|
: addressComments
|
|
632
1038
|
? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
|
|
633
|
-
: `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)})
|
|
1039
|
+
: `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`,
|
|
1040
|
+
spec: repairSpec,
|
|
1041
|
+
artifact: repairArtifact,
|
|
1042
|
+
artifacts: repairArtifact ? [repairArtifact] : [],
|
|
634
1043
|
};
|
|
635
1044
|
}
|
|
636
1045
|
}
|
|
1046
|
+
function publicationCapabilities(context, runtimeEnvironment) {
|
|
1047
|
+
const capabilities = new Set();
|
|
1048
|
+
const launchTarget = context.dispatch.qaBrowser?.launchTarget;
|
|
1049
|
+
if (runtimeEnvironment?.FORGE_PLAYWRIGHT_CONFIG
|
|
1050
|
+
|| launchTarget?.url
|
|
1051
|
+
|| launchTarget?.command
|
|
1052
|
+
|| launchTarget?.managed_runtime_port) {
|
|
1053
|
+
capabilities.add("browser_launch");
|
|
1054
|
+
}
|
|
1055
|
+
return capabilities;
|
|
1056
|
+
}
|
|
637
1057
|
async function stageDraftAttachments(dispatch, metadataRoot) {
|
|
638
1058
|
const attachments = dispatch.draft?.attachments ?? [];
|
|
639
1059
|
const stageable = attachments.filter((attachment) => (attachment.kind === "text" && typeof attachment.textContent === "string")
|
|
@@ -696,8 +1116,12 @@ function clipFileName(fileName, maxLength) {
|
|
|
696
1116
|
return `${stem.slice(0, maxStemLength)}${extension}`;
|
|
697
1117
|
}
|
|
698
1118
|
const providerPromptStdinThreshold = 8_000;
|
|
699
|
-
function shouldPipePromptToStdin(provider, prompt) {
|
|
700
|
-
return provider.trim().toLowerCase() === "codex"
|
|
1119
|
+
function shouldPipePromptToStdin(provider, prompt, force = false) {
|
|
1120
|
+
return provider.trim().toLowerCase() === "codex"
|
|
1121
|
+
&& (force || Buffer.byteLength(prompt, "utf8") > providerPromptStdinThreshold);
|
|
1122
|
+
}
|
|
1123
|
+
function usesManagedWindowsHost(dispatch) {
|
|
1124
|
+
return process.platform === "win32" && dispatch.sandbox?.executionMode === "host";
|
|
701
1125
|
}
|
|
702
1126
|
function isFakeWorkerModel(model) {
|
|
703
1127
|
return model.trim().toLowerCase() === fakeWorkerModel;
|