@loucompanion/forge-bridge 0.1.3-dev.d46a8e73d03d → 0.1.3-dev.eeefdb0d19c1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/bridge/bridge.base.d.ts +2 -1
- package/dist/bridge/bridge.service.d.ts +22 -3
- package/dist/bridge/bridge.service.js +416 -40
- package/dist/bridge/bridge.types.d.ts +59 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.js +37 -3
- package/dist/cli/cli.service.d.ts +30 -2
- package/dist/cli/cli.service.js +182 -23
- package/dist/cli/local-config.d.ts +4 -0
- package/dist/cli/local-config.js +41 -1
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
- package/dist/runtime/attempt-process-supervisor.js +343 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +103 -0
- package/dist/runtime/host-listener-inspector.d.ts +44 -0
- package/dist/runtime/host-listener-inspector.js +324 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +47 -0
- package/dist/runtime/host-runtime-contract.js +700 -0
- package/dist/runtime/host-runtime-lease.d.ts +133 -0
- package/dist/runtime/host-runtime-lease.js +1460 -0
- package/dist/runtime/host-runtime-promotion.d.ts +23 -0
- package/dist/runtime/host-runtime-promotion.js +159 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
- package/dist/runtime/node-host-runtime-adapter.js +268 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/preflight.d.ts +14 -0
- package/dist/sandbox/preflight.js +91 -12
- package/dist/sandbox/process.js +125 -40
- package/dist/sandbox/repository-lock.js +37 -1
- package/dist/sandbox/runtime.d.ts +66 -3
- package/dist/sandbox/runtime.js +670 -105
- package/dist/sandbox/sandbox.types.d.ts +3 -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 +12 -0
- package/dist/session/managed-attempt-tools.js +350 -0
- package/dist/session/phase-completion.artifacts.js +9 -2
- package/dist/session/phase-completion.d.ts +4 -2
- package/dist/session/phase-completion.helpers.js +27 -3
- package/dist/session/phase-completion.js +12 -3
- package/dist/session/phase-completion.prompts.d.ts +3 -3
- package/dist/session/phase-completion.prompts.js +19 -5
- package/dist/session/phase-completion.runtime.d.ts +12 -1
- package/dist/session/phase-completion.runtime.js +127 -7
- package/dist/session/phase-completion.schemas.js +108 -3
- package/dist/session/phase-completion.validation.d.ts +4 -1
- package/dist/session/phase-completion.validation.js +106 -2
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
- package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
- package/dist/session/provider-cli/provider-cli.process.d.ts +16 -0
- package/dist/session/provider-cli/provider-cli.process.js +212 -25
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.prompts.js +84 -17
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +3 -2
- package/dist/session/provider-cli/provider-cli.service.js +254 -71
- 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 +30 -2
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -0
- package/dist/session/session.base.d.ts +6 -1
- package/dist/session/session.service.d.ts +8 -2
- package/dist/session/session.service.js +436 -41
- package/dist/session/session.types.d.ts +55 -0
- package/dist/session/session.types.js +69 -2
- package/dist/shared/path/path.js +3 -1
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +57 -0
- package/dist/shared/process/host-process-supervisor.js +282 -0
- package/dist/shared/process/process.d.ts +1 -0
- package/dist/shared/process/process.js +1 -0
- package/dist/shared/redaction/redaction.js +6 -1
- 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 +10 -1
- package/dist/worktree/dependency-warmer.js +94 -5
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +7 -2
- package/dist/worktree/worktree.service.js +72 -44
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +3 -2
- package/runtime/windows-job-supervisor.ps1 +356 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -5,6 +5,7 @@ import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from ".
|
|
|
5
5
|
import { hasLaunchTarget } from "./provider-cli.qa-sidecars.js";
|
|
6
6
|
import { clip, isNodeError, messageFromError, normalizedString, parseJsonLine, stringField } from "./provider-output-state.js";
|
|
7
7
|
const developCyclePhaseOrder = ["implementation", "self_review", "validation"];
|
|
8
|
+
const nonPublishableWorktreePaths = [".forge", ".local"];
|
|
8
9
|
export async function emitDraftGateArtifacts(emit, worktreePath, workItemId) {
|
|
9
10
|
const draftArtifactFiles = {
|
|
10
11
|
draft_intent: "draft.intent.json",
|
|
@@ -147,7 +148,9 @@ export async function cleanupCompletionTools(completionTools, emit) {
|
|
|
147
148
|
}
|
|
148
149
|
export async function commitWorktreeChanges(git, worktreePath, workItemId) {
|
|
149
150
|
await git.run(worktreePath, ["add", "-A"]);
|
|
150
|
-
|
|
151
|
+
for (const reservedPath of nonPublishableWorktreePaths) {
|
|
152
|
+
await git.run(worktreePath, ["reset", "--", reservedPath]).catch(() => "");
|
|
153
|
+
}
|
|
151
154
|
const changedFiles = (await git.run(worktreePath, ["diff", "--cached", "--name-only"]))
|
|
152
155
|
.split(/\r?\n/)
|
|
153
156
|
.map((line) => line.trim())
|
|
@@ -237,18 +240,39 @@ export async function hasCompleteQaArtifactBundle(worktreePath, workItemId, owne
|
|
|
237
240
|
}
|
|
238
241
|
return true;
|
|
239
242
|
}
|
|
240
|
-
export function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
|
|
243
|
+
export async function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
|
|
241
244
|
const qaBrowser = dispatch.qaBrowser;
|
|
242
245
|
if (!qaBrowser?.required || hasLaunchTarget(qaBrowser.launchTarget ?? null)) {
|
|
243
246
|
return;
|
|
244
247
|
}
|
|
248
|
+
const evidence = await readJsonObjectFile(path.join(path.dirname(artifact.path), qaArtifactFileNames.evidence));
|
|
249
|
+
if (hasPassingRepositoryPlaywrightEvidence(evidence)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
245
252
|
const verdict = parseJsonLine(artifact.verdictJson) ?? {};
|
|
246
253
|
const status = normalizedString(stringField(verdict, "status") ?? artifact.status ?? "");
|
|
247
254
|
const blockerReason = normalizedString(stringField(verdict, "blocker_reason") ?? "");
|
|
248
255
|
if (status !== "blocked" || blockerReason !== "missing_app_launch_target") {
|
|
249
|
-
throw new Error("QA browser testing is required but
|
|
256
|
+
throw new Error("QA browser testing is required but neither an app launch target nor passing repository Playwright evidence was provided; provider must return blocked with blocker_reason missing_app_launch_target.");
|
|
250
257
|
}
|
|
251
258
|
}
|
|
259
|
+
function hasPassingRepositoryPlaywrightEvidence(evidence) {
|
|
260
|
+
const passingBrowserCheck = arrayField(evidence, "browser_checks").some(check => normalizedString(stringField(check, "status") ?? "") === "pass"
|
|
261
|
+
&& Boolean(stringField(check, "url") ?? stringField(check, "route")));
|
|
262
|
+
if (!passingBrowserCheck) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
return arrayField(evidence, "commands").some(command => {
|
|
266
|
+
const status = normalizedString(stringField(command, "status") ?? "");
|
|
267
|
+
const exitCode = command.exit_code;
|
|
268
|
+
const description = [stringField(command, "command"), stringField(command, "summary")]
|
|
269
|
+
.filter((value) => Boolean(value))
|
|
270
|
+
.join(" ");
|
|
271
|
+
return status === "pass"
|
|
272
|
+
&& exitCode === 0
|
|
273
|
+
&& /(?:playwright|test:e2e|e2e(?:\s|:|$)|browser(?:\s|-)?test)/iu.test(description);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
252
276
|
export async function validateQaBundleSidecarReferences(worktreePath, workItemId, ownerType, ownerId) {
|
|
253
277
|
const artifactRoot = ownerType && ownerId
|
|
254
278
|
? artifactRootForOwner(worktreePath, ownerType, ownerId)
|
|
@@ -444,7 +468,7 @@ export class LiveQaArtifactEmitter {
|
|
|
444
468
|
if (!artifact) {
|
|
445
469
|
return;
|
|
446
470
|
}
|
|
447
|
-
enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
|
|
471
|
+
await enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
|
|
448
472
|
this.emittedBundle = true;
|
|
449
473
|
await emitQaArtifactRecorded(this.emit, this.worktreePath, artifact);
|
|
450
474
|
await emitQaStructuredEvents(this.emit, artifact);
|
|
@@ -8,6 +8,7 @@ import { deterministicBrowserCheck, deterministicBrowserSidecar, hasLaunchTarget
|
|
|
8
8
|
import { buildProviderCliCommand, formatProviderCliCommand, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch, singleLine } from "./provider-cli.prompts.js";
|
|
9
9
|
import { estimatedDeterministicUsage, exactRedactionSecrets } from "./provider-cli.runtime.js";
|
|
10
10
|
import { buildProviderUsageEvent } from "./provider-usage.js";
|
|
11
|
+
import { writeCiRepairReadinessManifest } from "./ci-repair-readiness.js";
|
|
11
12
|
import { deterministicArchitectPayload, deterministicCompletionPayload, deterministicQaGatePayload } from "./provider-cli.deterministic-payloads.js";
|
|
12
13
|
export class DeterministicProviderCliService {
|
|
13
14
|
git;
|
|
@@ -212,6 +213,31 @@ export class DeterministicProviderCliService {
|
|
|
212
213
|
const { dispatch, worktree } = context;
|
|
213
214
|
const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
|
|
214
215
|
const addressComments = isPullRequestCommentsDispatch(dispatch);
|
|
216
|
+
let repairArtifact = null;
|
|
217
|
+
let repairSpec = null;
|
|
218
|
+
if (repairChecks) {
|
|
219
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
220
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
221
|
+
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
222
|
+
if (!laneCommand)
|
|
223
|
+
throw new Error("Deterministic CI repair could not materialize the approved lane spec.");
|
|
224
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, "ci_repair");
|
|
225
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
226
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
227
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), `${JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" })}\n`, "utf8");
|
|
228
|
+
repairSpec = { repoPath: worktree.worktreePath, path: specPath, body: specBody, producedBy: "forge-lane-command", status: "approved" };
|
|
229
|
+
await writeCiRepairReadinessManifest(dispatch, worktree.worktreePath, specPath, context.runtimeEnvironment);
|
|
230
|
+
const reference = await writeCompletionArtifact({
|
|
231
|
+
worktreePath: worktree.worktreePath,
|
|
232
|
+
workItemId: dispatch.workItem.id,
|
|
233
|
+
phase: "ci_repair",
|
|
234
|
+
producedBy: "forge-deterministic-provider",
|
|
235
|
+
}, expectedCompletionToolName("ci_repair"), deterministicCiRepairPayload());
|
|
236
|
+
repairArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "ci_repair");
|
|
237
|
+
if (!repairArtifact)
|
|
238
|
+
throw new Error(`Deterministic CI repair did not produce ${reference.path}.`);
|
|
239
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, repairArtifact);
|
|
240
|
+
}
|
|
215
241
|
await this.git.run(worktree.worktreePath, ["commit", "--allow-empty", "-m", `forge bridge ${repairChecks ? "ci repair" : addressComments ? "pr comments" : "conflict resolve"} for ${dispatch.workItem.id.slice(0, 8)}`]);
|
|
216
242
|
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
217
243
|
await context.emit({
|
|
@@ -229,10 +255,30 @@ export class DeterministicProviderCliService {
|
|
|
229
255
|
? `forge-bridge repaired PR checks for ${worktree.branchName}.`
|
|
230
256
|
: addressComments
|
|
231
257
|
? `forge-bridge addressed PR comments for ${worktree.branchName}.`
|
|
232
|
-
: `forge-bridge resolved conflicts for ${worktree.branchName}
|
|
258
|
+
: `forge-bridge resolved conflicts for ${worktree.branchName}.`,
|
|
259
|
+
spec: repairSpec,
|
|
260
|
+
artifact: repairArtifact,
|
|
261
|
+
artifacts: repairArtifact ? [repairArtifact] : [],
|
|
233
262
|
};
|
|
234
263
|
}
|
|
235
264
|
}
|
|
265
|
+
function deterministicCiRepairPayload() {
|
|
266
|
+
const command = { command: "forge deterministic ci-repair validation", exit_code: 0, status: "pass", summary: "Deterministic CI repair fixture passed." };
|
|
267
|
+
return {
|
|
268
|
+
status: "pass",
|
|
269
|
+
summary: "Deterministic CI repair handoff is ready for provider checks.",
|
|
270
|
+
failed_checks: [{ name: "CI / deterministic", provider_state: "failure", workflow: ".github/workflows/ci.yml", job: "deterministic", step: "fixture" }],
|
|
271
|
+
root_cause: { classification: "test expectation mismatch", summary: "Deterministic fixture failure repaired.", evidence: ["Fixture provider reported the expected failed check."] },
|
|
272
|
+
changed_files: [],
|
|
273
|
+
no_code_action: { action: "deterministic_fixture", reason: "The deterministic provider models the repair without a product diff." },
|
|
274
|
+
narrow_validation: [command],
|
|
275
|
+
workflow_validation: [{ step: "deterministic fixture", equivalence: "supported", command: command.command, exit_code: 0, status: "pass", summary: command.summary }],
|
|
276
|
+
broader_validation: [command],
|
|
277
|
+
remaining_risks: [],
|
|
278
|
+
provider_state: "ready_for_rerun",
|
|
279
|
+
ready_for_push: true,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
236
282
|
function deterministicDraftPayload(dispatch) {
|
|
237
283
|
const kind = dispatch.draft?.kind ?? "work_item";
|
|
238
284
|
if (kind === "work_order_decomposition") {
|
|
@@ -6,6 +6,7 @@ export interface ProviderProcessRunRequest {
|
|
|
6
6
|
readonly cwd: string;
|
|
7
7
|
readonly env?: Readonly<Record<string, string>>;
|
|
8
8
|
readonly stdin?: string;
|
|
9
|
+
readonly captureOutput?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export interface ProviderProcessRunResult {
|
|
11
12
|
readonly exitCode: number;
|
|
@@ -16,6 +17,7 @@ export interface ProviderProcessRunner {
|
|
|
16
17
|
run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
|
|
17
18
|
}
|
|
18
19
|
export interface ProviderProcessHandlers {
|
|
20
|
+
onStarted?(): Promise<void> | void;
|
|
19
21
|
onStdoutLine?(line: string): Promise<void> | void;
|
|
20
22
|
onStderrLine?(line: string): Promise<void> | void;
|
|
21
23
|
}
|
|
@@ -28,5 +30,19 @@ export declare function buildProviderChildEnvironment(requestEnv?: Readonly<Reco
|
|
|
28
30
|
export declare class StreamingProviderProcessRunner implements ProviderProcessRunner {
|
|
29
31
|
run(request: ProviderProcessRunRequest, handlers?: ProviderProcessHandlers, signal?: AbortSignal): Promise<ProviderProcessRunResult>;
|
|
30
32
|
}
|
|
33
|
+
export declare function resolveCodexNpmDirectLaunch(commandPath: string, args: readonly string[]): LaunchCommand | null;
|
|
34
|
+
/**
|
|
35
|
+
* Avoids cmd.exe's nested-quote handling for package-manager shims. That
|
|
36
|
+
* quoting loses executable paths containing spaces after the command is
|
|
37
|
+
* wrapped by the Windows Job Object supervisor.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolvePackageManagerDirectLaunch(commandPath: string, args: readonly string[]): LaunchCommand | null;
|
|
40
|
+
/**
|
|
41
|
+
* Resolves Windows commands without spawning where.exe. Runtime launches can
|
|
42
|
+
* happen while the machine is under enough process pressure for where.exe to
|
|
43
|
+
* miss its timeout; returning the unresolved package-manager name in that case
|
|
44
|
+
* makes Node fall back to a shell shim that cannot survive Job Object wrapping.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveWindowsCommandFromPath(command: string, pathValue?: string, pathExtValue?: string): string | null;
|
|
31
47
|
export declare function chooseWindowsCommandCandidate(candidates: readonly string[]): string | null;
|
|
32
48
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import readline from "node:readline";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
8
|
+
import { persistHostProcessSupervisorMetadata, resolveHostProcessSupervisedLaunch, terminateHostProcessTree, } from "../../shared/process/host-process-supervisor.js";
|
|
7
9
|
import { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
9
11
|
const allowedProviderParentEnvNames = [
|
|
@@ -137,24 +139,35 @@ function setEnvironmentValueIfMissing(env, name, value) {
|
|
|
137
139
|
}
|
|
138
140
|
export class StreamingProviderProcessRunner {
|
|
139
141
|
async run(request, handlers = {}, signal) {
|
|
140
|
-
const
|
|
142
|
+
const providerLaunch = await resolveProviderProcessLaunch(request.command, request.args);
|
|
143
|
+
const supervisedLaunch = resolveHostProcessSupervisedLaunch(request, providerLaunch.command, providerLaunch.args);
|
|
144
|
+
const launch = supervisedLaunch ?? { ...providerLaunch, detached: false };
|
|
141
145
|
return await new Promise((resolve, reject) => {
|
|
146
|
+
const rootCreatedAt = new Date().toISOString();
|
|
142
147
|
const child = spawn(launch.command, launch.args, {
|
|
143
148
|
cwd: request.cwd,
|
|
144
149
|
env: buildProviderChildEnvironment(request.env),
|
|
145
150
|
stdio: [request.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
146
|
-
windowsHide: true
|
|
151
|
+
windowsHide: true,
|
|
152
|
+
detached: launch.detached,
|
|
147
153
|
});
|
|
154
|
+
let termination = null;
|
|
155
|
+
const terminate = () => {
|
|
156
|
+
termination ??= terminateHostProcessTree(child, supervisedLaunch);
|
|
157
|
+
return termination;
|
|
158
|
+
};
|
|
148
159
|
const childStdout = child.stdout;
|
|
149
160
|
const childStderr = child.stderr;
|
|
150
161
|
if (!childStdout || !childStderr) {
|
|
151
|
-
|
|
162
|
+
void terminate();
|
|
152
163
|
reject(new Error("Provider process stdout/stderr streams were not piped."));
|
|
153
164
|
return;
|
|
154
165
|
}
|
|
155
166
|
let stdout = "";
|
|
156
167
|
let stderr = "";
|
|
157
168
|
let settled = false;
|
|
169
|
+
let closed = false;
|
|
170
|
+
let readiness = null;
|
|
158
171
|
const cleanup = () => {
|
|
159
172
|
signal?.removeEventListener("abort", abort);
|
|
160
173
|
};
|
|
@@ -164,15 +177,14 @@ export class StreamingProviderProcessRunner {
|
|
|
164
177
|
}
|
|
165
178
|
settled = true;
|
|
166
179
|
cleanup();
|
|
180
|
+
void terminate().catch(() => undefined);
|
|
167
181
|
reject(error);
|
|
168
182
|
};
|
|
169
183
|
const abort = () => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
catch
|
|
174
|
-
// Best effort; close/error will settle the promise.
|
|
175
|
-
}
|
|
184
|
+
// Cancellation is not a launch failure when the supervised tree can be
|
|
185
|
+
// proven stopped. Let the close handler settle the command after exact
|
|
186
|
+
// termination; only a failed termination proof rejects the run.
|
|
187
|
+
void terminate().catch(fail);
|
|
176
188
|
};
|
|
177
189
|
if (signal?.aborted) {
|
|
178
190
|
abort();
|
|
@@ -181,40 +193,66 @@ export class StreamingProviderProcessRunner {
|
|
|
181
193
|
signal?.addEventListener("abort", abort, { once: true });
|
|
182
194
|
}
|
|
183
195
|
child.on("error", fail);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
child.once("spawn", () => {
|
|
197
|
+
readiness = (async () => {
|
|
198
|
+
const providerReady = supervisedLaunch?.strategy === "windows_job_object"
|
|
199
|
+
? await waitForWindowsProviderReady(supervisedLaunch, signal, () => closed)
|
|
200
|
+
: undefined;
|
|
201
|
+
if (supervisedLaunch) {
|
|
202
|
+
persistHostProcessSupervisorMetadata(supervisedLaunch, request, child.pid ?? 0, rootCreatedAt, providerReady);
|
|
188
203
|
}
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
await handlers.onStarted?.();
|
|
205
|
+
if (supervisedLaunch?.strategy === "windows_job_object") {
|
|
206
|
+
grantWindowsProviderStart(supervisedLaunch);
|
|
191
207
|
}
|
|
208
|
+
})();
|
|
209
|
+
void readiness.catch(error => {
|
|
210
|
+
if (!closed && !signal?.aborted)
|
|
211
|
+
fail(error);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
const writeProviderInput = () => {
|
|
215
|
+
if (request.stdin === undefined)
|
|
216
|
+
return;
|
|
217
|
+
if (!child.stdin) {
|
|
218
|
+
void terminate();
|
|
192
219
|
fail(new Error("Provider process stdin stream was not piped."));
|
|
193
220
|
return;
|
|
194
221
|
}
|
|
195
222
|
child.stdin.on("error", (error) => {
|
|
196
|
-
|
|
197
|
-
child.kill("SIGTERM");
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// Best effort; the stdin error will fail the provider run.
|
|
201
|
-
}
|
|
223
|
+
void terminate();
|
|
202
224
|
fail(error);
|
|
203
225
|
});
|
|
204
226
|
child.stdin.end(request.stdin, "utf8");
|
|
227
|
+
};
|
|
228
|
+
if (request.stdin !== undefined) {
|
|
229
|
+
if (supervisedLaunch?.strategy === "windows_job_object") {
|
|
230
|
+
// The outer supervisor must receive stdin before it can launch the
|
|
231
|
+
// inner provider. Its start-grant file prevents prompt delivery.
|
|
232
|
+
writeProviderInput();
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
child.once("spawn", () => {
|
|
236
|
+
void readiness?.then(writeProviderInput).catch(fail);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
205
239
|
}
|
|
206
240
|
const readers = [
|
|
207
241
|
readProcessLines(childStdout, (line) => {
|
|
208
|
-
|
|
242
|
+
if (request.captureOutput !== false)
|
|
243
|
+
stdout += `${line}\n`;
|
|
209
244
|
return handlers.onStdoutLine?.(line);
|
|
210
245
|
}),
|
|
211
246
|
readProcessLines(childStderr, (line) => {
|
|
212
|
-
|
|
247
|
+
if (request.captureOutput !== false)
|
|
248
|
+
stderr += `${line}\n`;
|
|
213
249
|
return handlers.onStderrLine?.(line);
|
|
214
250
|
})
|
|
215
251
|
];
|
|
216
252
|
child.on("close", (code) => {
|
|
217
|
-
|
|
253
|
+
closed = true;
|
|
254
|
+
Promise.allSettled(readers).then(async (readerResults) => {
|
|
255
|
+
await readiness?.catch(() => undefined);
|
|
218
256
|
const rejected = readerResults.find((result) => result.status === "rejected");
|
|
219
257
|
if (rejected?.status === "rejected") {
|
|
220
258
|
fail(rejected.reason);
|
|
@@ -223,6 +261,18 @@ export class StreamingProviderProcessRunner {
|
|
|
223
261
|
if (settled) {
|
|
224
262
|
return;
|
|
225
263
|
}
|
|
264
|
+
try {
|
|
265
|
+
if (supervisedLaunch?.strategy === "unix_process_group") {
|
|
266
|
+
await terminate();
|
|
267
|
+
}
|
|
268
|
+
else if (termination) {
|
|
269
|
+
await termination;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
fail(error);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
226
276
|
settled = true;
|
|
227
277
|
cleanup();
|
|
228
278
|
resolve({
|
|
@@ -244,12 +294,115 @@ async function resolveProviderProcessLaunch(command, args) {
|
|
|
244
294
|
return cursorDirect;
|
|
245
295
|
}
|
|
246
296
|
const resolved = await resolveWindowsCommand(command);
|
|
247
|
-
|
|
297
|
+
if (!resolved) {
|
|
298
|
+
return { command, args };
|
|
299
|
+
}
|
|
300
|
+
return resolveCodexNpmDirectLaunch(resolved, args)
|
|
301
|
+
?? resolvePackageManagerDirectLaunch(resolved, args)
|
|
302
|
+
?? buildWindowsLaunchCommand(resolved, args);
|
|
303
|
+
}
|
|
304
|
+
function grantWindowsProviderStart(launch) {
|
|
305
|
+
if (!launch.startGrantPath) {
|
|
306
|
+
throw new Error("host_process_supervisor_invalid: Windows provider start-grant path was not assigned.");
|
|
307
|
+
}
|
|
308
|
+
fs.writeFileSync(launch.startGrantPath, "granted\n", {
|
|
309
|
+
encoding: "utf8",
|
|
310
|
+
flag: "wx",
|
|
311
|
+
mode: 0o600,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async function waitForWindowsProviderReady(launch, signal, isClosed) {
|
|
315
|
+
const readyPath = launch.readyPath;
|
|
316
|
+
if (!readyPath) {
|
|
317
|
+
throw new Error("host_process_supervisor_invalid: Windows provider-ready path was not assigned.");
|
|
318
|
+
}
|
|
319
|
+
while (true) {
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(await fs.promises.readFile(readyPath, "utf8"));
|
|
322
|
+
if (parsed.schemaVersion !== 1
|
|
323
|
+
|| parsed.supervisorId !== launch.supervisorId
|
|
324
|
+
|| !Number.isSafeInteger(parsed.rootPid)
|
|
325
|
+
|| (parsed.rootPid ?? 0) <= 0
|
|
326
|
+
|| typeof parsed.rootIdentity !== "string"
|
|
327
|
+
|| !/^win32:\d+$/u.test(parsed.rootIdentity)
|
|
328
|
+
|| !Number.isSafeInteger(parsed.providerPid)
|
|
329
|
+
|| (parsed.providerPid ?? 0) <= 0
|
|
330
|
+
|| typeof parsed.providerCreatedAt !== "string"
|
|
331
|
+
|| Number.isNaN(Date.parse(parsed.providerCreatedAt))
|
|
332
|
+
|| (parsed.providerIdentity !== undefined
|
|
333
|
+
&& (typeof parsed.providerIdentity !== "string" || !/^win32:\d+$/u.test(parsed.providerIdentity)))
|
|
334
|
+
|| !Number.isSafeInteger(parsed.startAttempt)
|
|
335
|
+
|| (parsed.startAttempt ?? 0) < 1) {
|
|
336
|
+
throw new Error("host_process_supervisor_invalid: provider-ready receipt was invalid.");
|
|
337
|
+
}
|
|
338
|
+
return parsed;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (error.code !== "ENOENT")
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
if (isClosed()) {
|
|
345
|
+
throw new Error("host_process_supervisor_start_failed: supervisor exited before the provider became ready.");
|
|
346
|
+
}
|
|
347
|
+
if (signal?.aborted) {
|
|
348
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Provider process launch was aborted.");
|
|
349
|
+
}
|
|
350
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export function resolveCodexNpmDirectLaunch(commandPath, args) {
|
|
354
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
355
|
+
if ((extension !== ".cmd" && extension !== ".ps1")
|
|
356
|
+
|| path.basename(commandPath, extension).toLowerCase() !== "codex") {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const entryPoint = path.join(path.dirname(commandPath), "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
360
|
+
return fs.existsSync(entryPoint)
|
|
361
|
+
? { command: process.execPath, args: [entryPoint, ...args] }
|
|
362
|
+
: null;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Avoids cmd.exe's nested-quote handling for package-manager shims. That
|
|
366
|
+
* quoting loses executable paths containing spaces after the command is
|
|
367
|
+
* wrapped by the Windows Job Object supervisor.
|
|
368
|
+
*/
|
|
369
|
+
export function resolvePackageManagerDirectLaunch(commandPath, args) {
|
|
370
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
371
|
+
if (extension !== ".cmd" && extension !== ".ps1")
|
|
372
|
+
return null;
|
|
373
|
+
const packageManager = path.basename(commandPath, extension).toLowerCase();
|
|
374
|
+
if (!["npm", "npx", "pnpm", "pnpx", "yarn", "yarnpkg"].includes(packageManager))
|
|
375
|
+
return null;
|
|
376
|
+
const root = path.dirname(commandPath);
|
|
377
|
+
const candidates = [
|
|
378
|
+
path.join(root, "node_modules", "corepack", "dist", `${packageManager}.js`),
|
|
379
|
+
...(packageManager === "npm" || packageManager === "npx"
|
|
380
|
+
? [path.join(root, "node_modules", "npm", "bin", `${packageManager}-cli.js`)]
|
|
381
|
+
: []),
|
|
382
|
+
...(packageManager === "pnpm" || packageManager === "pnpx"
|
|
383
|
+
? [path.join(root, "node_modules", "pnpm", "bin", `${packageManager}.cjs`)]
|
|
384
|
+
: []),
|
|
385
|
+
...(packageManager === "yarn" || packageManager === "yarnpkg"
|
|
386
|
+
? [path.join(root, "node_modules", "yarn", "bin", "yarn.js")]
|
|
387
|
+
: []),
|
|
388
|
+
];
|
|
389
|
+
const entryPoint = candidates.find(candidate => fs.existsSync(candidate));
|
|
390
|
+
if (!entryPoint)
|
|
391
|
+
return null;
|
|
392
|
+
const adjacentNode = path.join(root, "node.exe");
|
|
393
|
+
return {
|
|
394
|
+
command: fs.existsSync(adjacentNode) ? adjacentNode : process.execPath,
|
|
395
|
+
args: [entryPoint, ...args],
|
|
396
|
+
};
|
|
248
397
|
}
|
|
249
398
|
async function resolveWindowsCommand(command) {
|
|
250
399
|
if (path.isAbsolute(command)) {
|
|
251
400
|
return command;
|
|
252
401
|
}
|
|
402
|
+
const resolvedFromPath = resolveWindowsCommandFromPath(command);
|
|
403
|
+
if (resolvedFromPath) {
|
|
404
|
+
return resolvedFromPath;
|
|
405
|
+
}
|
|
253
406
|
try {
|
|
254
407
|
const result = await execFileAsync("where.exe", [command], { timeout: 2000 });
|
|
255
408
|
return chooseWindowsCommandCandidate(result.stdout.split(/\r?\n/));
|
|
@@ -258,6 +411,40 @@ async function resolveWindowsCommand(command) {
|
|
|
258
411
|
return null;
|
|
259
412
|
}
|
|
260
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Resolves Windows commands without spawning where.exe. Runtime launches can
|
|
416
|
+
* happen while the machine is under enough process pressure for where.exe to
|
|
417
|
+
* miss its timeout; returning the unresolved package-manager name in that case
|
|
418
|
+
* makes Node fall back to a shell shim that cannot survive Job Object wrapping.
|
|
419
|
+
*/
|
|
420
|
+
export function resolveWindowsCommandFromPath(command, pathValue = process.env.PATH ?? "", pathExtValue = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
|
|
421
|
+
if (!command || path.isAbsolute(command) || command.includes("/") || command.includes("\\")) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const hasExtension = path.extname(command).length > 0;
|
|
425
|
+
const extensions = hasExtension
|
|
426
|
+
? [""]
|
|
427
|
+
: [...new Set([
|
|
428
|
+
"",
|
|
429
|
+
".exe",
|
|
430
|
+
".ps1",
|
|
431
|
+
".cmd",
|
|
432
|
+
".bat",
|
|
433
|
+
...pathExtValue.split(";").map(value => value.trim().toLowerCase()).filter(Boolean),
|
|
434
|
+
])];
|
|
435
|
+
const candidates = [];
|
|
436
|
+
for (const rawDirectory of pathValue.split(path.delimiter)) {
|
|
437
|
+
const directory = rawDirectory.trim().replace(/^"|"$/gu, "");
|
|
438
|
+
if (!directory)
|
|
439
|
+
continue;
|
|
440
|
+
for (const extension of extensions) {
|
|
441
|
+
const candidate = path.join(directory, `${command}${extension}`);
|
|
442
|
+
if (fs.existsSync(candidate))
|
|
443
|
+
candidates.push(candidate);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return chooseWindowsCommandCandidate(candidates);
|
|
447
|
+
}
|
|
261
448
|
export function chooseWindowsCommandCandidate(candidates) {
|
|
262
449
|
const normalized = candidates
|
|
263
450
|
.map((candidate) => candidate.trim())
|
|
@@ -17,12 +17,15 @@ export declare function formatProviderCliCommandForCheckpoint(command: ProviderC
|
|
|
17
17
|
export declare function singleLine(value: string): string;
|
|
18
18
|
export declare function buildProviderCliCommand(dispatch: RunOnceDispatchPacket, run?: {
|
|
19
19
|
readonly prompt?: string;
|
|
20
|
+
readonly promptFromStdin?: boolean;
|
|
20
21
|
readonly worktreePath?: string;
|
|
21
22
|
readonly completionTools?: CompletionToolRuntime;
|
|
22
23
|
readonly skipGitRepoCheck?: boolean;
|
|
23
24
|
readonly imagePaths?: readonly string[];
|
|
25
|
+
readonly runtimeEnvironment?: Readonly<Record<string, string>>;
|
|
24
26
|
}): ProviderCliCommand;
|
|
25
|
-
export declare function
|
|
27
|
+
export declare function buildProviderCliPrompt(dispatch: RunOnceDispatchPacket, prompt: string, runtimeEnvironment?: Readonly<Record<string, string>>): string;
|
|
28
|
+
export declare function codexSandboxModeForPlatform(platform?: NodeJS.Platform, executionMode?: string): "workspace-write" | "danger-full-access";
|
|
26
29
|
export declare function buildArchitectPrompt(dispatch: RunOnceDispatchPacket): string;
|
|
27
30
|
export declare function architectDependencyInstruction(dispatch: RunOnceDispatchPacket): string;
|
|
28
31
|
export declare function designerRefInstruction(dispatch: RunOnceDispatchPacket): string;
|