@loucompanion/forge-bridge 0.1.1-dev.718bdb0018b9 → 0.1.1-dev.895607f4402a

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.
@@ -8,7 +8,7 @@ import { GitClient } from "../../shared/git/git.js";
8
8
  import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
9
9
  import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from "../qa-sidecar-validation.js";
10
10
  import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
11
- import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDevelopCycle, completionPromptForPhase, completionRedactionEnv, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDevelopCycleCompletion, writeCompletionArtifact, artifactRootFor } from "../phase-completion.js";
11
+ import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDebugCycle, completionPromptForDevelopCycle, completionPromptForPhase, completionPromptForQaCycle, completionRedactionEnv, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact, artifactRootFor, debugCompletionPhaseOrder, qaCompletionGateOrder } from "../phase-completion.js";
12
12
  import { addUsage, buildProviderUsageEvent, emptyUsage, hasUsage, readUsageNumber } from "./provider-usage.js";
13
13
  const fakeWorkerModel = "server-local-fake-worker";
14
14
  const execFileAsync = promisify(execFile);
@@ -47,6 +47,7 @@ export class ProcessBackedProviderCliService {
47
47
  const { dispatch, worktree } = context;
48
48
  const phase = completionPhaseFromDispatch(dispatch);
49
49
  const developCycle = usesDevelopCycleCompletion(dispatch);
50
+ const debugCycle = usesDebugCycleCompletion(dispatch);
50
51
  const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
51
52
  await fs.mkdir(metadataRoot, { recursive: true });
52
53
  const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
@@ -60,7 +61,11 @@ export class ProcessBackedProviderCliService {
60
61
  const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
61
62
  const command = buildProviderCliCommand(dispatch, {
62
63
  worktreePath: worktree.worktreePath,
63
- prompt: developCycle ? buildDevelopCyclePrompt(dispatch, phase) : buildImplementationPrompt(dispatch, phase),
64
+ prompt: debugCycle
65
+ ? buildDebugCyclePrompt(dispatch, phase)
66
+ : developCycle
67
+ ? buildDevelopCyclePrompt(dispatch, phase)
68
+ : buildImplementationPrompt(dispatch, phase),
64
69
  completionTools
65
70
  });
66
71
  await context.emit({
@@ -72,13 +77,15 @@ export class ProcessBackedProviderCliService {
72
77
  await context.emit({
73
78
  role: "runner",
74
79
  kind: "checkpoint",
75
- content: developCycle
76
- ? "Forge completion tools configured for implementation -> self_review -> validation; provider must call each MCP completion tool before exit."
77
- : `Forge completion tools configured for ${phase}; provider must call the MCP completion tool before exit.`,
80
+ content: debugCycle
81
+ ? "Forge Debug completion tools configured for debug_repro -> debug_root_cause -> debug_fix -> debug_regression; provider must call each MCP completion tool before exit."
82
+ : developCycle
83
+ ? "Forge completion tools configured for implementation -> self_review -> validation; provider must call each MCP completion tool before exit."
84
+ : `Forge completion tools configured for ${phase}; provider must call the MCP completion tool before exit.`,
78
85
  eventType: "completion_tools_configured"
79
86
  });
80
87
  const outputState = createProviderOutputState(dispatch.execution.provider);
81
- const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, context.emit);
88
+ const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
82
89
  let result;
83
90
  try {
84
91
  liveCompletionEvents.start();
@@ -112,7 +119,7 @@ export class ProcessBackedProviderCliService {
112
119
  });
113
120
  await emitProviderUsageEvent(context.emit, dispatch, outputState);
114
121
  const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
115
- const completionArtifacts = await readCompletionArtifactsForRun(worktree.worktreePath, dispatch.workItem.id, phase, developCycle);
122
+ const completionArtifacts = await readCompletionArtifactsForRun(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle);
116
123
  for (const artifact of completionArtifacts) {
117
124
  if (!liveCompletionEvents.hasEmitted(completionPhaseFromArtifact(artifact))) {
118
125
  await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, artifact);
@@ -138,7 +145,7 @@ export class ProcessBackedProviderCliService {
138
145
  });
139
146
  }
140
147
  return {
141
- summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}, ${committed.commitSha.slice(0, 12)}).`,
148
+ summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${debugCycle ? "debug cycle" : developCycle ? "develop cycle" : phase}, ${committed.commitSha.slice(0, 12)}).`,
142
149
  spec: {
143
150
  repoPath: worktree.worktreePath,
144
151
  path: specPath,
@@ -152,10 +159,20 @@ export class ProcessBackedProviderCliService {
152
159
  }
153
160
  async runQa(context) {
154
161
  const { dispatch, worktree } = context;
162
+ const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
163
+ await fs.mkdir(metadataRoot, { recursive: true });
164
+ const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
165
+ if (!laneCommand) {
166
+ throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
167
+ }
168
+ const specBody = buildSpecBody(laneCommand, dispatch.workItem, "qa");
169
+ const specPath = path.join(metadataRoot, "spec.md");
170
+ await fs.writeFile(specPath, specBody, "utf8");
171
+ await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
155
172
  const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
156
173
  const command = buildProviderCliCommand(dispatch, {
157
174
  worktreePath: worktree.worktreePath,
158
- prompt: buildQaPrompt(dispatch),
175
+ prompt: buildQaPrompt(dispatch, specBody),
159
176
  completionTools
160
177
  });
161
178
  const commandText = formatProviderCliCommand(command);
@@ -168,7 +185,7 @@ export class ProcessBackedProviderCliService {
168
185
  await context.emit({
169
186
  role: "runner",
170
187
  kind: "checkpoint",
171
- content: "Forge QA completion tool configured; provider must call forge.complete_qa before exit.",
188
+ content: "Forge QA completion tools configured; provider must call all seven QA MCP gates before exit.",
172
189
  eventType: "completion_tools_configured"
173
190
  });
174
191
  const outputState = createProviderOutputState(dispatch.execution.provider);
@@ -225,20 +242,30 @@ export class ProcessBackedProviderCliService {
225
242
  : "found rework";
226
243
  return {
227
244
  summary: `${dispatch.execution.provider} CLI QA ${statusSummary}.`,
245
+ spec: {
246
+ repoPath: worktree.worktreePath,
247
+ path: specPath,
248
+ body: specBody,
249
+ producedBy: "forge-lane-command",
250
+ status: "approved"
251
+ },
228
252
  artifact: qaArtifact,
229
253
  artifacts: [qaArtifact]
230
254
  };
231
255
  }
232
256
  async runConflictResolve(context) {
233
257
  const { dispatch, worktree } = context;
258
+ const addressComments = isPullRequestCommentsDispatch(dispatch);
234
259
  const command = buildProviderCliCommand(dispatch, {
235
260
  worktreePath: worktree.worktreePath,
236
- prompt: buildConflictResolvePrompt(dispatch, worktree.branchName)
261
+ prompt: addressComments
262
+ ? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
263
+ : buildConflictResolvePrompt(dispatch, worktree.branchName)
237
264
  });
238
265
  await context.emit({
239
266
  role: "runner",
240
267
  kind: "checkpoint",
241
- content: `Starting provider CLI conflict resolve: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
268
+ content: `Starting provider CLI ${addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
242
269
  eventType: "provider_cli_started"
243
270
  });
244
271
  const outputState = createProviderOutputState(dispatch.execution.provider);
@@ -264,11 +291,15 @@ export class ProcessBackedProviderCliService {
264
291
  await context.emit({
265
292
  role: "assistant",
266
293
  kind: "message",
267
- content: outputState.finalMessage || `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`,
268
- eventType: "conflict_resolve_complete"
294
+ content: outputState.finalMessage || (addressComments
295
+ ? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
296
+ : `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
297
+ eventType: addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
269
298
  });
270
299
  return {
271
- summary: `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
300
+ summary: addressComments
301
+ ? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
302
+ : `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
272
303
  };
273
304
  }
274
305
  }
@@ -281,6 +312,7 @@ export class DeterministicProviderCliService {
281
312
  const { dispatch, worktree } = context;
282
313
  const phase = completionPhaseFromDispatch(dispatch);
283
314
  const developCycle = usesDevelopCycleCompletion(dispatch);
315
+ const debugCycle = usesDebugCycleCompletion(dispatch);
284
316
  const providerCommand = buildProviderCliCommand(dispatch);
285
317
  const providerCommandText = formatProviderCliCommand(providerCommand);
286
318
  const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
@@ -311,9 +343,11 @@ export class DeterministicProviderCliService {
311
343
  content: `Created bridge changeset at ${changesetPath}`,
312
344
  eventType: "changeset"
313
345
  });
314
- const phases = developCycle
315
- ? ["implementation", "self_review", "validation"]
316
- : [phase];
346
+ const phases = debugCycle
347
+ ? debugCompletionPhaseOrder
348
+ : developCycle
349
+ ? ["implementation", "self_review", "validation"]
350
+ : [phase];
317
351
  const completionArtifacts = [];
318
352
  for (const completionPhase of phases) {
319
353
  await writeCompletionArtifact({
@@ -322,7 +356,9 @@ export class DeterministicProviderCliService {
322
356
  phase: completionPhase,
323
357
  producedBy: "forge-bridge",
324
358
  redactionSecrets: exactRedactionSecrets(dispatch)
325
- }, expectedCompletionToolName(completionPhase), deterministicCompletionPayload(completionPhase, dispatch, changesetBody));
359
+ }, isDebugCompletionPhaseForProvider(completionPhase)
360
+ ? expectedDebugCompletionToolName(completionPhase)
361
+ : expectedCompletionToolName(completionPhase), deterministicCompletionPayload(completionPhase, dispatch, changesetBody));
326
362
  const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, completionPhase);
327
363
  if (!completionArtifact) {
328
364
  throw new Error(`Deterministic provider did not produce the Forge ${completionPhase} completion artifact.`);
@@ -338,7 +374,7 @@ export class DeterministicProviderCliService {
338
374
  }
339
375
  await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
340
376
  return {
341
- summary: `forge-bridge completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}).`,
377
+ summary: `forge-bridge completed ${worktree.branchName} (${debugCycle ? "debug cycle" : developCycle ? "develop cycle" : phase}).`,
342
378
  spec: {
343
379
  repoPath: worktree.worktreePath,
344
380
  path: specPath,
@@ -367,23 +403,38 @@ export class DeterministicProviderCliService {
367
403
  const browserChecks = browserSidecar && launchTarget
368
404
  ? [deterministicBrowserCheck(launchTarget, browserSidecar.relativePath)]
369
405
  : [];
406
+ const sidecars = await writeQaSidecarFiles(worktree.worktreePath, browserSidecar ? [browserSidecar] : []);
370
407
  const summary = missingLaunchTarget
371
408
  ? "QA blocked because browser testing is required but no app launch target was provided."
372
409
  : "All checks passed. forge-bridge QA completed.";
373
- const qaArtifact = await writeQaArtifacts({
374
- worktreePath: worktree.worktreePath,
375
- workItemId: dispatch.workItem.id,
376
- acceptanceCriteria: dispatch.workItem.acceptanceCriteria,
377
- producedBy: "forge-bridge",
378
- provider: dispatch.execution.provider,
379
- model: dispatch.execution.model,
380
- command: "forge-bridge deterministic QA",
381
- summary,
382
- status: missingLaunchTarget ? "blocked" : "pass",
383
- blockerReason: missingLaunchTarget ? "missing_app_launch_target" : null,
384
- browserChecks,
385
- sidecarFiles: browserSidecar ? [browserSidecar] : []
386
- });
410
+ const criterion = dispatch.workItem.acceptanceCriteria.trim() || "Forge QA evidence exists";
411
+ for (const gate of qaCompletionGateOrder) {
412
+ await writeCompletionArtifact({
413
+ worktreePath: worktree.worktreePath,
414
+ workItemId: dispatch.workItem.id,
415
+ phase: "qa",
416
+ producedBy: "forge-bridge",
417
+ redactionSecrets: exactRedactionSecrets(dispatch),
418
+ qaCycle: true
419
+ }, expectedQaCompletionToolName(gate), deterministicQaGatePayload({
420
+ gate,
421
+ criterion,
422
+ summary,
423
+ missingLaunchTarget,
424
+ browserChecks,
425
+ sidecars
426
+ }));
427
+ await context.emit({
428
+ role: "hook",
429
+ kind: "checkpoint",
430
+ content: `qa_gate: ${gate} artifact accepted.`,
431
+ eventType: "qa_gate"
432
+ });
433
+ }
434
+ const qaArtifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id);
435
+ if (!qaArtifact) {
436
+ throw new Error("Deterministic QA did not produce the complete durable QA artifact bundle.");
437
+ }
387
438
  await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
388
439
  await emitQaStructuredEvents(context.emit, worktree.worktreePath, dispatch.workItem.id, qaArtifact);
389
440
  await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
@@ -395,22 +446,30 @@ export class DeterministicProviderCliService {
395
446
  }
396
447
  async runConflictResolve(context) {
397
448
  const { dispatch, worktree } = context;
398
- await this.git.run(worktree.worktreePath, ["commit", "--allow-empty", "-m", `forge bridge conflict resolve for ${dispatch.workItem.id.slice(0, 8)}`]);
449
+ const addressComments = isPullRequestCommentsDispatch(dispatch);
450
+ await this.git.run(worktree.worktreePath, ["commit", "--allow-empty", "-m", `forge bridge ${addressComments ? "pr comments" : "conflict resolve"} for ${dispatch.workItem.id.slice(0, 8)}`]);
399
451
  await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
400
452
  await context.emit({
401
453
  role: "assistant",
402
454
  kind: "message",
403
- content: `Deterministic conflict resolve completed for ${worktree.branchName}.`,
404
- eventType: "conflict_resolve_complete"
455
+ content: addressComments
456
+ ? `Deterministic PR comment addressing completed for ${worktree.branchName}.`
457
+ : `Deterministic conflict resolve completed for ${worktree.branchName}.`,
458
+ eventType: addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
405
459
  });
406
460
  return {
407
- summary: `forge-bridge resolved conflicts for ${worktree.branchName}.`
461
+ summary: addressComments
462
+ ? `forge-bridge addressed PR comments for ${worktree.branchName}.`
463
+ : `forge-bridge resolved conflicts for ${worktree.branchName}.`
408
464
  };
409
465
  }
410
466
  }
411
467
  function phaseCompleteEventType(phase) {
412
468
  return phase === "implementation" ? "implementation_complete" : `${phase}_complete`;
413
469
  }
470
+ function isDebugCompletionPhaseForProvider(phase) {
471
+ return debugCompletionPhaseOrder.includes(phase);
472
+ }
414
473
  function deterministicCompletionPayload(phase, dispatch, changesetBody) {
415
474
  const changesetHash = sha256(changesetBody);
416
475
  const criterion = dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists";
@@ -433,6 +492,56 @@ function deterministicCompletionPayload(phase, dispatch, changesetBody) {
433
492
  next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation."
434
493
  };
435
494
  }
495
+ if (phase === "debug_repro") {
496
+ return {
497
+ status: "pass",
498
+ summary: "forge-bridge reproduced the bug deterministically.",
499
+ repro_steps: ["Run the deterministic fake-worker debug reproduction."],
500
+ observed: "The configured bug scenario is reproducible in deterministic bridge mode.",
501
+ expected: "The bug scenario should be fixed before QA handoff.",
502
+ evidence: [{ kind: "changeset", path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash }]
503
+ };
504
+ }
505
+ if (phase === "debug_root_cause") {
506
+ return {
507
+ status: "pass",
508
+ summary: "forge-bridge isolated a deterministic root cause.",
509
+ root_cause: "The deterministic fake-worker bug is represented by the generated changeset evidence.",
510
+ evidence: [{ kind: "changeset", path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash }],
511
+ eliminated_alternatives: [{ summary: "No alternate deterministic fake-worker path was implicated." }],
512
+ risk_area: "forge-bridge deterministic debug lane"
513
+ };
514
+ }
515
+ if (phase === "debug_fix") {
516
+ return {
517
+ status: "pass",
518
+ summary: "forge-bridge applied a deterministic debug fix.",
519
+ changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic debug fix evidence." }],
520
+ tests_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic debug evidence doubles as fake-worker regression coverage." }],
521
+ remaining_risks: [],
522
+ resolved_findings: (dispatch.qaRework?.findingIds ?? []).map((qaId) => ({
523
+ qa_id: qaId,
524
+ status: "resolved",
525
+ summary: `Resolved deterministic QA finding ${qaId}.`,
526
+ evidence: { path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash },
527
+ changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic debug fix evidence." }],
528
+ tests_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic fake-worker regression evidence." }],
529
+ regression_evidence: [{ command: "forge-bridge deterministic debug regression", status: "pass" }]
530
+ }))
531
+ };
532
+ }
533
+ if (phase === "debug_regression") {
534
+ return {
535
+ status: "pass",
536
+ summary: "forge-bridge proved the deterministic debug regression is QA-ready.",
537
+ commands: [{ command: "forge-bridge deterministic debug regression", exit_code: 0, status: "pass", summary: "Deterministic debug regression completed." }],
538
+ required_regressions: ["Original deterministic repro no longer fails."],
539
+ build_status: "not_applicable",
540
+ test_status: "pass",
541
+ repro_negative: true,
542
+ qa_ready: true
543
+ };
544
+ }
436
545
  if (phase === "self_review") {
437
546
  return {
438
547
  status: "pass",
@@ -493,6 +602,110 @@ function deterministicCompletionPayload(phase, dispatch, changesetBody) {
493
602
  ready_for_dev_test: true
494
603
  };
495
604
  }
605
+ function deterministicQaGatePayload(options) {
606
+ const blocked = options.missingLaunchTarget;
607
+ const skippedBecauseBlocked = blocked
608
+ ? {
609
+ status: "skipped",
610
+ summary: "Skipped because browser QA was blocked by missing launch target.",
611
+ reason: "Upstream browser QA blocker prevents meaningful downstream evidence."
612
+ }
613
+ : null;
614
+ switch (options.gate) {
615
+ case "qa_plan":
616
+ return {
617
+ status: "pass",
618
+ summary: "Deterministic QA plan recorded.",
619
+ checklist: [
620
+ "Review the validated work item against acceptance criteria.",
621
+ "Run deterministic regression evidence.",
622
+ "Confirm no in-scope QA findings remain."
623
+ ],
624
+ acceptance_criteria: [options.criterion]
625
+ };
626
+ case "qa_regression":
627
+ return {
628
+ status: "pass",
629
+ summary: "Deterministic regression QA completed.",
630
+ commands: [
631
+ {
632
+ command: "forge-bridge deterministic QA",
633
+ exit_code: 0,
634
+ status: "pass",
635
+ summary: "Deterministic regression evidence completed."
636
+ }
637
+ ],
638
+ notes: "Regression gate passed in deterministic fake-worker mode."
639
+ };
640
+ case "qa_api":
641
+ return {
642
+ status: "skipped",
643
+ summary: "No API/backend surface was identified for deterministic QA.",
644
+ reason: "The deterministic fake-worker changeset does not exercise API/backend behavior."
645
+ };
646
+ case "qa_manual_web":
647
+ return blocked
648
+ ? {
649
+ status: "blocked",
650
+ summary: "Browser QA could not run because no app launch target was provided.",
651
+ blocker_reason: "missing_app_launch_target"
652
+ }
653
+ : options.browserChecks.length > 0
654
+ ? {
655
+ status: "pass",
656
+ summary: "Deterministic browser QA exercised the configured launch target.",
657
+ browser_checks: options.browserChecks,
658
+ sidecars: options.sidecars,
659
+ notes: "Browser sidecar evidence recorded."
660
+ }
661
+ : {
662
+ status: "skipped",
663
+ summary: "Manual web QA was not required for this deterministic run.",
664
+ reason: "Forge did not require browser QA and no launch target was necessary."
665
+ };
666
+ case "qa_bug_repro":
667
+ return skippedBecauseBlocked ?? {
668
+ status: "skipped",
669
+ summary: "No failed QA observation required bug reproduction.",
670
+ reason: "No defects or candidate bugs were observed."
671
+ };
672
+ case "qa_findings":
673
+ return skippedBecauseBlocked ?? {
674
+ status: "pass",
675
+ summary: "No QA findings or candidate bugs were identified.",
676
+ findings: [],
677
+ candidate_bugs: []
678
+ };
679
+ case "qa_verdict":
680
+ return {
681
+ status: blocked ? "blocked" : "pass",
682
+ summary: options.summary,
683
+ checklist: [
684
+ "Review the validated work item against acceptance criteria.",
685
+ "Run deterministic regression evidence.",
686
+ blocked ? "Record missing browser launch target blocker." : "Confirm no in-scope QA findings remain."
687
+ ],
688
+ tested_acceptance_criteria: blocked ? [] : [options.criterion],
689
+ commands: blocked
690
+ ? []
691
+ : [
692
+ {
693
+ command: "forge-bridge deterministic QA",
694
+ exit_code: 0,
695
+ status: "pass",
696
+ summary: "Deterministic QA command completed."
697
+ }
698
+ ],
699
+ browser_checks: blocked ? [] : options.browserChecks,
700
+ sidecars: blocked ? [] : options.sidecars,
701
+ notes: options.summary,
702
+ findings: [],
703
+ candidate_bugs: [],
704
+ blocker_reason: blocked ? "missing_app_launch_target" : null,
705
+ report_markdown: `# QA report\n\n${options.summary}\n`
706
+ };
707
+ }
708
+ }
496
709
  const qaArtifactFileNames = {
497
710
  plan: "qa.plan.json",
498
711
  evidence: "qa.evidence.json",
@@ -551,78 +764,6 @@ async function writeQaSidecarFiles(worktreePath, files) {
551
764
  }
552
765
  return sidecars;
553
766
  }
554
- async function writeQaArtifacts(request) {
555
- const artifactRoot = artifactRootFor(request.worktreePath, request.workItemId);
556
- await fs.mkdir(artifactRoot, { recursive: true });
557
- const sidecars = await writeQaSidecarFiles(request.worktreePath, request.sidecarFiles ?? []);
558
- const status = request.status ?? "pass";
559
- const criterion = request.acceptanceCriteria.trim() || "Forge QA evidence exists";
560
- const relativeEvidencePath = qaArtifactRelativePath(request.workItemId, qaArtifactFileNames.evidence);
561
- const relativeFindingsPath = qaArtifactRelativePath(request.workItemId, qaArtifactFileNames.findings);
562
- const relativeReportPath = qaArtifactRelativePath(request.workItemId, qaArtifactFileNames.report);
563
- const planJson = jsonText({
564
- schema_version: 1,
565
- phase: "qa",
566
- checklist: [
567
- "Review the validated work item against acceptance criteria.",
568
- "Run relevant provider QA verification.",
569
- "Confirm no in-scope QA findings remain."
570
- ],
571
- acceptance_criteria: [criterion]
572
- });
573
- const evidenceJson = jsonText({
574
- schema_version: 1,
575
- phase: "qa",
576
- commands: status === "blocked"
577
- ? []
578
- : [
579
- {
580
- command: request.command,
581
- exit_code: 0,
582
- status: "pass",
583
- summary: `${request.provider} QA completed successfully.`
584
- }
585
- ],
586
- browser_checks: request.browserChecks ?? [],
587
- sidecars,
588
- notes: request.summary
589
- });
590
- const findingsJson = jsonText({
591
- schema_version: 1,
592
- phase: "qa",
593
- findings: [],
594
- candidate_bugs: []
595
- });
596
- const verdictJson = jsonText({
597
- schema_version: 1,
598
- phase: "qa",
599
- status,
600
- summary: request.summary,
601
- tested_acceptance_criteria: status === "blocked" ? [] : [criterion],
602
- finding_ids: [],
603
- candidate_bug_ids: [],
604
- blocker_reason: status === "blocked" ? request.blockerReason ?? "inconclusive" : null,
605
- evidence_path: relativeEvidencePath,
606
- findings_path: relativeFindingsPath,
607
- report_path: relativeReportPath
608
- });
609
- const report = `# QA report\n\n${request.summary}\n\n- Provider: ${singleLine(request.provider)}\n- Model: ${singleLine(request.model)}\n- Command: ${singleLine(request.command)}\n`;
610
- await fs.writeFile(path.join(artifactRoot, qaArtifactFileNames.plan), planJson, "utf8");
611
- await fs.writeFile(path.join(artifactRoot, qaArtifactFileNames.evidence), evidenceJson, "utf8");
612
- await fs.writeFile(path.join(artifactRoot, qaArtifactFileNames.findings), findingsJson, "utf8");
613
- const verdictPath = path.join(artifactRoot, qaArtifactFileNames.verdict);
614
- await fs.writeFile(verdictPath, verdictJson, "utf8");
615
- await fs.writeFile(path.join(artifactRoot, qaArtifactFileNames.report), report, "utf8");
616
- return {
617
- repoPath: request.worktreePath,
618
- path: verdictPath,
619
- verdictJson,
620
- report,
621
- producedBy: request.producedBy,
622
- status,
623
- phase: "qa"
624
- };
625
- }
626
767
  async function emitQaArtifactRecorded(emit, worktreePath, artifact) {
627
768
  const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
628
769
  await emit({
@@ -835,9 +976,6 @@ function summarizeQaObject(source) {
835
976
  function qaArtifactRelativePath(workItemId, fileName) {
836
977
  return [".forge", "work-items", workItemId, "artifacts", fileName].join("/");
837
978
  }
838
- function jsonText(value) {
839
- return `${JSON.stringify(value, null, 2)}\n`;
840
- }
841
979
  async function emitCompletionArtifactRecorded(emit, worktreePath, artifact) {
842
980
  const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
843
981
  await emit({
@@ -872,7 +1010,15 @@ async function emitPhaseGateEvents(emit, artifact) {
872
1010
  ? "phase_progression: implementation -> self_review"
873
1011
  : phase === "self_review"
874
1012
  ? "phase_progression: self_review -> validation"
875
- : "phase_progression: validation -> QA ready";
1013
+ : phase === "debug_repro"
1014
+ ? "phase_progression: debug_repro -> debug_root_cause"
1015
+ : phase === "debug_root_cause"
1016
+ ? "phase_progression: debug_root_cause -> debug_fix"
1017
+ : phase === "debug_fix"
1018
+ ? "phase_progression: debug_fix -> debug_regression"
1019
+ : phase === "debug_regression"
1020
+ ? "phase_progression: debug_regression -> QA ready"
1021
+ : "phase_progression: validation -> QA ready";
876
1022
  await emit({
877
1023
  role: "hook",
878
1024
  kind: "checkpoint",
@@ -883,6 +1029,7 @@ async function emitPhaseGateEvents(emit, artifact) {
883
1029
  function completionPhaseFromArtifact(artifact) {
884
1030
  const phase = artifact.phase?.trim();
885
1031
  return phase === "self_review" || phase === "validation" || phase === "implementation" || phase === "qa"
1032
+ || phase === "debug_repro" || phase === "debug_root_cause" || phase === "debug_fix" || phase === "debug_regression"
886
1033
  ? phase
887
1034
  : "implementation";
888
1035
  }
@@ -894,13 +1041,18 @@ class LiveCompletionArtifactEmitter {
894
1041
  emitted = new Set();
895
1042
  timer = null;
896
1043
  pending = Promise.resolve();
897
- constructor(worktreePath, workItemId, phase, developCycle, emit) {
1044
+ constructor(worktreePath, workItemId, phase, developCycle, debugCycle, emit) {
898
1045
  this.worktreePath = worktreePath;
899
1046
  this.workItemId = workItemId;
900
1047
  this.emit = emit;
901
- this.phases = developCycle
902
- ? developCyclePhaseOrder.slice(developCyclePhaseOrder.indexOf(phase))
903
- : [phase];
1048
+ if (debugCycle && isDebugCompletionPhaseForProvider(phase)) {
1049
+ this.phases = debugCompletionPhaseOrder.slice(debugCompletionPhaseOrder.indexOf(phase));
1050
+ }
1051
+ else {
1052
+ this.phases = developCycle
1053
+ ? developCyclePhaseOrder.slice(developCyclePhaseOrder.indexOf(phase))
1054
+ : [phase];
1055
+ }
904
1056
  }
905
1057
  start() {
906
1058
  void this.pollQueued();
@@ -943,20 +1095,23 @@ class LiveCompletionArtifactEmitter {
943
1095
  }
944
1096
  }
945
1097
  }
946
- async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, developCycle) {
947
- if (!developCycle) {
1098
+ async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, developCycle, debugCycle) {
1099
+ if (!developCycle && !debugCycle) {
948
1100
  const artifact = await readCompletionArtifact(worktreePath, workItemId, phase);
949
1101
  if (!artifact) {
950
1102
  throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
951
1103
  }
952
1104
  return [artifact];
953
1105
  }
1106
+ const expectedPhases = debugCycle && isDebugCompletionPhaseForProvider(phase)
1107
+ ? debugCompletionPhaseOrder.slice(debugCompletionPhaseOrder.indexOf(phase))
1108
+ : ["implementation", "self_review", "validation"];
954
1109
  const artifacts = [];
955
- for (const expectedPhase of ["implementation", "self_review", "validation"]) {
1110
+ for (const expectedPhase of expectedPhases) {
956
1111
  const artifact = await readCompletionArtifact(worktreePath, workItemId, expectedPhase);
957
1112
  if (!artifact) {
958
1113
  if (artifacts.length === 0) {
959
- throw new Error("Provider CLI completed but did not call the Forge implementation completion tool.");
1114
+ throw new Error(`Provider CLI completed but did not call the Forge ${expectedPhase} completion tool.`);
960
1115
  }
961
1116
  const previous = artifacts[artifacts.length - 1];
962
1117
  if ((previous.status ?? "").trim().toLowerCase() === "pass") {
@@ -1132,9 +1287,6 @@ function quoteWindowsCommandLineArg(value) {
1132
1287
  }
1133
1288
  return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
1134
1289
  }
1135
- async function fileExists(inputPath) {
1136
- return fs.stat(inputPath).then((stat) => stat.isFile()).catch(() => false);
1137
- }
1138
1290
  async function readProcessLines(stream, onLine) {
1139
1291
  const reader = readline.createInterface({ input: stream });
1140
1292
  try {
@@ -1155,9 +1307,6 @@ function sha256(content) {
1155
1307
  export function formatProviderCliCommand(command) {
1156
1308
  return [command.command, ...command.args].map(singleLine).join(" ");
1157
1309
  }
1158
- function yamlQuotedScalar(value) {
1159
- return JSON.stringify(singleLine(value));
1160
- }
1161
1310
  function singleLine(value) {
1162
1311
  return value.replace(/\s+/g, " ").trim();
1163
1312
  }
@@ -1224,6 +1373,20 @@ ${completionPromptForDevelopCycle(phase)}
1224
1373
 
1225
1374
  Finish with a concise summary of changed files and verification after the validation completion tool has accepted the work, or after the current phase tool records a non-pass blocker.`;
1226
1375
  }
1376
+ function buildDebugCyclePrompt(dispatch, phase = completionPhaseFromDispatch(dispatch)) {
1377
+ return `You are Forge's Debug lane running locally in a prepared git worktree for a bug work item.
1378
+
1379
+ Read .forge/work-items/${dispatch.workItem.id}/spec.md, reproduce or characterize the bug, isolate root cause, apply the smallest safe fix, then prove the regression in this same provider session.
1380
+ Start in the ${phase.replace(/_/g, " ")} gate. Do not stop after a passing Debug gate; continue through the next Debug MCP instruction returned by the tool server until debug_regression is accepted or the current gate records a non-pass blocker.
1381
+ ${debugQaReworkInstruction(dispatch)}
1382
+ Use the repository's existing conventions. Run focused tests when practical, especially the original repro and required regressions.
1383
+ Forge owns protocol files: worktree prep, injected .forge commands/hooks/skills, Debug verdict artifacts, reports, and commits.
1384
+ Read Forge-provided context under .forge when needed, but do not edit .forge files or create Forge Debug artifacts by hand.
1385
+
1386
+ ${completionPromptForDebugCycle(isDebugCompletionPhaseForProvider(phase) ? phase : "debug_repro")}
1387
+
1388
+ Finish with a concise summary of changed files and verification after the debug_regression completion tool has accepted the work, or after the current Debug gate tool records a non-pass blocker.`;
1389
+ }
1227
1390
  function qaReworkImplementationInstruction(dispatch) {
1228
1391
  if (!dispatch.qaRework) {
1229
1392
  return "";
@@ -1235,6 +1398,17 @@ QA rework context:
1235
1398
  - Required QA finding IDs: ${dispatch.qaRework.findingIds.join(", ")}
1236
1399
  Read the packet JSON and fix or explicitly dispute each listed finding. The implementation completion artifact must include resolved_findings entries for every required QA ID with qa_id, status, summary, evidence, changed_files, tests_added_or_updated, and regression_evidence.`;
1237
1400
  }
1401
+ function debugQaReworkInstruction(dispatch) {
1402
+ if (!dispatch.qaRework) {
1403
+ return "";
1404
+ }
1405
+ return `
1406
+ QA rework context for Debug:
1407
+ - Attempt ${dispatch.qaRework.attempt}/${dispatch.qaRework.maxAttempts}
1408
+ - Rework packet: ${dispatch.qaRework.packetPath}
1409
+ - Required QA finding IDs: ${dispatch.qaRework.findingIds.join(", ")}
1410
+ Read the packet JSON, reproduce or characterize each listed finding, fix or explicitly dispute it, and include resolved_findings entries for every required QA ID in forge.complete_debug_fix with qa_id, status, summary, evidence, changed_files, tests_added_or_updated, and regression_evidence.`;
1411
+ }
1238
1412
  function qaReworkRetestInstruction(dispatch) {
1239
1413
  if (!dispatch.qaRework) {
1240
1414
  return "";
@@ -1270,9 +1444,16 @@ function qaBrowserInstruction(dispatch) {
1270
1444
  const routes = launchTarget.routes.length > 0 ? launchTarget.routes.join(", ") : "not specified";
1271
1445
  return `Browser/app launch context:\n- Browser QA required: ${qaBrowser.required ? "yes" : "no, 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"}\n- Browser sidecars path: ${qaBrowser.artifactsPath || `.forge/work-items/${dispatch.workItem.id}/artifacts/qa`}`;
1272
1446
  }
1273
- function buildQaPrompt(dispatch) {
1447
+ function buildQaPrompt(dispatch, laneSpec) {
1274
1448
  return `You are Forge's QA lane running locally in a prepared git worktree.
1275
1449
 
1450
+ 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.
1451
+
1452
+ Injected QA lane command and work item spec:
1453
+ ${laneSpec}
1454
+
1455
+ ---
1456
+
1276
1457
  Work item: ${dispatch.workItem.id}
1277
1458
  Title: ${dispatch.workItem.title}
1278
1459
  Description:
@@ -1295,9 +1476,9 @@ Record concrete evidence. For UI/browser work, include browser_checks with URLs/
1295
1476
  Report in-scope defects as actionable findings with repro steps, expected/actual behavior, evidence, likely area, required fix, and required regression.
1296
1477
  Out-of-scope or follow-up issues belong in candidate_bugs instead of blocking the current item.
1297
1478
 
1298
- ${completionPromptForPhase("qa")}
1479
+ ${completionPromptForQaCycle()}
1299
1480
 
1300
- Finish with a concise summary only after forge.complete_qa accepts the QA bundle.`;
1481
+ Finish with a concise summary only after forge.complete_qa_verdict accepts the QA bundle.`;
1301
1482
  }
1302
1483
  function buildConflictResolvePrompt(dispatch, branchName) {
1303
1484
  const targetBranch = dispatch.cycleRun.requestedBase || dispatch.repo.defaultBranch || "main";
@@ -1312,6 +1493,33 @@ Keep the original work item intent intact:
1312
1493
 
1313
1494
  Finish with a concise summary of the conflicts resolved and verification performed.`;
1314
1495
  }
1496
+ function buildPullRequestCommentsPrompt(dispatch, branchName) {
1497
+ const pr = dispatch.pullRequest;
1498
+ const targetBranch = pr?.targetBranch || dispatch.cycleRun.requestedBase || dispatch.repo.defaultBranch || "main";
1499
+ const prLocator = pr
1500
+ ? `PR #${pr.number}${pr.url ? ` (${pr.url})` : ""}`
1501
+ : "the open pull request for this work item";
1502
+ return `You are Forge's Pull Request Comments lane running locally in a prepared git worktree.
1503
+
1504
+ The current branch is the pull request branch ${branchName}. Address unresolved review comments on ${prLocator}.
1505
+ PR context:
1506
+ - Provider: ${pr?.provider || "not provided"}
1507
+ - Source branch: ${pr?.sourceBranch || branchName}
1508
+ - Target branch: ${targetBranch}
1509
+ - Unresolved comments: ${pr?.unresolvedThreads ?? "unknown"}
1510
+
1511
+ Use the PR URL, local git metadata, and available GitHub tooling/auth to inspect unresolved review threads. Make focused code or test changes that answer the feedback, commit the changes, and push ${branchName} to origin.
1512
+ If the review thread contents are unavailable, stop with a clear summary of what could not be accessed instead of guessing.
1513
+ Keep the original work item intent intact:
1514
+ - Work item: ${dispatch.workItem.id}
1515
+ - Title: ${dispatch.workItem.title}
1516
+ - Acceptance criteria: ${dispatch.workItem.acceptanceCriteria || "Not specified"}
1517
+
1518
+ Finish with a concise summary of comments addressed and verification performed.`;
1519
+ }
1520
+ function isPullRequestCommentsDispatch(dispatch) {
1521
+ return dispatch.cycleRun.kind.trim().toLowerCase() === "pr_comments";
1522
+ }
1315
1523
  async function cleanupCompletionTools(completionTools, emit) {
1316
1524
  try {
1317
1525
  await completionTools.cleanup();