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