@loucompanion/forge-bridge 0.1.3-dev.d1ca67076aa9 → 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 +127 -0
- package/dist/bridge/bridge.base.d.ts +8 -1
- package/dist/bridge/bridge.service.d.ts +93 -3
- package/dist/bridge/bridge.service.js +1358 -47
- package/dist/bridge/bridge.types.d.ts +202 -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 +47 -1
- package/dist/cli/cli.service.d.ts +45 -2
- package/dist/cli/cli.service.js +378 -26
- package/dist/cli/local-config.d.ts +7 -0
- package/dist/cli/local-config.js +51 -3
- 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/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +84 -0
- package/dist/sandbox/preflight.js +521 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +121 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +93 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +72 -0
- package/dist/session/managed-attempt-tools.d.ts +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 +256 -26
- 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.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +68 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +26 -0
- package/dist/session/provider-cli/provider-cli.process.js +474 -23
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +16 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +224 -24
- 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 +26 -3
- package/dist/session/provider-cli/provider-cli.service.js +650 -108
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.js +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 +549 -58
- package/dist/session/session.types.d.ts +124 -1
- package/dist/session/session.types.js +231 -4
- 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/process/windows-launch.d.ts +1 -1
- package/dist/shared/process/windows-launch.js +6 -3
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +16 -4
- 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 +39 -1
- package/dist/worktree/dependency-warmer.js +351 -8
- 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 +8 -4
- package/runtime/windows-job-supervisor.ps1 +444 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -1,19 +1,320 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { RepositoryCoordinatorLock } from "../../sandbox/repository-lock.js";
|
|
4
4
|
import { GitClient } from "../../shared/git/git.js";
|
|
5
5
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
6
|
+
import { defaultWorktreeCoordinatorStateRoot } from "../../worktree/worktree-paths.js";
|
|
6
7
|
import { completionPhaseFromDispatch, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion } from "../phase-completion.js";
|
|
7
|
-
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";
|
|
8
9
|
import { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
|
|
9
|
-
import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliCommand, buildPullRequestCommentsPrompt, buildQaPrompt,
|
|
10
|
+
import { buildArchitectPrompt, buildConflictResolvePrompt, buildDebugCyclePrompt, buildDevelopCyclePrompt, buildDraftPrompt, buildDraftSpec, buildImplementationPrompt, buildProviderCliPrompt, buildProviderCliCommand, durableDraftInput, buildPullRequestCheckRepairPrompt, buildPullRequestCommentsPrompt, buildQaPrompt, formatProviderCliCommandForCheckpoint, isDecomposeDraftDispatch, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch } from "./provider-cli.prompts.js";
|
|
10
11
|
import { StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
11
12
|
import { emitProviderUsageEvent, providerProcessEnv } from "./provider-cli.runtime.js";
|
|
12
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";
|
|
13
17
|
export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
|
|
14
|
-
export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand } from "./provider-cli.prompts.js";
|
|
15
|
-
export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
18
|
+
export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
|
|
19
|
+
export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, PROVIDER_OUTPUT_CAPTURE_LIMIT_CHARS, StreamingProviderProcessRunner } from "./provider-cli.process.js";
|
|
16
20
|
export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
|
|
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;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await git.run(worktreePath, ["push", "origin", `HEAD:${branchName}`]);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
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
|
+
}
|
|
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.`);
|
|
81
|
+
}
|
|
82
|
+
return { environment, processRunner: defaultRunner };
|
|
83
|
+
}
|
|
84
|
+
function remoteTrackingRefspec(branchName) {
|
|
85
|
+
return `refs/heads/${branchName}:refs/remotes/origin/${branchName}`;
|
|
86
|
+
}
|
|
87
|
+
function isNonFastForwardPushError(err) {
|
|
88
|
+
const message = errorMessage(err).toLowerCase();
|
|
89
|
+
return message.includes("non-fast-forward")
|
|
90
|
+
|| message.includes("fetch first")
|
|
91
|
+
|| message.includes("tip of your current branch is behind")
|
|
92
|
+
|| message.includes("updates were rejected because the tip of your current branch is behind")
|
|
93
|
+
|| message.includes("stale info");
|
|
94
|
+
}
|
|
95
|
+
function errorMessage(err) {
|
|
96
|
+
return err instanceof Error ? err.message : String(err);
|
|
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
|
+
}
|
|
17
318
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
18
319
|
export class RoutingProviderCliService {
|
|
19
320
|
realProvider;
|
|
@@ -32,6 +333,11 @@ export class RoutingProviderCliService {
|
|
|
32
333
|
? await this.deterministicProvider.runArchitect(context)
|
|
33
334
|
: await this.realProvider.runArchitect(context);
|
|
34
335
|
}
|
|
336
|
+
async runCiRepair(context) {
|
|
337
|
+
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
338
|
+
? await this.deterministicProvider.runCiRepair(context)
|
|
339
|
+
: await this.realProvider.runCiRepair(context);
|
|
340
|
+
}
|
|
35
341
|
async runConflictResolve(context) {
|
|
36
342
|
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
37
343
|
? await this.deterministicProvider.runConflictResolve(context)
|
|
@@ -51,34 +357,40 @@ export class RoutingProviderCliService {
|
|
|
51
357
|
export class ProcessBackedProviderCliService {
|
|
52
358
|
processRunner;
|
|
53
359
|
git;
|
|
54
|
-
|
|
360
|
+
providerToolHeartbeatIntervalMs;
|
|
361
|
+
publicationCoordinator;
|
|
362
|
+
prePublicationValidator;
|
|
363
|
+
constructor(processRunner = new StreamingProviderProcessRunner(), git = new GitClient(), providerToolHeartbeatIntervalMs = defaultProviderToolHeartbeatIntervalMs, publicationCoordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot()), prePublicationValidator = new PrePublicationValidator()) {
|
|
55
364
|
this.processRunner = processRunner;
|
|
56
365
|
this.git = git;
|
|
366
|
+
this.providerToolHeartbeatIntervalMs = providerToolHeartbeatIntervalMs;
|
|
367
|
+
this.publicationCoordinator = publicationCoordinator;
|
|
368
|
+
this.prePublicationValidator = prePublicationValidator;
|
|
57
369
|
}
|
|
58
370
|
async runDraft(context) {
|
|
59
371
|
const { dispatch } = context;
|
|
60
372
|
if (!dispatch.draft) {
|
|
61
373
|
throw new Error("Draft dispatch context is missing.");
|
|
62
374
|
}
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
repoPath: tempDir,
|
|
66
|
-
worktreePath: tempDir,
|
|
67
|
-
branchName: "draft",
|
|
68
|
-
baseBranch: dispatch.cycleRun.baseBranch || dispatch.repo.defaultBranch || "draft",
|
|
69
|
-
cleanupWorktrees: true
|
|
70
|
-
};
|
|
71
|
-
const metadataRoot = path.join(tempDir, ".forge", "work-items", dispatch.workItem.id);
|
|
375
|
+
const { worktree } = context;
|
|
376
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
72
377
|
await fs.mkdir(metadataRoot, { recursive: true });
|
|
73
|
-
await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(dispatch
|
|
378
|
+
await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(durableDraftInput(dispatch), null, 2) + "\n", "utf8");
|
|
74
379
|
await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
|
|
380
|
+
const stagedAttachments = await stageDraftAttachments(dispatch, metadataRoot);
|
|
75
381
|
const decomposeDraft = isDecomposeDraftDispatch(dispatch);
|
|
76
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree
|
|
382
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: decomposeDraft ? "decompose" : "draft", runtimeEnvironment: context.runtimeEnvironment });
|
|
383
|
+
const draftPrompt = buildDraftPrompt(dispatch, { attachmentReferences: stagedAttachments.textReferences });
|
|
384
|
+
const providerPrompt = buildProviderCliPrompt(dispatch, draftPrompt, context.runtimeEnvironment);
|
|
385
|
+
const pipePromptToStdin = shouldPipePromptToStdin(dispatch.execution.provider, providerPrompt, usesManagedWindowsHost(dispatch));
|
|
77
386
|
const command = buildProviderCliCommand(dispatch, {
|
|
78
|
-
worktreePath:
|
|
79
|
-
prompt:
|
|
387
|
+
worktreePath: worktree.worktreePath,
|
|
388
|
+
prompt: draftPrompt,
|
|
389
|
+
promptFromStdin: pipePromptToStdin,
|
|
80
390
|
completionTools: completionTools ?? undefined,
|
|
81
|
-
skipGitRepoCheck:
|
|
391
|
+
skipGitRepoCheck: false,
|
|
392
|
+
imagePaths: stagedAttachments.imagePaths,
|
|
393
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
82
394
|
});
|
|
83
395
|
const draftArtifactSlugs = dispatch.artifacts
|
|
84
396
|
.filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
|
|
@@ -92,8 +404,8 @@ export class ProcessBackedProviderCliService {
|
|
|
92
404
|
await context.emit({
|
|
93
405
|
role: "runner",
|
|
94
406
|
kind: "checkpoint",
|
|
95
|
-
content: `Starting provider CLI Draft: ${
|
|
96
|
-
eventType: "
|
|
407
|
+
content: `Starting provider CLI Draft: ${formatProviderCliCommandForCheckpoint(command)}`,
|
|
408
|
+
eventType: "provider_cli_starting"
|
|
97
409
|
});
|
|
98
410
|
await context.emit({
|
|
99
411
|
role: "runner",
|
|
@@ -104,6 +416,7 @@ export class ProcessBackedProviderCliService {
|
|
|
104
416
|
eventType: "completion_tools_configured"
|
|
105
417
|
});
|
|
106
418
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
419
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
107
420
|
try {
|
|
108
421
|
const providerCancellation = new AbortController();
|
|
109
422
|
const abortProvider = () => providerCancellation.abort();
|
|
@@ -114,16 +427,23 @@ export class ProcessBackedProviderCliService {
|
|
|
114
427
|
context.signal?.addEventListener("abort", abortProvider, { once: true });
|
|
115
428
|
}
|
|
116
429
|
try {
|
|
117
|
-
const runPromise = this.processRunner.run({
|
|
430
|
+
const runPromise = (context.processRunner ?? this.processRunner).run({
|
|
118
431
|
command: command.command,
|
|
119
432
|
args: command.args,
|
|
120
|
-
cwd:
|
|
121
|
-
env: providerProcessEnv(dispatch)
|
|
433
|
+
cwd: worktree.worktreePath,
|
|
434
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
435
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
122
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
|
+
},
|
|
123
445
|
onStdoutLine: async (line) => {
|
|
124
|
-
|
|
125
|
-
await context.emit(event);
|
|
126
|
-
}
|
|
446
|
+
await toolHeartbeat.consume(line);
|
|
127
447
|
}
|
|
128
448
|
}, providerCancellation.signal);
|
|
129
449
|
const artifactWaitCancellation = new AbortController();
|
|
@@ -134,7 +454,7 @@ export class ProcessBackedProviderCliService {
|
|
|
134
454
|
else {
|
|
135
455
|
context.signal?.addEventListener("abort", abortArtifactWait, { once: true });
|
|
136
456
|
}
|
|
137
|
-
const artifactWaitPromise = waitForCompletionArtifact(
|
|
457
|
+
const artifactWaitPromise = waitForCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, decomposeDraft ? "decompose" : "draft", artifactWaitCancellation.signal);
|
|
138
458
|
let result;
|
|
139
459
|
let earlyProposalArtifact = null;
|
|
140
460
|
try {
|
|
@@ -158,7 +478,7 @@ export class ProcessBackedProviderCliService {
|
|
|
158
478
|
context.signal?.removeEventListener("abort", abortArtifactWait);
|
|
159
479
|
}
|
|
160
480
|
if (result.exitCode !== 0 && !earlyProposalArtifact) {
|
|
161
|
-
throw
|
|
481
|
+
throw providerCliExitFailure(command.command, result);
|
|
162
482
|
}
|
|
163
483
|
await context.emit({
|
|
164
484
|
role: "runner",
|
|
@@ -168,8 +488,8 @@ export class ProcessBackedProviderCliService {
|
|
|
168
488
|
});
|
|
169
489
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
170
490
|
if (decomposeDraft) {
|
|
171
|
-
await emitDecomposeGateArtifacts(context.emit,
|
|
172
|
-
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(
|
|
491
|
+
await emitDecomposeGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
|
|
492
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "decompose");
|
|
173
493
|
if (!proposalArtifact) {
|
|
174
494
|
throw new Error("Provider CLI Decompose completed but did not call forge.complete_decompose_proposal.");
|
|
175
495
|
}
|
|
@@ -191,8 +511,8 @@ export class ProcessBackedProviderCliService {
|
|
|
191
511
|
artifacts: [proposalArtifact]
|
|
192
512
|
};
|
|
193
513
|
}
|
|
194
|
-
await emitDraftGateArtifacts(context.emit,
|
|
195
|
-
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(
|
|
514
|
+
await emitDraftGateArtifacts(context.emit, worktree.worktreePath, dispatch.workItem.id);
|
|
515
|
+
const proposalArtifact = earlyProposalArtifact ?? await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "draft");
|
|
196
516
|
if (!proposalArtifact) {
|
|
197
517
|
throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
|
|
198
518
|
}
|
|
@@ -219,8 +539,8 @@ export class ProcessBackedProviderCliService {
|
|
|
219
539
|
}
|
|
220
540
|
}
|
|
221
541
|
finally {
|
|
542
|
+
toolHeartbeat.stop();
|
|
222
543
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
223
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
224
544
|
}
|
|
225
545
|
}
|
|
226
546
|
async runImplementation(context) {
|
|
@@ -238,21 +558,26 @@ export class ProcessBackedProviderCliService {
|
|
|
238
558
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
239
559
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
240
560
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
241
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
|
|
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));
|
|
242
569
|
const command = buildProviderCliCommand(dispatch, {
|
|
243
570
|
worktreePath: worktree.worktreePath,
|
|
244
|
-
prompt:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
: buildImplementationPrompt(dispatch, phase),
|
|
249
|
-
completionTools
|
|
571
|
+
prompt: implementationPrompt,
|
|
572
|
+
promptFromStdin: pipePromptToStdin,
|
|
573
|
+
completionTools,
|
|
574
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
250
575
|
});
|
|
251
576
|
await context.emit({
|
|
252
577
|
role: "runner",
|
|
253
578
|
kind: "checkpoint",
|
|
254
|
-
content: `Starting provider CLI: ${
|
|
255
|
-
eventType: "
|
|
579
|
+
content: `Starting provider CLI: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
|
|
580
|
+
eventType: "provider_cli_starting"
|
|
256
581
|
});
|
|
257
582
|
await context.emit({
|
|
258
583
|
role: "runner",
|
|
@@ -265,30 +590,38 @@ export class ProcessBackedProviderCliService {
|
|
|
265
590
|
eventType: "completion_tools_configured"
|
|
266
591
|
});
|
|
267
592
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
593
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
268
594
|
const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
|
|
269
595
|
let result;
|
|
270
596
|
try {
|
|
271
597
|
liveCompletionEvents.start();
|
|
272
|
-
result = await this.processRunner.run({
|
|
598
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
273
599
|
command: command.command,
|
|
274
600
|
args: command.args,
|
|
275
601
|
cwd: worktree.worktreePath,
|
|
276
|
-
env: providerProcessEnv(dispatch)
|
|
602
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
603
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
277
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
|
+
},
|
|
278
613
|
onStdoutLine: async (line) => {
|
|
279
|
-
|
|
280
|
-
await context.emit(event);
|
|
281
|
-
}
|
|
614
|
+
await toolHeartbeat.consume(line);
|
|
282
615
|
}
|
|
283
616
|
}, context.signal);
|
|
284
617
|
}
|
|
285
618
|
finally {
|
|
619
|
+
toolHeartbeat.stop();
|
|
286
620
|
await liveCompletionEvents.stop();
|
|
287
621
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
288
622
|
}
|
|
289
623
|
if (result.exitCode !== 0) {
|
|
290
|
-
|
|
291
|
-
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
624
|
+
throw providerCliExitFailure(command.command, result);
|
|
292
625
|
}
|
|
293
626
|
await context.emit({
|
|
294
627
|
role: "runner",
|
|
@@ -297,9 +630,12 @@ export class ProcessBackedProviderCliService {
|
|
|
297
630
|
eventType: "provider_cli_completed"
|
|
298
631
|
});
|
|
299
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
|
+
}
|
|
300
636
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
301
|
-
if (developCycle || debugCycle || phase === "implementation") {
|
|
302
|
-
await this.git
|
|
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);
|
|
303
639
|
await context.emit({
|
|
304
640
|
role: "runner",
|
|
305
641
|
kind: "checkpoint",
|
|
@@ -357,18 +693,23 @@ export class ProcessBackedProviderCliService {
|
|
|
357
693
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
358
694
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
359
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");
|
|
360
|
-
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));
|
|
361
700
|
const command = buildProviderCliCommand(dispatch, {
|
|
362
701
|
worktreePath: worktree.worktreePath,
|
|
363
|
-
prompt:
|
|
364
|
-
|
|
702
|
+
prompt: architectPrompt,
|
|
703
|
+
promptFromStdin: pipePromptToStdin,
|
|
704
|
+
completionTools,
|
|
705
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
365
706
|
});
|
|
366
|
-
const commandText =
|
|
707
|
+
const commandText = formatProviderCliCommandForCheckpoint(command);
|
|
367
708
|
await context.emit({
|
|
368
709
|
role: "runner",
|
|
369
710
|
kind: "checkpoint",
|
|
370
711
|
content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
|
|
371
|
-
eventType: "
|
|
712
|
+
eventType: "provider_cli_starting"
|
|
372
713
|
});
|
|
373
714
|
await context.emit({
|
|
374
715
|
role: "runner",
|
|
@@ -377,30 +718,36 @@ export class ProcessBackedProviderCliService {
|
|
|
377
718
|
eventType: "completion_tools_configured"
|
|
378
719
|
});
|
|
379
720
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
721
|
+
const toolHeartbeat = new ProviderToolActivityHeartbeat(outputState, context.emit, this.providerToolHeartbeatIntervalMs);
|
|
380
722
|
const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
|
|
381
723
|
let result;
|
|
382
724
|
try {
|
|
383
725
|
liveArchitectEvents.start();
|
|
384
|
-
result = await this.processRunner.run({
|
|
726
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
385
727
|
command: command.command,
|
|
386
728
|
args: command.args,
|
|
387
729
|
cwd: worktree.worktreePath,
|
|
388
|
-
env: providerProcessEnv(dispatch)
|
|
730
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
731
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
389
732
|
}, {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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)
|
|
395
742
|
}, context.signal);
|
|
396
743
|
}
|
|
397
744
|
finally {
|
|
745
|
+
toolHeartbeat.stop();
|
|
398
746
|
await liveArchitectEvents.stop();
|
|
399
747
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
400
748
|
}
|
|
401
749
|
if (result.exitCode !== 0) {
|
|
402
|
-
|
|
403
|
-
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
750
|
+
throw providerCliExitFailure(command.command, result);
|
|
404
751
|
}
|
|
405
752
|
await context.emit({
|
|
406
753
|
role: "runner",
|
|
@@ -448,18 +795,23 @@ export class ProcessBackedProviderCliService {
|
|
|
448
795
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
449
796
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
450
797
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
451
|
-
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
|
|
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));
|
|
452
802
|
const command = buildProviderCliCommand(dispatch, {
|
|
453
803
|
worktreePath: worktree.worktreePath,
|
|
454
|
-
prompt:
|
|
455
|
-
|
|
804
|
+
prompt: qaPrompt,
|
|
805
|
+
promptFromStdin: pipePromptToStdin,
|
|
806
|
+
completionTools,
|
|
807
|
+
runtimeEnvironment: context.runtimeEnvironment
|
|
456
808
|
});
|
|
457
|
-
const commandText =
|
|
809
|
+
const commandText = formatProviderCliCommandForCheckpoint(command);
|
|
458
810
|
await context.emit({
|
|
459
811
|
role: "runner",
|
|
460
812
|
kind: "checkpoint",
|
|
461
813
|
content: `Starting provider CLI QA: ${commandText} in ${worktree.worktreePath}`,
|
|
462
|
-
eventType: "
|
|
814
|
+
eventType: "provider_cli_starting"
|
|
463
815
|
});
|
|
464
816
|
await context.emit({
|
|
465
817
|
role: "runner",
|
|
@@ -468,30 +820,39 @@ export class ProcessBackedProviderCliService {
|
|
|
468
820
|
eventType: "completion_tools_configured"
|
|
469
821
|
});
|
|
470
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);
|
|
471
825
|
const liveQaEvents = new LiveQaArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, dispatch, context.emit);
|
|
472
826
|
let result;
|
|
473
827
|
try {
|
|
474
828
|
liveQaEvents.start();
|
|
475
|
-
|
|
829
|
+
verificationProgress.start();
|
|
830
|
+
result = await (context.processRunner ?? this.processRunner).run({
|
|
476
831
|
command: command.command,
|
|
477
832
|
args: command.args,
|
|
478
833
|
cwd: worktree.worktreePath,
|
|
479
|
-
env: providerProcessEnv(dispatch)
|
|
834
|
+
env: providerProcessEnv(dispatch, context.runtimeEnvironment),
|
|
835
|
+
stdin: pipePromptToStdin ? providerPrompt : undefined
|
|
480
836
|
}, {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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)
|
|
486
846
|
}, context.signal);
|
|
487
847
|
}
|
|
488
848
|
finally {
|
|
849
|
+
toolHeartbeat.stop();
|
|
850
|
+
await verificationProgress.stop();
|
|
489
851
|
await liveQaEvents.stop();
|
|
490
852
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
491
853
|
}
|
|
492
854
|
if (result.exitCode !== 0) {
|
|
493
|
-
|
|
494
|
-
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
855
|
+
throw providerCliExitFailure(command.command, result);
|
|
495
856
|
}
|
|
496
857
|
await context.emit({
|
|
497
858
|
role: "runner",
|
|
@@ -507,7 +868,7 @@ export class ProcessBackedProviderCliService {
|
|
|
507
868
|
? "Provider CLI QA completed but did not produce the complete durable QA artifact bundle."
|
|
508
869
|
: "Provider CLI QA completed but did not call the Forge QA completion tool.");
|
|
509
870
|
}
|
|
510
|
-
enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
|
|
871
|
+
await enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
|
|
511
872
|
if (!liveQaEvents.hasEmitted()) {
|
|
512
873
|
await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
|
|
513
874
|
await emitQaStructuredEvents(context.emit, qaArtifact);
|
|
@@ -531,56 +892,237 @@ export class ProcessBackedProviderCliService {
|
|
|
531
892
|
artifacts: [qaArtifact]
|
|
532
893
|
};
|
|
533
894
|
}
|
|
895
|
+
async runCiRepair(context) {
|
|
896
|
+
return await this.runPullRequestRepair(context, "ci_repair");
|
|
897
|
+
}
|
|
534
898
|
async runConflictResolve(context) {
|
|
899
|
+
return await this.runPullRequestRepair(context, "conflict_resolve");
|
|
900
|
+
}
|
|
901
|
+
async runPullRequestRepair(context, route) {
|
|
535
902
|
const { dispatch, worktree } = context;
|
|
536
|
-
const
|
|
903
|
+
const { expectedRepairHeadSha, preparedHeadSha } = await assertRepairHeadEpoch(context, this.git);
|
|
904
|
+
const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
|
|
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));
|
|
537
950
|
const command = buildProviderCliCommand(dispatch, {
|
|
538
951
|
worktreePath: worktree.worktreePath,
|
|
539
|
-
prompt:
|
|
540
|
-
|
|
541
|
-
|
|
952
|
+
prompt: repairPrompt,
|
|
953
|
+
promptFromStdin: pipePromptToStdin,
|
|
954
|
+
completionTools: completionTools ?? undefined,
|
|
955
|
+
runtimeEnvironment: repairEnvironment
|
|
542
956
|
});
|
|
543
957
|
await context.emit({
|
|
544
958
|
role: "runner",
|
|
545
959
|
kind: "checkpoint",
|
|
546
|
-
content: `Starting provider CLI ${addressComments ? "PR comment address" : "conflict resolve"}: ${
|
|
547
|
-
eventType: "
|
|
960
|
+
content: `Starting provider CLI ${repairChecks ? "CI repair" : addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommandForCheckpoint(command)} in ${worktree.worktreePath}`,
|
|
961
|
+
eventType: "provider_cli_starting"
|
|
548
962
|
});
|
|
549
963
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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);
|
|
559
984
|
}
|
|
985
|
+
}, context.signal);
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
toolHeartbeat.stop();
|
|
989
|
+
if (completionTools) {
|
|
990
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
560
991
|
}
|
|
561
|
-
}
|
|
992
|
+
}
|
|
562
993
|
if (result.exitCode !== 0) {
|
|
563
|
-
|
|
564
|
-
|
|
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.`);
|
|
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);
|
|
565
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));
|
|
566
1019
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
567
|
-
|
|
1020
|
+
if (providerOwnsBranchPublication(context)) {
|
|
1021
|
+
await pushWorktreeBranch(this.git, worktree.worktreePath, worktree.branchName, worktree.repoPath, this.publicationCoordinator, context.signal, context.emit, expectedRepairHeadSha);
|
|
1022
|
+
}
|
|
568
1023
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
569
1024
|
await context.emit({
|
|
570
1025
|
role: "assistant",
|
|
571
1026
|
kind: "message",
|
|
572
|
-
content: outputState.finalMessage || (
|
|
573
|
-
? `${dispatch.execution.provider} CLI
|
|
574
|
-
:
|
|
575
|
-
|
|
1027
|
+
content: outputState.finalMessage || (repairChecks
|
|
1028
|
+
? `${dispatch.execution.provider} CLI repaired pull request checks for ${worktree.branchName}.`
|
|
1029
|
+
: addressComments
|
|
1030
|
+
? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
|
|
1031
|
+
: `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
|
|
1032
|
+
eventType: repairChecks ? "pr_checks_complete" : addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
|
|
576
1033
|
});
|
|
577
1034
|
return {
|
|
578
|
-
summary:
|
|
579
|
-
? `${dispatch.execution.provider} CLI
|
|
580
|
-
:
|
|
1035
|
+
summary: repairChecks
|
|
1036
|
+
? `${dispatch.execution.provider} CLI repaired PR checks on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
|
|
1037
|
+
: addressComments
|
|
1038
|
+
? `${dispatch.execution.provider} CLI addressed PR comments 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] : [],
|
|
581
1043
|
};
|
|
582
1044
|
}
|
|
583
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
|
+
}
|
|
1057
|
+
async function stageDraftAttachments(dispatch, metadataRoot) {
|
|
1058
|
+
const attachments = dispatch.draft?.attachments ?? [];
|
|
1059
|
+
const stageable = attachments.filter((attachment) => (attachment.kind === "text" && typeof attachment.textContent === "string")
|
|
1060
|
+
|| (attachment.kind === "image" && typeof attachment.contentBase64 === "string"));
|
|
1061
|
+
if (stageable.length === 0) {
|
|
1062
|
+
return { textReferences: [], imagePaths: [] };
|
|
1063
|
+
}
|
|
1064
|
+
const attachmentRoot = path.join(metadataRoot, "draft-attachments");
|
|
1065
|
+
const worktreeRoot = path.resolve(metadataRoot, "../../..");
|
|
1066
|
+
await fs.mkdir(attachmentRoot, { recursive: true });
|
|
1067
|
+
const textReferences = [];
|
|
1068
|
+
const imagePaths = [];
|
|
1069
|
+
for (const attachment of stageable) {
|
|
1070
|
+
const fileName = `${String(attachment.index + 1).padStart(2, "0")}-${safeAttachmentFileName(attachment.fileName)}`;
|
|
1071
|
+
const absolutePath = path.join(attachmentRoot, fileName);
|
|
1072
|
+
if (attachment.kind === "text") {
|
|
1073
|
+
await fs.writeFile(absolutePath, attachment.textContent ?? "", { encoding: "utf8", mode: 0o600 });
|
|
1074
|
+
textReferences.push({ index: attachment.index, path: path.relative(worktreeRoot, absolutePath).replaceAll(path.sep, "/") });
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const imageContent = decodeDraftImageAttachment(attachment.contentBase64 ?? "", attachment.fileName, attachment.index);
|
|
1078
|
+
await fs.writeFile(absolutePath, imageContent, { mode: 0o600 });
|
|
1079
|
+
imagePaths.push(absolutePath);
|
|
1080
|
+
}
|
|
1081
|
+
return { textReferences, imagePaths };
|
|
1082
|
+
}
|
|
1083
|
+
function decodeDraftImageAttachment(contentBase64, fileName, index) {
|
|
1084
|
+
const normalized = contentBase64.replace(/\s+/gu, "");
|
|
1085
|
+
if (normalized.length === 0 || normalized.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(normalized)) {
|
|
1086
|
+
throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
|
|
1087
|
+
}
|
|
1088
|
+
const decoded = Buffer.from(normalized, "base64");
|
|
1089
|
+
if (decoded.length === 0 || decoded.toString("base64") !== normalized) {
|
|
1090
|
+
throw new Error(`Draft image attachment ${index + 1} (${fileName}) is not valid base64.`);
|
|
1091
|
+
}
|
|
1092
|
+
return decoded;
|
|
1093
|
+
}
|
|
1094
|
+
function safeAttachmentFileName(fileName) {
|
|
1095
|
+
const sanitized = path.basename(fileName || "attachment.txt")
|
|
1096
|
+
.replace(/[^a-zA-Z0-9._-]/gu, "_")
|
|
1097
|
+
.replace(/[. ]+$/gu, "") || "attachment.txt";
|
|
1098
|
+
const maxFileNameLength = 120;
|
|
1099
|
+
const clipped = clipFileName(sanitized, maxFileNameLength);
|
|
1100
|
+
const extension = path.extname(clipped);
|
|
1101
|
+
const stem = path.basename(clipped, extension).replace(/[. ]+$/gu, "") || "attachment";
|
|
1102
|
+
const windowsReservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
|
|
1103
|
+
const safeStem = stem === "." || stem === ".." || windowsReservedName.test(stem) ? `attachment-${stem.replaceAll(".", "_")}` : stem;
|
|
1104
|
+
return clipFileName(`${safeStem}${extension}`, maxFileNameLength);
|
|
1105
|
+
}
|
|
1106
|
+
function clipFileName(fileName, maxLength) {
|
|
1107
|
+
if (fileName.length <= maxLength) {
|
|
1108
|
+
return fileName;
|
|
1109
|
+
}
|
|
1110
|
+
const originalExtension = path.extname(fileName);
|
|
1111
|
+
const extension = originalExtension.length > maxLength - 1
|
|
1112
|
+
? originalExtension.slice(0, maxLength - 1)
|
|
1113
|
+
: originalExtension;
|
|
1114
|
+
const stem = path.basename(fileName, originalExtension);
|
|
1115
|
+
const maxStemLength = Math.max(1, maxLength - extension.length);
|
|
1116
|
+
return `${stem.slice(0, maxStemLength)}${extension}`;
|
|
1117
|
+
}
|
|
1118
|
+
const providerPromptStdinThreshold = 8_000;
|
|
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";
|
|
1125
|
+
}
|
|
584
1126
|
function isFakeWorkerModel(model) {
|
|
585
1127
|
return model.trim().toLowerCase() === fakeWorkerModel;
|
|
586
1128
|
}
|