@loucompanion/forge-bridge 0.1.3-dev.ba7d36726eda → 0.1.3-dev.bca7d0ccee53

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 (73) hide show
  1. package/dist/bridge/bridge.base.d.ts +2 -1
  2. package/dist/bridge/bridge.service.d.ts +8 -0
  3. package/dist/bridge/bridge.service.js +184 -12
  4. package/dist/bridge/bridge.types.d.ts +37 -0
  5. package/dist/cli/cli.help.js +1 -1
  6. package/dist/cli/cli.service.js +1 -1
  7. package/dist/cli/local-config.js +2 -1
  8. package/dist/runtime/attempt-node-runtime-launcher.d.ts +3 -1
  9. package/dist/runtime/attempt-node-runtime-launcher.js +62 -11
  10. package/dist/runtime/attempt-process-supervisor-registry.js +1 -1
  11. package/dist/runtime/attempt-process-supervisor.d.ts +12 -0
  12. package/dist/runtime/attempt-process-supervisor.js +75 -15
  13. package/dist/runtime/generated-output-reset.d.ts +10 -0
  14. package/dist/runtime/generated-output-reset.js +103 -0
  15. package/dist/runtime/host-listener-inspector.d.ts +16 -0
  16. package/dist/runtime/host-listener-inspector.js +143 -4
  17. package/dist/runtime/host-runtime-contract.d.ts +9 -1
  18. package/dist/runtime/host-runtime-contract.js +411 -6
  19. package/dist/runtime/host-runtime-lease.d.ts +12 -1
  20. package/dist/runtime/host-runtime-lease.js +271 -50
  21. package/dist/runtime/host-runtime-promotion.d.ts +2 -0
  22. package/dist/runtime/host-runtime-promotion.js +35 -4
  23. package/dist/runtime/node-host-runtime-adapter.d.ts +7 -0
  24. package/dist/runtime/node-host-runtime-adapter.js +71 -8
  25. package/dist/runtime/playwright-host-runtime-adapter.js +84 -0
  26. package/dist/sandbox/preflight.d.ts +9 -0
  27. package/dist/sandbox/preflight.js +39 -4
  28. package/dist/sandbox/workspace.service.js +7 -2
  29. package/dist/session/managed-attempt-tools.d.ts +17 -0
  30. package/dist/session/managed-attempt-tools.js +712 -0
  31. package/dist/session/phase-completion.artifacts.js +9 -2
  32. package/dist/session/phase-completion.d.ts +11 -2
  33. package/dist/session/phase-completion.helpers.d.ts +5 -0
  34. package/dist/session/phase-completion.helpers.js +45 -9
  35. package/dist/session/phase-completion.js +12 -3
  36. package/dist/session/phase-completion.prompts.d.ts +3 -3
  37. package/dist/session/phase-completion.prompts.js +21 -6
  38. package/dist/session/phase-completion.runtime.d.ts +13 -1
  39. package/dist/session/phase-completion.runtime.js +183 -8
  40. package/dist/session/phase-completion.schemas.js +108 -3
  41. package/dist/session/phase-completion.validation.d.ts +4 -1
  42. package/dist/session/phase-completion.validation.js +144 -18
  43. package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
  44. package/dist/session/provider-cli/ci-repair-readiness.js +95 -0
  45. package/dist/session/provider-cli/provider-cli.deterministic.js +47 -1
  46. package/dist/session/provider-cli/provider-cli.process.d.ts +14 -0
  47. package/dist/session/provider-cli/provider-cli.process.js +176 -18
  48. package/dist/session/provider-cli/provider-cli.prompts.js +30 -8
  49. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +1 -0
  50. package/dist/session/provider-cli/provider-cli.service.d.ts +19 -2
  51. package/dist/session/provider-cli/provider-cli.service.js +390 -83
  52. package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
  53. package/dist/session/provider-cli/provider-output-state.d.ts +2 -0
  54. package/dist/session/provider-cli/provider-output-state.js +30 -2
  55. package/dist/session/session.base.d.ts +2 -1
  56. package/dist/session/session.service.d.ts +2 -1
  57. package/dist/session/session.service.js +303 -30
  58. package/dist/session/session.types.d.ts +38 -0
  59. package/dist/session/session.types.js +17 -0
  60. package/dist/shared/process/host-process-supervisor.d.ts +18 -1
  61. package/dist/shared/process/host-process-supervisor.js +94 -7
  62. package/dist/shared/process/process.d.ts +1 -0
  63. package/dist/shared/process/process.js +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/dist/worktree/artifact-injector.d.ts +3 -2
  67. package/dist/worktree/artifact-injector.js +43 -4
  68. package/dist/worktree/dependency-warmer.d.ts +10 -1
  69. package/dist/worktree/dependency-warmer.js +94 -5
  70. package/dist/worktree/worktree.service.d.ts +1 -0
  71. package/dist/worktree/worktree.service.js +12 -1
  72. package/package.json +1 -1
  73. package/runtime/windows-job-supervisor.ps1 +89 -20
@@ -163,19 +163,11 @@ export class StreamingProviderProcessRunner {
163
163
  reject(new Error("Provider process stdout/stderr streams were not piped."));
164
164
  return;
165
165
  }
166
- try {
167
- if (supervisedLaunch) {
168
- persistHostProcessSupervisorMetadata(supervisedLaunch, request, child.pid ?? 0, rootCreatedAt);
169
- }
170
- }
171
- catch (error) {
172
- void terminate();
173
- reject(error);
174
- return;
175
- }
176
166
  let stdout = "";
177
167
  let stderr = "";
178
168
  let settled = false;
169
+ let closed = false;
170
+ let readiness = null;
179
171
  const cleanup = () => {
180
172
  signal?.removeEventListener("abort", abort);
181
173
  };
@@ -185,11 +177,14 @@ export class StreamingProviderProcessRunner {
185
177
  }
186
178
  settled = true;
187
179
  cleanup();
188
- void terminate();
180
+ void terminate().catch(() => undefined);
189
181
  reject(error);
190
182
  };
191
183
  const abort = () => {
192
- void terminate();
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);
193
188
  };
194
189
  if (signal?.aborted) {
195
190
  abort();
@@ -198,7 +193,27 @@ export class StreamingProviderProcessRunner {
198
193
  signal?.addEventListener("abort", abort, { once: true });
199
194
  }
200
195
  child.on("error", fail);
201
- if (request.stdin !== undefined) {
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);
203
+ }
204
+ await handlers.onStarted?.();
205
+ if (supervisedLaunch?.strategy === "windows_job_object") {
206
+ grantWindowsProviderStart(supervisedLaunch);
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;
202
217
  if (!child.stdin) {
203
218
  void terminate();
204
219
  fail(new Error("Provider process stdin stream was not piped."));
@@ -209,6 +224,18 @@ export class StreamingProviderProcessRunner {
209
224
  fail(error);
210
225
  });
211
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
+ }
212
239
  }
213
240
  const readers = [
214
241
  readProcessLines(childStdout, (line) => {
@@ -223,7 +250,9 @@ export class StreamingProviderProcessRunner {
223
250
  })
224
251
  ];
225
252
  child.on("close", (code) => {
253
+ closed = true;
226
254
  Promise.allSettled(readers).then(async (readerResults) => {
255
+ await readiness?.catch(() => undefined);
227
256
  const rejected = readerResults.find((result) => result.status === "rejected");
228
257
  if (rejected?.status === "rejected") {
229
258
  fail(rejected.reason);
@@ -232,11 +261,17 @@ export class StreamingProviderProcessRunner {
232
261
  if (settled) {
233
262
  return;
234
263
  }
235
- if (supervisedLaunch?.strategy === "unix_process_group") {
236
- await terminate();
264
+ try {
265
+ if (supervisedLaunch?.strategy === "unix_process_group") {
266
+ await terminate();
267
+ }
268
+ else if (termination) {
269
+ await termination;
270
+ }
237
271
  }
238
- else if (termination) {
239
- await termination;
272
+ catch (error) {
273
+ fail(error);
274
+ return;
240
275
  }
241
276
  settled = true;
242
277
  cleanup();
@@ -262,7 +297,58 @@ async function resolveProviderProcessLaunch(command, args) {
262
297
  if (!resolved) {
263
298
  return { command, args };
264
299
  }
265
- return resolveCodexNpmDirectLaunch(resolved, args) ?? buildWindowsLaunchCommand(resolved, args);
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
+ }
266
352
  }
267
353
  export function resolveCodexNpmDirectLaunch(commandPath, args) {
268
354
  const extension = path.extname(commandPath).toLowerCase();
@@ -275,10 +361,48 @@ export function resolveCodexNpmDirectLaunch(commandPath, args) {
275
361
  ? { command: process.execPath, args: [entryPoint, ...args] }
276
362
  : null;
277
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
+ };
397
+ }
278
398
  async function resolveWindowsCommand(command) {
279
399
  if (path.isAbsolute(command)) {
280
400
  return command;
281
401
  }
402
+ const resolvedFromPath = resolveWindowsCommandFromPath(command);
403
+ if (resolvedFromPath) {
404
+ return resolvedFromPath;
405
+ }
282
406
  try {
283
407
  const result = await execFileAsync("where.exe", [command], { timeout: 2000 });
284
408
  return chooseWindowsCommandCandidate(result.stdout.split(/\r?\n/));
@@ -287,6 +411,40 @@ async function resolveWindowsCommand(command) {
287
411
  return null;
288
412
  }
289
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
+ }
290
448
  export function chooseWindowsCommandCandidate(candidates) {
291
449
  const normalized = candidates
292
450
  .map((candidate) => candidate.trim())
@@ -212,6 +212,8 @@ export function buildProviderCliPrompt(dispatch, prompt, runtimeEnvironment) {
212
212
  - \`FORGE_RUNTIME_MODE=host\`, \`FORGE_RESOURCE_LEASE_ID\`, and \`FORGE_DISPATCH_ATTEMPT_ID\` identify the fenced runtime lease. \`FORGE_WORKTREE_PATH\` identifies the prepared repository root.
213
213
  ${concurrency}
214
214
  - Forge owns this attempt's process tree through \`FORGE_PROCESS_SUPERVISOR_ID\`. Do not detach, adopt pre-existing servers, or kill processes by name or port; cancellation and teardown are supervisor-owned.
215
+ - Prefer \`forge.start_managed_verification\` for Forge .NET tiers or the solution suite so the runner owns cwd, arguments, isolated output, timeout, and supervision. Use \`forge.start_managed_command\` only for other commands expected to run longer than two minutes. Continue independent work, then call \`forge.wait_managed_command\` once for its terminal receipt; do not poll \`forge.get_managed_command\`. Forge keeps live progress in its durable UI receipt and returns one terminal result; its timeout terminates and verifies the exact attempt-owned process tree. Do not run those commands through the generic shell tool.
216
+ - If a tool creates \`TestResults\`, \`test-results\`, or \`playwright-report\` beneath source, use \`forge.reset_generated_outputs\` to quarantine only those untracked generated directories. Do not issue a recursive delete.
215
217
  - Use the attempt-scoped \`FORGE_TMPDIR\`, \`FORGE_LOG_DIR\`, \`FORGE_CACHE_DIR\`, \`FORGE_BROWSER_PROFILE_DIR\`, and \`FORGE_TEST_OUTPUT_DIR\` paths instead of shared host paths.
216
218
  - Use supplied \`FORGE_*\` URLs, ports, and paths. Do not search for a free port, kill a process occupying one, or reuse an existing dev server, browser profile, database, container, network, or volume.
217
219
  - Never run broad process or Docker cleanup. Stop only resources you created for this resource lease.${playwright}`;
@@ -421,13 +423,13 @@ export function qaBrowserInstruction(dispatch) {
421
423
  : "";
422
424
  const launchTarget = qaBrowser.launchTarget;
423
425
  if (qaBrowser.required && !hasLaunchTarget(launchTarget ?? null)) {
424
- return `Browser/app launch context:\n- UI/browser QA is required for this item by ${scopeAuthority}.\n- No configured app launch target was provided.${scopeEvidence}${playwrightPolicy}\n- If a managed or repository-owned Playwright workflow successfully exercises the affected UI, record both its passing command and browser_checks evidence. Otherwise return blocked with blocker_reason: missing_app_launch_target instead of pretending browser testing occurred.\n- Artifact sidecars path when available: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
426
+ return `Browser/app launch context:\n- UI/browser QA is required for this item by ${scopeAuthority}.\n- No configured app launch target was provided.${scopeEvidence}${playwrightPolicy}\n- If a managed or repository-owned Playwright workflow successfully exercises the affected UI, record both its passing command and browser_checks evidence. Otherwise return blocked with blocker_reason: missing_app_launch_target instead of pretending browser testing occurred.\n- Artifact sidecars path when available: ${qaSidecarRoot(dispatch)}`;
425
427
  }
426
428
  if (!launchTarget) {
427
- return `Browser/app launch context:\n- UI/browser QA is not required by ${scopeAuthority} for this item.\n- If the diff or acceptance criteria require browser testing, use a managed or repository-owned Playwright workflow when available; otherwise return blocked with blocker_reason: missing_app_launch_target.${scopeEvidence}${playwrightPolicy}\n- Browser sidecars path: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
429
+ return `Browser/app launch context:\n- UI/browser QA is not required by ${scopeAuthority} for this item.\n- If the diff or acceptance criteria require browser testing, use a managed or repository-owned Playwright workflow when available; otherwise return blocked with blocker_reason: missing_app_launch_target.${scopeEvidence}${playwrightPolicy}\n- Browser sidecars path: ${qaSidecarRoot(dispatch)}`;
428
430
  }
429
431
  const routes = launchTarget.routes.length > 0 ? launchTarget.routes.join(", ") : "not specified";
430
- return `Browser/app launch context:\n- Browser QA required: ${qaBrowser.required ? `yes, by ${scopeAuthority}` : `no by ${scopeAuthority}, unless the diff/AC requires it`}\n- URL: ${launchTarget.url || "not specified"}\n- Ready URL: ${launchTarget.ready_url || "not specified"}\n- Launch command: ${launchTarget.command || "not specified"}\n- Working directory: ${launchTarget.working_directory || "repo root"}\n- Routes to cover: ${routes}\n- Notes: ${launchTarget.notes || "none"}${scopeEvidence}${playwrightPolicy}\n- Browser sidecars path: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
432
+ return `Browser/app launch context:\n- Browser QA required: ${qaBrowser.required ? `yes, by ${scopeAuthority}` : `no by ${scopeAuthority}, unless the diff/AC requires it`}\n- URL: ${launchTarget.url || "not specified"}\n- Ready URL: ${launchTarget.ready_url || "not specified"}\n- Launch command: ${launchTarget.command || "not specified"}\n- Working directory: ${launchTarget.working_directory || "repo root"}\n- Routes to cover: ${routes}\n- Notes: ${launchTarget.notes || "none"}${scopeEvidence}${playwrightPolicy}\n- Browser sidecars path: ${qaSidecarRoot(dispatch)}`;
431
433
  }
432
434
  export function storyQaInstruction(dispatch) {
433
435
  const storyQa = dispatch.storyQa;
@@ -463,6 +465,8 @@ ${finalQa}
463
465
  This is parent Story QA: inspect the integrated Story result, all current-plan child specs/evidence, and the final worktree state. Do not treat the final child task as separately signed off.`;
464
466
  }
465
467
  export function buildQaPrompt(dispatch, laneSpec) {
468
+ const sidecarRoot = qaSidecarRoot(dispatch);
469
+ const artifactRoot = sidecarRoot.replace(/\/qa\/?$/i, "");
466
470
  return `You are Forge's QA lane running locally in a prepared git worktree.
467
471
 
468
472
  First read and follow the injected QA lane command at .forge/work-items/${dispatch.workItem.id}/spec.md. It defines the required gated QA skill workflow for this run.
@@ -487,18 +491,28 @@ System info / environment notes:
487
491
  ${dispatch.workItem.systemInfo || "Not provided."}
488
492
 
489
493
  ${storyQaInstruction(dispatch)}
490
- Inspect the validated implementation and Forge artifacts under .forge/work-items/${dispatch.workItem.id}/artifacts/ when present, especially implementation, self-review, and validation evidence.
494
+ Inspect the validated implementation evidence referenced by the dispatch and the owner-scoped QA artifacts under ${artifactRoot}/ when present.
491
495
  ${qaBrowserInstruction(dispatch)}
492
496
  ${qaReworkRetestInstruction(dispatch)}
493
497
  QA is read-mostly: inspect code, run commands, and exercise browser/app flows when available, but do not edit product source or tests.
494
- Record concrete evidence. For UI/browser work, include browser_checks with URLs/routes, actions, observed results, screenshots/traces when available, console errors, failed network requests, and responsive checks. Store browser sidecars under .forge/work-items/${dispatch.workItem.id}/artifacts/qa/.
498
+ Record concrete evidence. For UI/browser work, include browser_checks with URLs/routes, actions, observed results, screenshots/traces when available, console errors, failed network requests, and responsive checks. Store browser sidecars under ${sidecarRoot}/.
495
499
  Report in-scope defects as actionable findings with repro steps, expected/actual behavior, evidence, likely area, required fix, and required regression.
496
- Out-of-scope or follow-up issues belong in candidate_bugs instead of blocking the current item.
500
+ Out-of-scope or follow-up issues belong in candidate_bugs instead of blocking the current item. If a broad regression command fails only on a surface outside both the current diff and acceptance criteria, reproduce that failure with a focused command. The regression gate may pass only when the current item has independent passing evidence and the failed command is preserved with classification out_of_scope_failure, candidate_bug_id, scope_evidence, acceptance_criteria_independent: true, repro_command, and a non-zero repro_exit_code. Never use this classification for an unexplained failure, a runner/environment failure, or a failure that can affect the current item.
497
501
 
498
- ${completionPromptForQaCycle()}
502
+ ${completionPromptForQaCycle("qa_plan", artifactRoot)}
499
503
 
500
504
  Finish with a concise summary only after forge.complete_qa_verdict accepts the QA bundle.`;
501
505
  }
506
+ function qaSidecarRoot(dispatch) {
507
+ const configured = dispatch.qaBrowser?.artifactsPath?.trim().replace(/\\/g, "/").replace(/\/+$/, "");
508
+ if (configured) {
509
+ return configured;
510
+ }
511
+ const ownerType = dispatch.cycleRun.ownerType?.trim().toLowerCase();
512
+ const ownerId = dispatch.cycleRun.ownerId?.trim() || dispatch.workItem.id;
513
+ const ownerDirectory = ownerType === "story" ? "stories" : "work-items";
514
+ return `.forge/${ownerDirectory}/${ownerId}/artifacts/qa`;
515
+ }
502
516
  export function buildConflictResolvePrompt(dispatch, branchName) {
503
517
  const targetBranch = dispatch.cycleRun.requestedBase || dispatch.repo.defaultBranch || "main";
504
518
  return `You are Forge's Conflict Resolve lane running locally in a prepared git worktree.
@@ -532,12 +546,20 @@ This is not a merge-conflict resolution task. Only fetch/merge/rebase ${targetBr
532
546
 
533
547
  Load and follow .forge/commands/operational/ci-repair-prompt.md, including its CI repair phase gate and validation skill. Inspect the failed provider check logs, identify the exact failed workflow job/step, make the smallest scoped repair, and before pushing run the failed job's local workflow-equivalent gate, including pre-test guard/drift steps when available.
534
548
 
549
+ Forge prepared two required inputs before this agent started:
550
+ - Approved spec: .forge/work-items/${dispatch.workItem.id}/spec.md
551
+ - Readiness contract: .forge/work-items/${dispatch.workItem.id}/ci-repair.readiness.json
552
+
553
+ Read both first. The readiness contract supplies attempt-scoped E2E URLs, Playwright/browser readiness, and any provider-only local-equivalence classifications. A provider-only step is not permission to weaken CI; record why it cannot run inside the managed attempt and leave provider CI authoritative.
554
+
555
+ After repair and validation, call forge.complete_ci_repair with the complete structured handoff. Forge Bridge will validate it and will not commit, push, or report success until that gate passes. If the tool rejects the payload, correct the exact missing evidence it reports and call it again.
556
+
535
557
  Keep the original work item intent intact:
536
558
  - Work item: ${dispatch.workItem.id}
537
559
  - Title: ${dispatch.workItem.title}
538
560
  - Acceptance criteria: ${dispatch.workItem.acceptanceCriteria || "Not specified"}
539
561
 
540
- Finish with a concise summary of failing checks, changed files, validation commands, pushed commit status, and any remaining provider checks still pending.`;
562
+ Forge Bridge owns the final commit and push after the completion gate succeeds. Finish with a concise summary of failing checks, changed files, validation commands, and remaining provider checks.`;
541
563
  }
542
564
  export function buildPullRequestCommentsPrompt(dispatch, branchName) {
543
565
  const pr = dispatch.pullRequest;
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  export function hasLaunchTarget(target) {
5
5
  return Boolean(target?.url?.trim()
6
6
  || target?.ready_url?.trim()
7
+ || target?.managed_runtime_port?.trim()
7
8
  || target?.command?.trim()
8
9
  || (target?.routes ?? []).some((route) => route.trim().length > 0));
9
10
  }
@@ -1,12 +1,27 @@
1
1
  import { GitClient } from "../../shared/git/git.js";
2
+ import type { WorktreeLifecycleCoordinator } from "../../worktree/worktree.base.js";
3
+ import type { RunnerDispatchEvent } from "../session.types.js";
2
4
  import type { ProviderCliService } from "./provider-cli.base.js";
3
5
  import type { ProviderCliDraftContext, ProviderCliRunContext, ProviderCliRunResult } from "./provider-cli.types.js";
4
6
  import { type ProviderProcessRunner } from "./provider-cli.process.js";
5
7
  export { DeterministicProviderCliService } from "./provider-cli.deterministic.js";
6
8
  export { buildProviderCliCommand, codexSandboxModeForPlatform, formatProviderCliCommand, formatProviderCliCommandForCheckpoint } from "./provider-cli.prompts.js";
7
- export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, StreamingProviderProcessRunner } from "./provider-cli.process.js";
9
+ export { buildProviderChildEnvironment, chooseWindowsCommandCandidate, resolveCodexNpmDirectLaunch, resolvePackageManagerDirectLaunch, resolveWindowsCommandFromPath, StreamingProviderProcessRunner } from "./provider-cli.process.js";
8
10
  export { buildWindowsLaunchCommand } from "../../shared/process/windows-launch.js";
9
11
  export type { ProviderCliCommand, ProviderProcessRunner, ProviderProcessRunRequest, ProviderProcessRunResult } from "./provider-cli.process.js";
12
+ export declare class ManagedVerificationProgressEmitter {
13
+ private readonly logDirectory;
14
+ private readonly emit;
15
+ private readonly intervalMs;
16
+ private timer;
17
+ private readonly observedStatuses;
18
+ private readonly completedCommands;
19
+ constructor(logDirectory: string | undefined, emit: (event: RunnerDispatchEvent) => Promise<void>, intervalMs?: number);
20
+ start(): void;
21
+ stop(): Promise<void>;
22
+ private scan;
23
+ private emitTransition;
24
+ }
10
25
  export declare class RoutingProviderCliService implements ProviderCliService {
11
26
  private readonly realProvider;
12
27
  private readonly deterministicProvider;
@@ -21,7 +36,9 @@ export declare class RoutingProviderCliService implements ProviderCliService {
21
36
  export declare class ProcessBackedProviderCliService implements ProviderCliService {
22
37
  private readonly processRunner;
23
38
  private readonly git;
24
- constructor(processRunner?: ProviderProcessRunner, git?: GitClient);
39
+ private readonly providerToolHeartbeatIntervalMs;
40
+ private readonly publicationCoordinator;
41
+ constructor(processRunner?: ProviderProcessRunner, git?: GitClient, providerToolHeartbeatIntervalMs?: number, publicationCoordinator?: WorktreeLifecycleCoordinator);
25
42
  runDraft(context: ProviderCliDraftContext): Promise<ProviderCliRunResult>;
26
43
  runImplementation(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
27
44
  runArchitect(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;