@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.
Files changed (118) hide show
  1. package/README.md +34 -0
  2. package/dist/bridge/bridge.base.d.ts +2 -1
  3. package/dist/bridge/bridge.service.d.ts +22 -3
  4. package/dist/bridge/bridge.service.js +416 -40
  5. package/dist/bridge/bridge.types.d.ts +59 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +37 -3
  14. package/dist/cli/cli.service.d.ts +30 -2
  15. package/dist/cli/cli.service.js +182 -23
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +41 -1
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +17 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +101 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +86 -0
  23. package/dist/runtime/attempt-process-supervisor.js +343 -0
  24. package/dist/runtime/generated-output-reset.d.ts +10 -0
  25. package/dist/runtime/generated-output-reset.js +103 -0
  26. package/dist/runtime/host-listener-inspector.d.ts +44 -0
  27. package/dist/runtime/host-listener-inspector.js +324 -0
  28. package/dist/runtime/host-port-broker.d.ts +76 -0
  29. package/dist/runtime/host-port-broker.js +333 -0
  30. package/dist/runtime/host-runtime-contract.d.ts +47 -0
  31. package/dist/runtime/host-runtime-contract.js +700 -0
  32. package/dist/runtime/host-runtime-lease.d.ts +133 -0
  33. package/dist/runtime/host-runtime-lease.js +1460 -0
  34. package/dist/runtime/host-runtime-promotion.d.ts +23 -0
  35. package/dist/runtime/host-runtime-promotion.js +159 -0
  36. package/dist/runtime/node-host-runtime-adapter.d.ts +51 -0
  37. package/dist/runtime/node-host-runtime-adapter.js +268 -0
  38. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  39. package/dist/runtime/playwright-host-runtime-adapter.js +220 -0
  40. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  41. package/dist/runtime/runtime-isolation-lease.js +158 -0
  42. package/dist/sandbox/preflight.d.ts +14 -0
  43. package/dist/sandbox/preflight.js +91 -12
  44. package/dist/sandbox/process.js +125 -40
  45. package/dist/sandbox/repository-lock.js +37 -1
  46. package/dist/sandbox/runtime.d.ts +66 -3
  47. package/dist/sandbox/runtime.js +670 -105
  48. package/dist/sandbox/sandbox.types.d.ts +3 -0
  49. package/dist/sandbox/smoke.d.ts +76 -0
  50. package/dist/sandbox/smoke.js +380 -0
  51. package/dist/sandbox/worker-supervisor.js +1 -1
  52. package/dist/sandbox/worker.js +2 -5
  53. package/dist/sandbox/workspace.service.d.ts +1 -0
  54. package/dist/sandbox/workspace.service.js +28 -5
  55. package/dist/session/managed-attempt-tools.d.ts +12 -0
  56. package/dist/session/managed-attempt-tools.js +350 -0
  57. package/dist/session/phase-completion.artifacts.js +9 -2
  58. package/dist/session/phase-completion.d.ts +4 -2
  59. package/dist/session/phase-completion.helpers.js +27 -3
  60. package/dist/session/phase-completion.js +12 -3
  61. package/dist/session/phase-completion.prompts.d.ts +3 -3
  62. package/dist/session/phase-completion.prompts.js +19 -5
  63. package/dist/session/phase-completion.runtime.d.ts +12 -1
  64. package/dist/session/phase-completion.runtime.js +127 -7
  65. package/dist/session/phase-completion.schemas.js +108 -3
  66. package/dist/session/phase-completion.validation.d.ts +4 -1
  67. package/dist/session/phase-completion.validation.js +106 -2
  68. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  69. package/dist/session/provider-cli/ci-repair-readiness.js +90 -0
  70. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  71. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  72. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  73. package/dist/session/provider-cli/provider-cli.process.d.ts +16 -0
  74. package/dist/session/provider-cli/provider-cli.process.js +212 -25
  75. package/dist/session/provider-cli/provider-cli.prompts.d.ts +4 -1
  76. package/dist/session/provider-cli/provider-cli.prompts.js +84 -17
  77. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  78. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  79. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  80. package/dist/session/provider-cli/provider-cli.service.d.ts +3 -2
  81. package/dist/session/provider-cli/provider-cli.service.js +254 -71
  82. package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
  83. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  84. package/dist/session/provider-cli/provider-output-state.js +30 -2
  85. package/dist/session/session-redaction.d.ts +3 -0
  86. package/dist/session/session-redaction.js +23 -0
  87. package/dist/session/session.base.d.ts +6 -1
  88. package/dist/session/session.service.d.ts +8 -2
  89. package/dist/session/session.service.js +436 -41
  90. package/dist/session/session.types.d.ts +55 -0
  91. package/dist/session/session.types.js +69 -2
  92. package/dist/shared/path/path.js +3 -1
  93. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  94. package/dist/shared/process/host-process-recovery.js +78 -0
  95. package/dist/shared/process/host-process-supervisor.d.ts +57 -0
  96. package/dist/shared/process/host-process-supervisor.js +282 -0
  97. package/dist/shared/process/process.d.ts +1 -0
  98. package/dist/shared/process/process.js +1 -0
  99. package/dist/shared/redaction/redaction.js +6 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.js +1 -1
  102. package/dist/worktree/artifact-injector.d.ts +3 -2
  103. package/dist/worktree/artifact-injector.js +43 -4
  104. package/dist/worktree/dependency-warmer.d.ts +10 -1
  105. package/dist/worktree/dependency-warmer.js +94 -5
  106. package/dist/worktree/index.d.ts +1 -0
  107. package/dist/worktree/index.js +1 -0
  108. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  109. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  110. package/dist/worktree/worktree-paths.d.ts +4 -0
  111. package/dist/worktree/worktree-paths.js +30 -0
  112. package/dist/worktree/worktree.base.d.ts +3 -0
  113. package/dist/worktree/worktree.service.d.ts +7 -2
  114. package/dist/worktree/worktree.service.js +72 -44
  115. package/dist/worktree/worktree.types.d.ts +3 -1
  116. package/package.json +3 -2
  117. package/runtime/windows-job-supervisor.ps1 +356 -0
  118. 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
- await git.run(worktreePath, ["reset", "--", ".forge"]).catch(() => "");
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 no app launch target was provided; provider must return blocked with blocker_reason missing_app_launch_target.");
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 launch = await resolveProviderProcessLaunch(request.command, request.args);
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
- child.kill("SIGTERM");
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
- try {
171
- child.kill("SIGTERM");
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
- if (request.stdin !== undefined) {
185
- if (!child.stdin) {
186
- try {
187
- child.kill("SIGTERM");
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
- catch {
190
- // Best effort; the missing stdin pipe will fail the provider run.
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
- try {
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
- stdout += `${line}\n`;
242
+ if (request.captureOutput !== false)
243
+ stdout += `${line}\n`;
209
244
  return handlers.onStdoutLine?.(line);
210
245
  }),
211
246
  readProcessLines(childStderr, (line) => {
212
- stderr += `${line}\n`;
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
- Promise.allSettled(readers).then((readerResults) => {
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
- return resolved ? buildWindowsLaunchCommand(resolved, args) : { command, args };
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 codexSandboxModeForPlatform(platform?: NodeJS.Platform): "workspace-write" | "danger-full-access";
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;