@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.
- package/dist/bridge/bridge.service.js +39 -8
- package/dist/session/internals.d.ts +1 -1
- package/dist/session/phase-completion.d.ts +27 -2
- package/dist/session/phase-completion.js +831 -25
- package/dist/session/provider-cli/provider-cli.service.js +339 -131
- package/dist/session/session.service.js +6 -1
- package/dist/session/session.types.d.ts +23 -0
- package/dist/session/session.types.js +29 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +1 -0
- package/dist/worktree/artifact-injector.js +30 -1
- package/dist/worktree/worktree.service.js +5 -3
- package/dist/worktree/worktree.types.d.ts +2 -0
- package/package.json +8 -3
|
@@ -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:
|
|
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:
|
|
76
|
-
? "Forge completion tools configured for
|
|
77
|
-
:
|
|
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
|
|
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:
|
|
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 ||
|
|
268
|
-
|
|
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:
|
|
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 =
|
|
315
|
-
?
|
|
316
|
-
:
|
|
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
|
-
},
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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:
|
|
404
|
-
|
|
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:
|
|
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
|
-
:
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
|
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(
|
|
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
|
-
${
|
|
1479
|
+
${completionPromptForQaCycle()}
|
|
1299
1480
|
|
|
1300
|
-
Finish with a concise summary only after forge.
|
|
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();
|