@loucompanion/forge-bridge 0.1.1-dev.e52b249f07a8 → 0.1.1-dev.eee62899e922
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.d.ts +1 -0
- package/dist/bridge/bridge.service.js +181 -24
- package/dist/bridge/bridge.types.d.ts +4 -2
- package/dist/cli/cli.service.js +225 -10
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -2
- package/dist/config.js +1 -1
- package/dist/session/internals.d.ts +1 -1
- package/dist/session/phase-completion.d.ts +30 -5
- package/dist/session/phase-completion.js +1178 -27
- package/dist/session/provider-cli/provider-cli.base.d.ts +3 -1
- package/dist/session/provider-cli/provider-cli.service.d.ts +8 -1
- package/dist/session/provider-cli/provider-cli.service.js +1195 -236
- package/dist/session/provider-cli/provider-cli.types.d.ts +5 -0
- package/dist/session/session.service.js +47 -5
- package/dist/session/session.types.d.ts +90 -0
- package/dist/session/session.types.js +166 -1
- 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 +78 -1
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +132 -6
- package/dist/worktree/worktree.types.d.ts +8 -1
- package/package.json +8 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { execFile, spawn } from "node:child_process";
|
|
5
6
|
import readline from "node:readline";
|
|
@@ -8,11 +9,52 @@ import { GitClient } from "../../shared/git/git.js";
|
|
|
8
9
|
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
9
10
|
import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from "../qa-sidecar-validation.js";
|
|
10
11
|
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";
|
|
12
|
+
import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDebugCycle, completionPromptForDevelopCycle, completionPromptForPhase, completionPromptForQaCycle, completionRedactionEnv, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact, artifactRootFor, artifactRootForOwner, debugCompletionPhaseOrder, qaCompletionGateOrder } from "../phase-completion.js";
|
|
12
13
|
import { addUsage, buildProviderUsageEvent, emptyUsage, hasUsage, readUsageNumber } from "./provider-usage.js";
|
|
13
14
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
14
15
|
const execFileAsync = promisify(execFile);
|
|
15
16
|
const developCyclePhaseOrder = ["implementation", "self_review", "validation"];
|
|
17
|
+
const allowedProviderParentEnvNames = [
|
|
18
|
+
"PATH",
|
|
19
|
+
"HOME",
|
|
20
|
+
"USERPROFILE",
|
|
21
|
+
"TMPDIR",
|
|
22
|
+
"TEMP",
|
|
23
|
+
"TMP",
|
|
24
|
+
"LANG"
|
|
25
|
+
];
|
|
26
|
+
const windowsAllowedProviderParentEnvNames = [
|
|
27
|
+
"SystemRoot",
|
|
28
|
+
"ComSpec",
|
|
29
|
+
"PATHEXT",
|
|
30
|
+
"APPDATA",
|
|
31
|
+
"LOCALAPPDATA"
|
|
32
|
+
];
|
|
33
|
+
function buildProviderChildEnvironment(requestEnv) {
|
|
34
|
+
const env = {};
|
|
35
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
36
|
+
if (typeof value === "string" && isAllowedProviderParentEnv(key)) {
|
|
37
|
+
env[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (requestEnv) {
|
|
41
|
+
for (const [key, value] of Object.entries(requestEnv)) {
|
|
42
|
+
const trimmedKey = key.trim();
|
|
43
|
+
if (trimmedKey.length > 0) {
|
|
44
|
+
env[trimmedKey] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return env;
|
|
49
|
+
}
|
|
50
|
+
function isAllowedProviderParentEnv(key) {
|
|
51
|
+
return isAllowedProviderParentEnvName(key, allowedProviderParentEnvNames)
|
|
52
|
+
|| key.toUpperCase().startsWith("LC_")
|
|
53
|
+
|| (process.platform === "win32" && isAllowedProviderParentEnvName(key, windowsAllowedProviderParentEnvNames));
|
|
54
|
+
}
|
|
55
|
+
function isAllowedProviderParentEnvName(key, allowedNames) {
|
|
56
|
+
return allowedNames.some((allowed) => allowed.toLowerCase() === key.toLowerCase());
|
|
57
|
+
}
|
|
16
58
|
export class RoutingProviderCliService {
|
|
17
59
|
realProvider;
|
|
18
60
|
deterministicProvider;
|
|
@@ -25,6 +67,11 @@ export class RoutingProviderCliService {
|
|
|
25
67
|
? await this.deterministicProvider.runImplementation(context)
|
|
26
68
|
: await this.realProvider.runImplementation(context);
|
|
27
69
|
}
|
|
70
|
+
async runArchitect(context) {
|
|
71
|
+
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
72
|
+
? await this.deterministicProvider.runArchitect(context)
|
|
73
|
+
: await this.realProvider.runArchitect(context);
|
|
74
|
+
}
|
|
28
75
|
async runConflictResolve(context) {
|
|
29
76
|
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
30
77
|
? await this.deterministicProvider.runConflictResolve(context)
|
|
@@ -35,6 +82,11 @@ export class RoutingProviderCliService {
|
|
|
35
82
|
? await this.deterministicProvider.runQa(context)
|
|
36
83
|
: await this.realProvider.runQa(context);
|
|
37
84
|
}
|
|
85
|
+
async runDraft(context) {
|
|
86
|
+
return isFakeWorkerModel(context.dispatch.execution.model)
|
|
87
|
+
? await this.deterministicProvider.runDraft(context)
|
|
88
|
+
: await this.realProvider.runDraft(context);
|
|
89
|
+
}
|
|
38
90
|
}
|
|
39
91
|
export class ProcessBackedProviderCliService {
|
|
40
92
|
processRunner;
|
|
@@ -43,10 +95,65 @@ export class ProcessBackedProviderCliService {
|
|
|
43
95
|
this.processRunner = processRunner;
|
|
44
96
|
this.git = git;
|
|
45
97
|
}
|
|
98
|
+
async runDraft(context) {
|
|
99
|
+
const { dispatch } = context;
|
|
100
|
+
if (!dispatch.draft) {
|
|
101
|
+
throw new Error("Draft dispatch context is missing.");
|
|
102
|
+
}
|
|
103
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "forge-bridge-draft-"));
|
|
104
|
+
const command = buildProviderCliCommand(dispatch, {
|
|
105
|
+
worktreePath: tempDir,
|
|
106
|
+
prompt: buildDraftPrompt(dispatch),
|
|
107
|
+
skipGitRepoCheck: true
|
|
108
|
+
});
|
|
109
|
+
await context.emit({
|
|
110
|
+
role: "runner",
|
|
111
|
+
kind: "checkpoint",
|
|
112
|
+
content: `Starting provider CLI Draft: ${formatProviderCliCommand(command)}`,
|
|
113
|
+
eventType: "provider_cli_started"
|
|
114
|
+
});
|
|
115
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
116
|
+
try {
|
|
117
|
+
const result = await this.processRunner.run({
|
|
118
|
+
command: command.command,
|
|
119
|
+
args: command.args,
|
|
120
|
+
cwd: tempDir,
|
|
121
|
+
env: providerProcessEnv(dispatch)
|
|
122
|
+
}, {
|
|
123
|
+
onStdoutLine: async (line) => {
|
|
124
|
+
for (const event of outputState.consume(line)) {
|
|
125
|
+
await context.emit(event);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, context.signal);
|
|
129
|
+
if (result.exitCode !== 0) {
|
|
130
|
+
throw new Error(`Provider CLI exited with code ${result.exitCode}${summarizeStderr(result.stderr)}.`);
|
|
131
|
+
}
|
|
132
|
+
const content = outputState.finalMessage ?? result.stdout.trim();
|
|
133
|
+
if (!content) {
|
|
134
|
+
throw new Error("Provider CLI completed without returning a draft.");
|
|
135
|
+
}
|
|
136
|
+
const json = extractJsonObject(content);
|
|
137
|
+
await context.emit({
|
|
138
|
+
role: "assistant",
|
|
139
|
+
kind: "message",
|
|
140
|
+
content: json,
|
|
141
|
+
eventType: "draft_complete"
|
|
142
|
+
});
|
|
143
|
+
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
144
|
+
return {
|
|
145
|
+
summary: "Draft generated through Forge Runner."
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
46
152
|
async runImplementation(context) {
|
|
47
153
|
const { dispatch, worktree } = context;
|
|
48
154
|
const phase = completionPhaseFromDispatch(dispatch);
|
|
49
155
|
const developCycle = usesDevelopCycleCompletion(dispatch);
|
|
156
|
+
const debugCycle = usesDebugCycleCompletion(dispatch);
|
|
50
157
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
51
158
|
await fs.mkdir(metadataRoot, { recursive: true });
|
|
52
159
|
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
@@ -60,7 +167,11 @@ export class ProcessBackedProviderCliService {
|
|
|
60
167
|
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
|
|
61
168
|
const command = buildProviderCliCommand(dispatch, {
|
|
62
169
|
worktreePath: worktree.worktreePath,
|
|
63
|
-
prompt:
|
|
170
|
+
prompt: debugCycle
|
|
171
|
+
? buildDebugCyclePrompt(dispatch, phase)
|
|
172
|
+
: developCycle
|
|
173
|
+
? buildDevelopCyclePrompt(dispatch, phase)
|
|
174
|
+
: buildImplementationPrompt(dispatch, phase),
|
|
64
175
|
completionTools
|
|
65
176
|
});
|
|
66
177
|
await context.emit({
|
|
@@ -72,13 +183,15 @@ export class ProcessBackedProviderCliService {
|
|
|
72
183
|
await context.emit({
|
|
73
184
|
role: "runner",
|
|
74
185
|
kind: "checkpoint",
|
|
75
|
-
content:
|
|
76
|
-
? "Forge completion tools configured for
|
|
77
|
-
:
|
|
186
|
+
content: debugCycle
|
|
187
|
+
? "Forge Debug completion tools configured for debug_repro -> debug_root_cause -> debug_fix -> debug_regression; provider must call each MCP completion tool before exit."
|
|
188
|
+
: developCycle
|
|
189
|
+
? "Forge completion tools configured for implementation -> self_review -> validation; provider must call each MCP completion tool before exit."
|
|
190
|
+
: `Forge completion tools configured for ${phase}; provider must call the MCP completion tool before exit.`,
|
|
78
191
|
eventType: "completion_tools_configured"
|
|
79
192
|
});
|
|
80
193
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
81
|
-
const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, context.emit);
|
|
194
|
+
const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle, context.emit);
|
|
82
195
|
let result;
|
|
83
196
|
try {
|
|
84
197
|
liveCompletionEvents.start();
|
|
@@ -86,11 +199,10 @@ export class ProcessBackedProviderCliService {
|
|
|
86
199
|
command: command.command,
|
|
87
200
|
args: command.args,
|
|
88
201
|
cwd: worktree.worktreePath,
|
|
89
|
-
env:
|
|
202
|
+
env: providerProcessEnv(dispatch)
|
|
90
203
|
}, {
|
|
91
204
|
onStdoutLine: async (line) => {
|
|
92
|
-
const event
|
|
93
|
-
if (event) {
|
|
205
|
+
for (const event of outputState.consume(line)) {
|
|
94
206
|
await context.emit(event);
|
|
95
207
|
}
|
|
96
208
|
}
|
|
@@ -112,7 +224,7 @@ export class ProcessBackedProviderCliService {
|
|
|
112
224
|
});
|
|
113
225
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
114
226
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
115
|
-
const completionArtifacts = await readCompletionArtifactsForRun(worktree.worktreePath, dispatch.workItem.id, phase, developCycle);
|
|
227
|
+
const completionArtifacts = await readCompletionArtifactsForRun(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, debugCycle);
|
|
116
228
|
for (const artifact of completionArtifacts) {
|
|
117
229
|
if (!liveCompletionEvents.hasEmitted(completionPhaseFromArtifact(artifact))) {
|
|
118
230
|
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, artifact);
|
|
@@ -138,7 +250,7 @@ export class ProcessBackedProviderCliService {
|
|
|
138
250
|
});
|
|
139
251
|
}
|
|
140
252
|
return {
|
|
141
|
-
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}, ${committed.commitSha.slice(0, 12)}).`,
|
|
253
|
+
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${debugCycle ? "debug cycle" : developCycle ? "develop cycle" : phase}, ${committed.commitSha.slice(0, 12)}).`,
|
|
142
254
|
spec: {
|
|
143
255
|
repoPath: worktree.worktreePath,
|
|
144
256
|
path: specPath,
|
|
@@ -150,12 +262,105 @@ export class ProcessBackedProviderCliService {
|
|
|
150
262
|
artifacts: completionArtifacts
|
|
151
263
|
};
|
|
152
264
|
}
|
|
265
|
+
async runArchitect(context) {
|
|
266
|
+
const { dispatch, worktree } = context;
|
|
267
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "architect" });
|
|
268
|
+
const command = buildProviderCliCommand(dispatch, {
|
|
269
|
+
worktreePath: worktree.worktreePath,
|
|
270
|
+
prompt: buildArchitectPrompt(dispatch),
|
|
271
|
+
completionTools
|
|
272
|
+
});
|
|
273
|
+
const commandText = formatProviderCliCommand(command);
|
|
274
|
+
await context.emit({
|
|
275
|
+
role: "runner",
|
|
276
|
+
kind: "checkpoint",
|
|
277
|
+
content: `Starting provider CLI Architect: ${commandText} in ${worktree.worktreePath}`,
|
|
278
|
+
eventType: "provider_cli_started"
|
|
279
|
+
});
|
|
280
|
+
await context.emit({
|
|
281
|
+
role: "runner",
|
|
282
|
+
kind: "checkpoint",
|
|
283
|
+
content: "Forge Architect completion tools configured; provider must call forge.complete_architect_plan before exit.",
|
|
284
|
+
eventType: "completion_tools_configured"
|
|
285
|
+
});
|
|
286
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
287
|
+
const liveArchitectEvents = new LiveArchitectArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, context.emit);
|
|
288
|
+
let result;
|
|
289
|
+
try {
|
|
290
|
+
liveArchitectEvents.start();
|
|
291
|
+
result = await this.processRunner.run({
|
|
292
|
+
command: command.command,
|
|
293
|
+
args: command.args,
|
|
294
|
+
cwd: worktree.worktreePath,
|
|
295
|
+
env: providerProcessEnv(dispatch)
|
|
296
|
+
}, {
|
|
297
|
+
onStdoutLine: async (line) => {
|
|
298
|
+
for (const event of outputState.consume(line)) {
|
|
299
|
+
await context.emit(event);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}, context.signal);
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
await liveArchitectEvents.stop();
|
|
306
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
307
|
+
}
|
|
308
|
+
if (result.exitCode !== 0) {
|
|
309
|
+
const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
|
|
310
|
+
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
311
|
+
}
|
|
312
|
+
await context.emit({
|
|
313
|
+
role: "runner",
|
|
314
|
+
kind: "checkpoint",
|
|
315
|
+
content: `Provider CLI Architect completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
316
|
+
eventType: "provider_cli_completed"
|
|
317
|
+
});
|
|
318
|
+
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
319
|
+
const architectArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "architect");
|
|
320
|
+
if (!architectArtifact) {
|
|
321
|
+
throw new Error("Provider CLI Architect completed but did not call the Forge Architect completion tool.");
|
|
322
|
+
}
|
|
323
|
+
if (!liveArchitectEvents.hasEmitted()) {
|
|
324
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, architectArtifact);
|
|
325
|
+
await emitArchitectStructuredEvents(context.emit, worktree.worktreePath, dispatch.workItem.id, architectArtifact);
|
|
326
|
+
}
|
|
327
|
+
await context.emit({
|
|
328
|
+
role: "assistant",
|
|
329
|
+
kind: "message",
|
|
330
|
+
content: outputState.finalMessage || `${dispatch.execution.provider} CLI produced an Architect plan for ${worktree.branchName}.`,
|
|
331
|
+
eventType: "architect_complete"
|
|
332
|
+
});
|
|
333
|
+
const specPath = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id, "spec.md");
|
|
334
|
+
const specBody = await fs.readFile(specPath, "utf8").catch(() => `# ${dispatch.workItem.title}\n\nArchitect plan pending approval.\n`);
|
|
335
|
+
return {
|
|
336
|
+
summary: `${dispatch.execution.provider} CLI Architect plan produced.`,
|
|
337
|
+
spec: {
|
|
338
|
+
repoPath: worktree.worktreePath,
|
|
339
|
+
path: specPath,
|
|
340
|
+
body: specBody,
|
|
341
|
+
producedBy: `${dispatch.execution.provider}-cli`,
|
|
342
|
+
status: "pending_approval"
|
|
343
|
+
},
|
|
344
|
+
artifact: architectArtifact,
|
|
345
|
+
artifacts: [architectArtifact]
|
|
346
|
+
};
|
|
347
|
+
}
|
|
153
348
|
async runQa(context) {
|
|
154
349
|
const { dispatch, worktree } = context;
|
|
350
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
351
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
352
|
+
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
353
|
+
if (!laneCommand) {
|
|
354
|
+
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
355
|
+
}
|
|
356
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, "qa");
|
|
357
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
358
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
359
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
155
360
|
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase: "qa" });
|
|
156
361
|
const command = buildProviderCliCommand(dispatch, {
|
|
157
362
|
worktreePath: worktree.worktreePath,
|
|
158
|
-
prompt: buildQaPrompt(dispatch),
|
|
363
|
+
prompt: buildQaPrompt(dispatch, specBody),
|
|
159
364
|
completionTools
|
|
160
365
|
});
|
|
161
366
|
const commandText = formatProviderCliCommand(command);
|
|
@@ -168,7 +373,7 @@ export class ProcessBackedProviderCliService {
|
|
|
168
373
|
await context.emit({
|
|
169
374
|
role: "runner",
|
|
170
375
|
kind: "checkpoint",
|
|
171
|
-
content: "Forge QA completion
|
|
376
|
+
content: "Forge QA completion tools configured; provider must call all seven QA MCP gates before exit.",
|
|
172
377
|
eventType: "completion_tools_configured"
|
|
173
378
|
});
|
|
174
379
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
@@ -180,11 +385,10 @@ export class ProcessBackedProviderCliService {
|
|
|
180
385
|
command: command.command,
|
|
181
386
|
args: command.args,
|
|
182
387
|
cwd: worktree.worktreePath,
|
|
183
|
-
env:
|
|
388
|
+
env: providerProcessEnv(dispatch)
|
|
184
389
|
}, {
|
|
185
390
|
onStdoutLine: async (line) => {
|
|
186
|
-
const event
|
|
187
|
-
if (event) {
|
|
391
|
+
for (const event of outputState.consume(line)) {
|
|
188
392
|
await context.emit(event);
|
|
189
393
|
}
|
|
190
394
|
}
|
|
@@ -205,7 +409,7 @@ export class ProcessBackedProviderCliService {
|
|
|
205
409
|
eventType: "provider_cli_completed"
|
|
206
410
|
});
|
|
207
411
|
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
208
|
-
const qaArtifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id);
|
|
412
|
+
const qaArtifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId);
|
|
209
413
|
if (!qaArtifact) {
|
|
210
414
|
const verdictOnly = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "qa");
|
|
211
415
|
throw new Error(verdictOnly
|
|
@@ -215,7 +419,7 @@ export class ProcessBackedProviderCliService {
|
|
|
215
419
|
enforceQaBrowserLaunchTargetGate(dispatch, qaArtifact);
|
|
216
420
|
if (!liveQaEvents.hasEmitted()) {
|
|
217
421
|
await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
|
|
218
|
-
await emitQaStructuredEvents(context.emit,
|
|
422
|
+
await emitQaStructuredEvents(context.emit, qaArtifact);
|
|
219
423
|
}
|
|
220
424
|
const status = (qaArtifact.status ?? "unknown").trim().toLowerCase();
|
|
221
425
|
const statusSummary = status === "pass"
|
|
@@ -225,31 +429,41 @@ export class ProcessBackedProviderCliService {
|
|
|
225
429
|
: "found rework";
|
|
226
430
|
return {
|
|
227
431
|
summary: `${dispatch.execution.provider} CLI QA ${statusSummary}.`,
|
|
432
|
+
spec: {
|
|
433
|
+
repoPath: worktree.worktreePath,
|
|
434
|
+
path: specPath,
|
|
435
|
+
body: specBody,
|
|
436
|
+
producedBy: "forge-lane-command",
|
|
437
|
+
status: "approved"
|
|
438
|
+
},
|
|
228
439
|
artifact: qaArtifact,
|
|
229
440
|
artifacts: [qaArtifact]
|
|
230
441
|
};
|
|
231
442
|
}
|
|
232
443
|
async runConflictResolve(context) {
|
|
233
444
|
const { dispatch, worktree } = context;
|
|
445
|
+
const addressComments = isPullRequestCommentsDispatch(dispatch);
|
|
234
446
|
const command = buildProviderCliCommand(dispatch, {
|
|
235
447
|
worktreePath: worktree.worktreePath,
|
|
236
|
-
prompt:
|
|
448
|
+
prompt: addressComments
|
|
449
|
+
? buildPullRequestCommentsPrompt(dispatch, worktree.branchName)
|
|
450
|
+
: buildConflictResolvePrompt(dispatch, worktree.branchName)
|
|
237
451
|
});
|
|
238
452
|
await context.emit({
|
|
239
453
|
role: "runner",
|
|
240
454
|
kind: "checkpoint",
|
|
241
|
-
content: `Starting provider CLI conflict resolve: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
|
|
455
|
+
content: `Starting provider CLI ${addressComments ? "PR comment address" : "conflict resolve"}: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
|
|
242
456
|
eventType: "provider_cli_started"
|
|
243
457
|
});
|
|
244
458
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
245
459
|
const result = await this.processRunner.run({
|
|
246
460
|
command: command.command,
|
|
247
461
|
args: command.args,
|
|
248
|
-
cwd: worktree.worktreePath
|
|
462
|
+
cwd: worktree.worktreePath,
|
|
463
|
+
env: providerProcessEnv(dispatch)
|
|
249
464
|
}, {
|
|
250
465
|
onStdoutLine: async (line) => {
|
|
251
|
-
const event
|
|
252
|
-
if (event) {
|
|
466
|
+
for (const event of outputState.consume(line)) {
|
|
253
467
|
await context.emit(event);
|
|
254
468
|
}
|
|
255
469
|
}
|
|
@@ -264,11 +478,15 @@ export class ProcessBackedProviderCliService {
|
|
|
264
478
|
await context.emit({
|
|
265
479
|
role: "assistant",
|
|
266
480
|
kind: "message",
|
|
267
|
-
content: outputState.finalMessage ||
|
|
268
|
-
|
|
481
|
+
content: outputState.finalMessage || (addressComments
|
|
482
|
+
? `${dispatch.execution.provider} CLI addressed pull request comments for ${worktree.branchName}.`
|
|
483
|
+
: `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`),
|
|
484
|
+
eventType: addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
|
|
269
485
|
});
|
|
270
486
|
return {
|
|
271
|
-
summary:
|
|
487
|
+
summary: addressComments
|
|
488
|
+
? `${dispatch.execution.provider} CLI addressed PR comments on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
|
|
489
|
+
: `${dispatch.execution.provider} CLI resolved conflicts on ${worktree.branchName} (${committed.commitSha.slice(0, 12)}).`
|
|
272
490
|
};
|
|
273
491
|
}
|
|
274
492
|
}
|
|
@@ -277,10 +495,22 @@ export class DeterministicProviderCliService {
|
|
|
277
495
|
constructor(git = new GitClient()) {
|
|
278
496
|
this.git = git;
|
|
279
497
|
}
|
|
498
|
+
async runDraft(context) {
|
|
499
|
+
const content = JSON.stringify(deterministicDraftPayload(context.dispatch));
|
|
500
|
+
await context.emit({
|
|
501
|
+
role: "assistant",
|
|
502
|
+
kind: "message",
|
|
503
|
+
content,
|
|
504
|
+
eventType: "draft_complete"
|
|
505
|
+
});
|
|
506
|
+
await context.emit(buildProviderUsageEvent(context.dispatch, estimatedDeterministicUsage()));
|
|
507
|
+
return { summary: "forge-bridge generated deterministic draft." };
|
|
508
|
+
}
|
|
280
509
|
async runImplementation(context) {
|
|
281
510
|
const { dispatch, worktree } = context;
|
|
282
511
|
const phase = completionPhaseFromDispatch(dispatch);
|
|
283
512
|
const developCycle = usesDevelopCycleCompletion(dispatch);
|
|
513
|
+
const debugCycle = usesDebugCycleCompletion(dispatch);
|
|
284
514
|
const providerCommand = buildProviderCliCommand(dispatch);
|
|
285
515
|
const providerCommandText = formatProviderCliCommand(providerCommand);
|
|
286
516
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
@@ -311,9 +541,11 @@ export class DeterministicProviderCliService {
|
|
|
311
541
|
content: `Created bridge changeset at ${changesetPath}`,
|
|
312
542
|
eventType: "changeset"
|
|
313
543
|
});
|
|
314
|
-
const phases =
|
|
315
|
-
?
|
|
316
|
-
:
|
|
544
|
+
const phases = debugCycle
|
|
545
|
+
? debugCompletionPhaseOrder
|
|
546
|
+
: developCycle
|
|
547
|
+
? ["implementation", "self_review", "validation"]
|
|
548
|
+
: [phase];
|
|
317
549
|
const completionArtifacts = [];
|
|
318
550
|
for (const completionPhase of phases) {
|
|
319
551
|
await writeCompletionArtifact({
|
|
@@ -322,7 +554,9 @@ export class DeterministicProviderCliService {
|
|
|
322
554
|
phase: completionPhase,
|
|
323
555
|
producedBy: "forge-bridge",
|
|
324
556
|
redactionSecrets: exactRedactionSecrets(dispatch)
|
|
325
|
-
},
|
|
557
|
+
}, isDebugCompletionPhaseForProvider(completionPhase)
|
|
558
|
+
? expectedDebugCompletionToolName(completionPhase)
|
|
559
|
+
: expectedCompletionToolName(completionPhase), deterministicCompletionPayload(completionPhase, dispatch, changesetBody));
|
|
326
560
|
const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, completionPhase);
|
|
327
561
|
if (!completionArtifact) {
|
|
328
562
|
throw new Error(`Deterministic provider did not produce the Forge ${completionPhase} completion artifact.`);
|
|
@@ -338,7 +572,7 @@ export class DeterministicProviderCliService {
|
|
|
338
572
|
}
|
|
339
573
|
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
340
574
|
return {
|
|
341
|
-
summary: `forge-bridge completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}).`,
|
|
575
|
+
summary: `forge-bridge completed ${worktree.branchName} (${debugCycle ? "debug cycle" : developCycle ? "develop cycle" : phase}).`,
|
|
342
576
|
spec: {
|
|
343
577
|
repoPath: worktree.worktreePath,
|
|
344
578
|
path: specPath,
|
|
@@ -350,6 +584,47 @@ export class DeterministicProviderCliService {
|
|
|
350
584
|
artifacts: completionArtifacts
|
|
351
585
|
};
|
|
352
586
|
}
|
|
587
|
+
async runArchitect(context) {
|
|
588
|
+
const { dispatch, worktree } = context;
|
|
589
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
590
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
591
|
+
const specBody = `---\ntitle: "${dispatch.workItem.title}"\nstatus: pending_approval\nproduced_by: forge-bridge\n---\n\n# ${dispatch.workItem.title}\n\nArchitect plan pending approval.\n`;
|
|
592
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
593
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
594
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-bridge" }) + "\n", "utf8");
|
|
595
|
+
await writeCompletionArtifact({
|
|
596
|
+
worktreePath: worktree.worktreePath,
|
|
597
|
+
workItemId: dispatch.workItem.id,
|
|
598
|
+
phase: "architect",
|
|
599
|
+
producedBy: "forge-bridge",
|
|
600
|
+
redactionSecrets: exactRedactionSecrets(dispatch),
|
|
601
|
+
architectCycle: true
|
|
602
|
+
}, expectedCompletionToolName("architect"), deterministicArchitectPayload(dispatch));
|
|
603
|
+
const artifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "architect");
|
|
604
|
+
if (!artifact) {
|
|
605
|
+
throw new Error("Deterministic provider did not produce the Forge Architect completion artifact.");
|
|
606
|
+
}
|
|
607
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, artifact);
|
|
608
|
+
await emitArchitectStructuredEvents(context.emit, worktree.worktreePath, dispatch.workItem.id, artifact);
|
|
609
|
+
await context.emit({
|
|
610
|
+
role: "assistant",
|
|
611
|
+
kind: "message",
|
|
612
|
+
content: "Architect plan complete. forge-bridge produced deterministic planning artifacts.",
|
|
613
|
+
eventType: "architect_complete"
|
|
614
|
+
});
|
|
615
|
+
return {
|
|
616
|
+
summary: "forge-bridge completed Architect plan (deterministic).",
|
|
617
|
+
spec: {
|
|
618
|
+
repoPath: worktree.worktreePath,
|
|
619
|
+
path: specPath,
|
|
620
|
+
body: specBody,
|
|
621
|
+
producedBy: "forge-bridge",
|
|
622
|
+
status: "pending_approval"
|
|
623
|
+
},
|
|
624
|
+
artifact,
|
|
625
|
+
artifacts: [artifact]
|
|
626
|
+
};
|
|
627
|
+
}
|
|
353
628
|
async runQa(context) {
|
|
354
629
|
const { dispatch, worktree } = context;
|
|
355
630
|
await context.emit({
|
|
@@ -362,30 +637,45 @@ export class DeterministicProviderCliService {
|
|
|
362
637
|
const launchTarget = qaBrowser?.launchTarget ?? null;
|
|
363
638
|
const missingLaunchTarget = qaBrowser?.required === true && !hasLaunchTarget(launchTarget);
|
|
364
639
|
const browserSidecar = !missingLaunchTarget && qaBrowser?.required === true && launchTarget
|
|
365
|
-
? deterministicBrowserSidecar(dispatch.workItem.id, launchTarget)
|
|
640
|
+
? deterministicBrowserSidecar(dispatch.workItem.id, launchTarget, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId)
|
|
366
641
|
: null;
|
|
367
642
|
const browserChecks = browserSidecar && launchTarget
|
|
368
643
|
? [deterministicBrowserCheck(launchTarget, browserSidecar.relativePath)]
|
|
369
644
|
: [];
|
|
645
|
+
const sidecars = await writeQaSidecarFiles(worktree.worktreePath, browserSidecar ? [browserSidecar] : []);
|
|
370
646
|
const summary = missingLaunchTarget
|
|
371
647
|
? "QA blocked because browser testing is required but no app launch target was provided."
|
|
372
648
|
: "All checks passed. forge-bridge QA completed.";
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
649
|
+
const criterion = dispatch.workItem.acceptanceCriteria.trim() || "Forge QA evidence exists";
|
|
650
|
+
for (const gate of qaCompletionGateOrder) {
|
|
651
|
+
await writeCompletionArtifact({
|
|
652
|
+
worktreePath: worktree.worktreePath,
|
|
653
|
+
workItemId: dispatch.workItem.id,
|
|
654
|
+
phase: "qa",
|
|
655
|
+
producedBy: "forge-bridge",
|
|
656
|
+
redactionSecrets: exactRedactionSecrets(dispatch),
|
|
657
|
+
qaCycle: true,
|
|
658
|
+
ownerType: dispatch.cycleRun.ownerType ?? undefined,
|
|
659
|
+
ownerId: dispatch.cycleRun.ownerId ?? undefined
|
|
660
|
+
}, expectedQaCompletionToolName(gate), deterministicQaGatePayload({
|
|
661
|
+
gate,
|
|
662
|
+
criterion,
|
|
663
|
+
summary,
|
|
664
|
+
missingLaunchTarget,
|
|
665
|
+
browserChecks,
|
|
666
|
+
sidecars
|
|
667
|
+
}));
|
|
668
|
+
const gateArtifact = await readQaGateArtifact(qaArtifactRootForDispatch(worktree.worktreePath, dispatch.workItem.id, dispatch), gate);
|
|
669
|
+
if (gateArtifact) {
|
|
670
|
+
await emitQaGateAccepted(context.emit, worktree.worktreePath, gateArtifact, nextQaGate(gate));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const qaArtifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId);
|
|
674
|
+
if (!qaArtifact) {
|
|
675
|
+
throw new Error("Deterministic QA did not produce the complete durable QA artifact bundle.");
|
|
676
|
+
}
|
|
387
677
|
await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
|
|
388
|
-
await emitQaStructuredEvents(context.emit,
|
|
678
|
+
await emitQaStructuredEvents(context.emit, qaArtifact);
|
|
389
679
|
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
390
680
|
return {
|
|
391
681
|
summary: missingLaunchTarget ? "forge-bridge QA blocked." : "forge-bridge QA passed.",
|
|
@@ -395,22 +685,30 @@ export class DeterministicProviderCliService {
|
|
|
395
685
|
}
|
|
396
686
|
async runConflictResolve(context) {
|
|
397
687
|
const { dispatch, worktree } = context;
|
|
398
|
-
|
|
688
|
+
const addressComments = isPullRequestCommentsDispatch(dispatch);
|
|
689
|
+
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
690
|
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
400
691
|
await context.emit({
|
|
401
692
|
role: "assistant",
|
|
402
693
|
kind: "message",
|
|
403
|
-
content:
|
|
404
|
-
|
|
694
|
+
content: addressComments
|
|
695
|
+
? `Deterministic PR comment addressing completed for ${worktree.branchName}.`
|
|
696
|
+
: `Deterministic conflict resolve completed for ${worktree.branchName}.`,
|
|
697
|
+
eventType: addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
|
|
405
698
|
});
|
|
406
699
|
return {
|
|
407
|
-
summary:
|
|
700
|
+
summary: addressComments
|
|
701
|
+
? `forge-bridge addressed PR comments for ${worktree.branchName}.`
|
|
702
|
+
: `forge-bridge resolved conflicts for ${worktree.branchName}.`
|
|
408
703
|
};
|
|
409
704
|
}
|
|
410
705
|
}
|
|
411
706
|
function phaseCompleteEventType(phase) {
|
|
412
707
|
return phase === "implementation" ? "implementation_complete" : `${phase}_complete`;
|
|
413
708
|
}
|
|
709
|
+
function isDebugCompletionPhaseForProvider(phase) {
|
|
710
|
+
return debugCompletionPhaseOrder.includes(phase);
|
|
711
|
+
}
|
|
414
712
|
function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
415
713
|
const changesetHash = sha256(changesetBody);
|
|
416
714
|
const criterion = dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists";
|
|
@@ -433,6 +731,56 @@ function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
|
433
731
|
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation."
|
|
434
732
|
};
|
|
435
733
|
}
|
|
734
|
+
if (phase === "debug_repro") {
|
|
735
|
+
return {
|
|
736
|
+
status: "pass",
|
|
737
|
+
summary: "forge-bridge reproduced the bug deterministically.",
|
|
738
|
+
repro_steps: ["Run the deterministic fake-worker debug reproduction."],
|
|
739
|
+
observed: "The configured bug scenario is reproducible in deterministic bridge mode.",
|
|
740
|
+
expected: "The bug scenario should be fixed before QA handoff.",
|
|
741
|
+
evidence: [{ kind: "changeset", path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash }]
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
if (phase === "debug_root_cause") {
|
|
745
|
+
return {
|
|
746
|
+
status: "pass",
|
|
747
|
+
summary: "forge-bridge isolated a deterministic root cause.",
|
|
748
|
+
root_cause: "The deterministic fake-worker bug is represented by the generated changeset evidence.",
|
|
749
|
+
evidence: [{ kind: "changeset", path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash }],
|
|
750
|
+
eliminated_alternatives: [{ summary: "No alternate deterministic fake-worker path was implicated." }],
|
|
751
|
+
risk_area: "forge-bridge deterministic debug lane"
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (phase === "debug_fix") {
|
|
755
|
+
return {
|
|
756
|
+
status: "pass",
|
|
757
|
+
summary: "forge-bridge applied a deterministic debug fix.",
|
|
758
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic debug fix evidence." }],
|
|
759
|
+
tests_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic debug evidence doubles as fake-worker regression coverage." }],
|
|
760
|
+
remaining_risks: [],
|
|
761
|
+
resolved_findings: (dispatch.qaRework?.findingIds ?? []).map((qaId) => ({
|
|
762
|
+
qa_id: qaId,
|
|
763
|
+
status: "resolved",
|
|
764
|
+
summary: `Resolved deterministic QA finding ${qaId}.`,
|
|
765
|
+
evidence: { path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash },
|
|
766
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic debug fix evidence." }],
|
|
767
|
+
tests_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic fake-worker regression evidence." }],
|
|
768
|
+
regression_evidence: [{ command: "forge-bridge deterministic debug regression", status: "pass" }]
|
|
769
|
+
}))
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
if (phase === "debug_regression") {
|
|
773
|
+
return {
|
|
774
|
+
status: "pass",
|
|
775
|
+
summary: "forge-bridge proved the deterministic debug regression is QA-ready.",
|
|
776
|
+
commands: [{ command: "forge-bridge deterministic debug regression", exit_code: 0, status: "pass", summary: "Deterministic debug regression completed." }],
|
|
777
|
+
required_regressions: ["Original deterministic repro no longer fails."],
|
|
778
|
+
build_status: "not_applicable",
|
|
779
|
+
test_status: "pass",
|
|
780
|
+
repro_negative: true,
|
|
781
|
+
qa_ready: true
|
|
782
|
+
};
|
|
783
|
+
}
|
|
436
784
|
if (phase === "self_review") {
|
|
437
785
|
return {
|
|
438
786
|
status: "pass",
|
|
@@ -493,6 +841,149 @@ function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
|
493
841
|
ready_for_dev_test: true
|
|
494
842
|
};
|
|
495
843
|
}
|
|
844
|
+
function deterministicArchitectPayload(dispatch) {
|
|
845
|
+
const planId = `architect-${dispatch.workItem.id.replace(/-/g, "")}`;
|
|
846
|
+
return {
|
|
847
|
+
status: "pass",
|
|
848
|
+
summary: "forge-bridge produced a deterministic Architect plan.",
|
|
849
|
+
implementation_plan: {
|
|
850
|
+
schema_version: 1,
|
|
851
|
+
plan_id: planId,
|
|
852
|
+
story_id: dispatch.workItem.id,
|
|
853
|
+
phases: [
|
|
854
|
+
{ id: "phase-1", name: "Foundation", tasks: ["task-1"], gate: "phase-1-gate" }
|
|
855
|
+
],
|
|
856
|
+
tasks: [
|
|
857
|
+
{
|
|
858
|
+
id: "task-1",
|
|
859
|
+
title: `Implement ${dispatch.workItem.title}`,
|
|
860
|
+
phase: "phase-1",
|
|
861
|
+
summary: "Deterministic bridge Architect task proving MCP artifact contract persistence.",
|
|
862
|
+
acceptance_criteria: [dispatch.workItem.acceptanceCriteria || `Preserve story intent for ${dispatch.workItem.title}`],
|
|
863
|
+
tests: [
|
|
864
|
+
{ command: "dotnet test /home/dillon/repos/Forge/Forge.sln --filter Architect", purpose: "Architect bridge contract regression" }
|
|
865
|
+
]
|
|
866
|
+
}
|
|
867
|
+
],
|
|
868
|
+
dependencies: [],
|
|
869
|
+
phase_gates: [
|
|
870
|
+
{ id: "phase-1-gate", phase: "phase-1", checks: ["Task tests pass", "Artifacts validate"] }
|
|
871
|
+
]
|
|
872
|
+
},
|
|
873
|
+
qa_plan: {
|
|
874
|
+
schema_version: 1,
|
|
875
|
+
plan_id: planId,
|
|
876
|
+
final_qa: [
|
|
877
|
+
{ id: "qa-1", coverage: `Verify Architect plan artifacts and pending approval status for ${dispatch.workItem.title}` }
|
|
878
|
+
]
|
|
879
|
+
},
|
|
880
|
+
report_markdown: `# Architect Report\n\nWork item: ${dispatch.workItem.id}\nTitle: ${dispatch.workItem.title}\n\nVerdict: pass\n\nDeterministic bridge Architect plan produced for pending approval.\n`
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function deterministicQaGatePayload(options) {
|
|
884
|
+
const blocked = options.missingLaunchTarget;
|
|
885
|
+
const skippedBecauseBlocked = blocked
|
|
886
|
+
? {
|
|
887
|
+
status: "skipped",
|
|
888
|
+
summary: "Skipped because browser QA was blocked by missing launch target.",
|
|
889
|
+
reason: "Upstream browser QA blocker prevents meaningful downstream evidence."
|
|
890
|
+
}
|
|
891
|
+
: null;
|
|
892
|
+
switch (options.gate) {
|
|
893
|
+
case "qa_plan":
|
|
894
|
+
return {
|
|
895
|
+
status: "pass",
|
|
896
|
+
summary: "Deterministic QA plan recorded.",
|
|
897
|
+
checklist: [
|
|
898
|
+
"Review the validated work item against acceptance criteria.",
|
|
899
|
+
"Run deterministic regression evidence.",
|
|
900
|
+
"Confirm no in-scope QA findings remain."
|
|
901
|
+
],
|
|
902
|
+
acceptance_criteria: [options.criterion]
|
|
903
|
+
};
|
|
904
|
+
case "qa_regression":
|
|
905
|
+
return {
|
|
906
|
+
status: "pass",
|
|
907
|
+
summary: "Deterministic regression QA completed.",
|
|
908
|
+
commands: [
|
|
909
|
+
{
|
|
910
|
+
command: "forge-bridge deterministic QA",
|
|
911
|
+
exit_code: 0,
|
|
912
|
+
status: "pass",
|
|
913
|
+
summary: "Deterministic regression evidence completed."
|
|
914
|
+
}
|
|
915
|
+
],
|
|
916
|
+
notes: "Regression gate passed in deterministic fake-worker mode."
|
|
917
|
+
};
|
|
918
|
+
case "qa_api":
|
|
919
|
+
return {
|
|
920
|
+
status: "skipped",
|
|
921
|
+
summary: "No API/backend surface was identified for deterministic QA.",
|
|
922
|
+
reason: "The deterministic fake-worker changeset does not exercise API/backend behavior."
|
|
923
|
+
};
|
|
924
|
+
case "qa_manual_web":
|
|
925
|
+
return blocked
|
|
926
|
+
? {
|
|
927
|
+
status: "blocked",
|
|
928
|
+
summary: "Browser QA could not run because no app launch target was provided.",
|
|
929
|
+
blocker_reason: "missing_app_launch_target"
|
|
930
|
+
}
|
|
931
|
+
: options.browserChecks.length > 0
|
|
932
|
+
? {
|
|
933
|
+
status: "pass",
|
|
934
|
+
summary: "Deterministic browser QA exercised the configured launch target.",
|
|
935
|
+
browser_checks: options.browserChecks,
|
|
936
|
+
sidecars: options.sidecars,
|
|
937
|
+
notes: "Browser sidecar evidence recorded."
|
|
938
|
+
}
|
|
939
|
+
: {
|
|
940
|
+
status: "skipped",
|
|
941
|
+
summary: "Manual web QA was not required for this deterministic run.",
|
|
942
|
+
reason: "Forge did not require browser QA and no launch target was necessary."
|
|
943
|
+
};
|
|
944
|
+
case "qa_bug_repro":
|
|
945
|
+
return skippedBecauseBlocked ?? {
|
|
946
|
+
status: "skipped",
|
|
947
|
+
summary: "No failed QA observation required bug reproduction.",
|
|
948
|
+
reason: "No defects or candidate bugs were observed."
|
|
949
|
+
};
|
|
950
|
+
case "qa_findings":
|
|
951
|
+
return skippedBecauseBlocked ?? {
|
|
952
|
+
status: "pass",
|
|
953
|
+
summary: "No QA findings or candidate bugs were identified.",
|
|
954
|
+
findings: [],
|
|
955
|
+
candidate_bugs: []
|
|
956
|
+
};
|
|
957
|
+
case "qa_verdict":
|
|
958
|
+
return {
|
|
959
|
+
status: blocked ? "blocked" : "pass",
|
|
960
|
+
summary: options.summary,
|
|
961
|
+
checklist: [
|
|
962
|
+
"Review the validated work item against acceptance criteria.",
|
|
963
|
+
"Run deterministic regression evidence.",
|
|
964
|
+
blocked ? "Record missing browser launch target blocker." : "Confirm no in-scope QA findings remain."
|
|
965
|
+
],
|
|
966
|
+
tested_acceptance_criteria: blocked ? [] : [options.criterion],
|
|
967
|
+
commands: blocked
|
|
968
|
+
? []
|
|
969
|
+
: [
|
|
970
|
+
{
|
|
971
|
+
command: "forge-bridge deterministic QA",
|
|
972
|
+
exit_code: 0,
|
|
973
|
+
status: "pass",
|
|
974
|
+
summary: "Deterministic QA command completed."
|
|
975
|
+
}
|
|
976
|
+
],
|
|
977
|
+
browser_checks: blocked ? [] : options.browserChecks,
|
|
978
|
+
sidecars: blocked ? [] : options.sidecars,
|
|
979
|
+
notes: options.summary,
|
|
980
|
+
findings: [],
|
|
981
|
+
candidate_bugs: [],
|
|
982
|
+
blocker_reason: blocked ? "missing_app_launch_target" : null,
|
|
983
|
+
report_markdown: `# QA report\n\n${options.summary}\n`
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
}
|
|
496
987
|
const qaArtifactFileNames = {
|
|
497
988
|
plan: "qa.plan.json",
|
|
498
989
|
evidence: "qa.evidence.json",
|
|
@@ -500,16 +991,25 @@ const qaArtifactFileNames = {
|
|
|
500
991
|
verdict: "qa.verdict.json",
|
|
501
992
|
report: "qa.report.md"
|
|
502
993
|
};
|
|
994
|
+
const qaGateArtifactFileNames = {
|
|
995
|
+
qa_plan: "qa.plan.json",
|
|
996
|
+
qa_regression: "qa.regression.json",
|
|
997
|
+
qa_api: "qa.api.json",
|
|
998
|
+
qa_manual_web: "qa.manual-web.json",
|
|
999
|
+
qa_bug_repro: "qa.bug-repro.json",
|
|
1000
|
+
qa_findings: "qa.findings.json",
|
|
1001
|
+
qa_verdict: "qa.verdict.json"
|
|
1002
|
+
};
|
|
503
1003
|
function hasLaunchTarget(target) {
|
|
504
1004
|
return Boolean(target?.url?.trim()
|
|
505
1005
|
|| target?.ready_url?.trim()
|
|
506
1006
|
|| target?.command?.trim()
|
|
507
1007
|
|| (target?.routes ?? []).some((route) => route.trim().length > 0));
|
|
508
1008
|
}
|
|
509
|
-
function deterministicBrowserSidecar(workItemId, target) {
|
|
1009
|
+
function deterministicBrowserSidecar(workItemId, target, ownerType, ownerId) {
|
|
510
1010
|
const route = primaryLaunchRoute(target);
|
|
511
1011
|
return {
|
|
512
|
-
relativePath: qaArtifactRelativePath(workItemId, "qa/screenshots/deterministic-browser-check.txt"),
|
|
1012
|
+
relativePath: qaArtifactRelativePath(workItemId, "qa/screenshots/deterministic-browser-check.txt", ownerType, ownerId),
|
|
513
1013
|
content: `Deterministic Forge QA browser evidence\nRoute: ${route}\nConsole errors: []\nNetwork errors: []\nResponsive checks: desktop, mobile\n`
|
|
514
1014
|
};
|
|
515
1015
|
}
|
|
@@ -551,78 +1051,6 @@ async function writeQaSidecarFiles(worktreePath, files) {
|
|
|
551
1051
|
}
|
|
552
1052
|
return sidecars;
|
|
553
1053
|
}
|
|
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
1054
|
async function emitQaArtifactRecorded(emit, worktreePath, artifact) {
|
|
627
1055
|
const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
|
|
628
1056
|
await emit({
|
|
@@ -638,16 +1066,29 @@ async function emitQaArtifactRecorded(emit, worktreePath, artifact) {
|
|
|
638
1066
|
eventType: "phase_gate"
|
|
639
1067
|
});
|
|
640
1068
|
}
|
|
641
|
-
async function readQaCompletionArtifactBundle(worktreePath, workItemId) {
|
|
642
|
-
const artifact = await readCompletionArtifact(worktreePath, workItemId, "qa");
|
|
643
|
-
if (!artifact || !await hasCompleteQaArtifactBundle(worktreePath, workItemId)) {
|
|
1069
|
+
async function readQaCompletionArtifactBundle(worktreePath, workItemId, ownerType, ownerId) {
|
|
1070
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, "qa", ownerType, ownerId);
|
|
1071
|
+
if (!artifact || !await hasCompleteQaArtifactBundle(worktreePath, workItemId, ownerType, ownerId)) {
|
|
644
1072
|
return null;
|
|
645
1073
|
}
|
|
646
|
-
|
|
647
|
-
|
|
1074
|
+
const artifactRoot = ownerType && ownerId
|
|
1075
|
+
? artifactRootForOwner(worktreePath, ownerType, ownerId)
|
|
1076
|
+
: artifactRootFor(worktreePath, workItemId);
|
|
1077
|
+
const planJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.plan), "utf8");
|
|
1078
|
+
const evidenceJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.evidence), "utf8");
|
|
1079
|
+
const findingsJson = await fs.readFile(path.join(artifactRoot, qaArtifactFileNames.findings), "utf8");
|
|
1080
|
+
await validateQaBundleSidecarReferences(worktreePath, workItemId, ownerType, ownerId);
|
|
1081
|
+
return {
|
|
1082
|
+
...artifact,
|
|
1083
|
+
planJson,
|
|
1084
|
+
evidenceJson,
|
|
1085
|
+
findingsJson
|
|
1086
|
+
};
|
|
648
1087
|
}
|
|
649
|
-
async function hasCompleteQaArtifactBundle(worktreePath, workItemId) {
|
|
650
|
-
const artifactRoot =
|
|
1088
|
+
async function hasCompleteQaArtifactBundle(worktreePath, workItemId, ownerType, ownerId) {
|
|
1089
|
+
const artifactRoot = ownerType && ownerId
|
|
1090
|
+
? artifactRootForOwner(worktreePath, ownerType, ownerId)
|
|
1091
|
+
: artifactRootFor(worktreePath, workItemId);
|
|
651
1092
|
for (const fileName of Object.values(qaArtifactFileNames)) {
|
|
652
1093
|
try {
|
|
653
1094
|
const stat = await fs.stat(path.join(artifactRoot, fileName));
|
|
@@ -676,8 +1117,10 @@ function enforceQaBrowserLaunchTargetGate(dispatch, artifact) {
|
|
|
676
1117
|
throw new Error("QA browser testing is required but no app launch target was provided; provider must return blocked with blocker_reason missing_app_launch_target.");
|
|
677
1118
|
}
|
|
678
1119
|
}
|
|
679
|
-
async function validateQaBundleSidecarReferences(worktreePath, workItemId) {
|
|
680
|
-
const artifactRoot =
|
|
1120
|
+
async function validateQaBundleSidecarReferences(worktreePath, workItemId, ownerType, ownerId) {
|
|
1121
|
+
const artifactRoot = ownerType && ownerId
|
|
1122
|
+
? artifactRootForOwner(worktreePath, ownerType, ownerId)
|
|
1123
|
+
: artifactRootFor(worktreePath, workItemId);
|
|
681
1124
|
const evidence = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.evidence));
|
|
682
1125
|
const sidecars = arrayField(evidence, "sidecars");
|
|
683
1126
|
if (sidecars.length === 0) {
|
|
@@ -696,7 +1139,9 @@ async function validateQaBundleSidecarReferences(worktreePath, workItemId) {
|
|
|
696
1139
|
}
|
|
697
1140
|
const fullPath = path.resolve(worktreePath, sidecarPath);
|
|
698
1141
|
if (!isPathUnderRoot(qaSidecarRoot, fullPath)) {
|
|
699
|
-
|
|
1142
|
+
const ownerDirectory = ownerType && ownerId && ownerType.trim().toLowerCase() === "story" ? "stories" : "work-items";
|
|
1143
|
+
const ownerSegment = ownerType && ownerId ? ownerId : workItemId;
|
|
1144
|
+
throw new Error(`QA sidecar ${index} path must be under .forge/${ownerDirectory}/${ownerSegment}/artifacts/qa/.`);
|
|
700
1145
|
}
|
|
701
1146
|
const stat = await fs.lstat(fullPath).catch((error) => {
|
|
702
1147
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
@@ -717,8 +1162,8 @@ async function validateQaBundleSidecarReferences(worktreePath, workItemId) {
|
|
|
717
1162
|
}
|
|
718
1163
|
}
|
|
719
1164
|
}
|
|
720
|
-
async function emitQaStructuredEvents(emit,
|
|
721
|
-
const artifactRoot =
|
|
1165
|
+
async function emitQaStructuredEvents(emit, artifact) {
|
|
1166
|
+
const artifactRoot = path.dirname(artifact.path);
|
|
722
1167
|
const plan = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.plan));
|
|
723
1168
|
const evidence = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.evidence));
|
|
724
1169
|
const findings = await readJsonObjectFile(path.join(artifactRoot, qaArtifactFileNames.findings));
|
|
@@ -758,12 +1203,76 @@ async function emitQaStructuredEvents(emit, worktreePath, workItemId, artifact)
|
|
|
758
1203
|
eventType: status === "blocked" ? "qa_blocked" : "qa_complete"
|
|
759
1204
|
});
|
|
760
1205
|
}
|
|
1206
|
+
function qaArtifactRootForDispatch(worktreePath, workItemId, dispatch) {
|
|
1207
|
+
return dispatch.cycleRun.ownerType && dispatch.cycleRun.ownerId
|
|
1208
|
+
? artifactRootForOwner(worktreePath, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId)
|
|
1209
|
+
: artifactRootFor(worktreePath, workItemId);
|
|
1210
|
+
}
|
|
1211
|
+
async function readQaGateArtifact(artifactRoot, gate) {
|
|
1212
|
+
const artifactPath = path.join(artifactRoot, qaGateArtifactFileNames[gate]);
|
|
1213
|
+
try {
|
|
1214
|
+
const stat = await fs.stat(artifactPath);
|
|
1215
|
+
if (!stat.isFile()) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
throw error;
|
|
1224
|
+
}
|
|
1225
|
+
const payload = await readJsonObjectFile(artifactPath);
|
|
1226
|
+
return {
|
|
1227
|
+
gate,
|
|
1228
|
+
path: artifactPath,
|
|
1229
|
+
status: stringField(payload, "status") ?? "unknown",
|
|
1230
|
+
producedBy: stringField(payload, "produced_by") ?? stringField(payload, "producedBy") ?? undefined,
|
|
1231
|
+
contentHash: stringField(payload, "content_hash") ?? undefined
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function nextQaGate(gate) {
|
|
1235
|
+
const index = qaCompletionGateOrder.indexOf(gate);
|
|
1236
|
+
return index >= 0 ? qaCompletionGateOrder[index + 1] ?? null : null;
|
|
1237
|
+
}
|
|
1238
|
+
async function emitQaGateAccepted(emit, worktreePath, artifact, nextGate) {
|
|
1239
|
+
const relativePath = path.relative(worktreePath, artifact.path).split(path.sep).join("/");
|
|
1240
|
+
await emit({
|
|
1241
|
+
role: "hook",
|
|
1242
|
+
kind: artifact.status === "blocked" ? "error" : "checkpoint",
|
|
1243
|
+
content: `phase_gate: ${artifact.gate} artifact accepted with status ${artifact.status}.`,
|
|
1244
|
+
eventType: "phase_gate",
|
|
1245
|
+
data: JSON.stringify({
|
|
1246
|
+
phase: "qa",
|
|
1247
|
+
qa_gate: artifact.gate,
|
|
1248
|
+
status: artifact.status,
|
|
1249
|
+
artifact_relative_paths: [relativePath],
|
|
1250
|
+
content_hash: artifact.contentHash ?? null,
|
|
1251
|
+
produced_by: artifact.producedBy ?? null
|
|
1252
|
+
})
|
|
1253
|
+
});
|
|
1254
|
+
if (!nextGate) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
await emit({
|
|
1258
|
+
role: "hook",
|
|
1259
|
+
kind: "checkpoint",
|
|
1260
|
+
content: `phase_progression: ${artifact.gate} -> ${nextGate}`,
|
|
1261
|
+
eventType: "phase_gate",
|
|
1262
|
+
data: JSON.stringify({
|
|
1263
|
+
phase: "qa",
|
|
1264
|
+
from_gate: artifact.gate,
|
|
1265
|
+
to_gate: nextGate
|
|
1266
|
+
})
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
761
1269
|
class LiveQaArtifactEmitter {
|
|
762
1270
|
worktreePath;
|
|
763
1271
|
workItemId;
|
|
764
1272
|
dispatch;
|
|
765
1273
|
emit;
|
|
766
|
-
|
|
1274
|
+
emittedBundle = false;
|
|
1275
|
+
emittedGates = new Set();
|
|
767
1276
|
timer = null;
|
|
768
1277
|
pending = Promise.resolve();
|
|
769
1278
|
constructor(worktreePath, workItemId, dispatch, emit) {
|
|
@@ -786,7 +1295,7 @@ class LiveQaArtifactEmitter {
|
|
|
786
1295
|
await this.pollQueued();
|
|
787
1296
|
}
|
|
788
1297
|
hasEmitted() {
|
|
789
|
-
return this.
|
|
1298
|
+
return this.emittedBundle;
|
|
790
1299
|
}
|
|
791
1300
|
pollQueued() {
|
|
792
1301
|
this.pending = this.pending
|
|
@@ -795,19 +1304,125 @@ class LiveQaArtifactEmitter {
|
|
|
795
1304
|
return this.pending;
|
|
796
1305
|
}
|
|
797
1306
|
async poll() {
|
|
798
|
-
|
|
1307
|
+
await this.emitAvailableGateEvents();
|
|
1308
|
+
if (this.emittedBundle) {
|
|
799
1309
|
return;
|
|
800
1310
|
}
|
|
801
|
-
const artifact = await readQaCompletionArtifactBundle(this.worktreePath, this.workItemId);
|
|
1311
|
+
const artifact = await readQaCompletionArtifactBundle(this.worktreePath, this.workItemId, this.dispatch.cycleRun.ownerType, this.dispatch.cycleRun.ownerId);
|
|
802
1312
|
if (!artifact) {
|
|
803
1313
|
return;
|
|
804
1314
|
}
|
|
805
1315
|
enforceQaBrowserLaunchTargetGate(this.dispatch, artifact);
|
|
806
|
-
this.
|
|
1316
|
+
this.emittedBundle = true;
|
|
807
1317
|
await emitQaArtifactRecorded(this.emit, this.worktreePath, artifact);
|
|
808
|
-
await emitQaStructuredEvents(this.emit,
|
|
1318
|
+
await emitQaStructuredEvents(this.emit, artifact);
|
|
1319
|
+
}
|
|
1320
|
+
async emitAvailableGateEvents() {
|
|
1321
|
+
const artifactRoot = qaArtifactRootForDispatch(this.worktreePath, this.workItemId, this.dispatch);
|
|
1322
|
+
for (const gate of qaCompletionGateOrder) {
|
|
1323
|
+
if (this.emittedGates.has(gate)) {
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const artifact = await readQaGateArtifact(artifactRoot, gate);
|
|
1327
|
+
if (!artifact) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
this.emittedGates.add(gate);
|
|
1331
|
+
await emitQaGateAccepted(this.emit, this.worktreePath, artifact, nextQaGate(gate));
|
|
1332
|
+
}
|
|
809
1333
|
}
|
|
810
1334
|
}
|
|
1335
|
+
class LiveArchitectArtifactEmitter {
|
|
1336
|
+
worktreePath;
|
|
1337
|
+
workItemId;
|
|
1338
|
+
emit;
|
|
1339
|
+
emitted = false;
|
|
1340
|
+
timer = null;
|
|
1341
|
+
pending = Promise.resolve();
|
|
1342
|
+
constructor(worktreePath, workItemId, emit) {
|
|
1343
|
+
this.worktreePath = worktreePath;
|
|
1344
|
+
this.workItemId = workItemId;
|
|
1345
|
+
this.emit = emit;
|
|
1346
|
+
}
|
|
1347
|
+
start() {
|
|
1348
|
+
void this.pollQueued();
|
|
1349
|
+
this.timer = setInterval(() => {
|
|
1350
|
+
void this.pollQueued();
|
|
1351
|
+
}, 250);
|
|
1352
|
+
}
|
|
1353
|
+
async stop() {
|
|
1354
|
+
if (this.timer) {
|
|
1355
|
+
clearInterval(this.timer);
|
|
1356
|
+
this.timer = null;
|
|
1357
|
+
}
|
|
1358
|
+
await this.pollQueued();
|
|
1359
|
+
}
|
|
1360
|
+
hasEmitted() {
|
|
1361
|
+
return this.emitted;
|
|
1362
|
+
}
|
|
1363
|
+
pollQueued() {
|
|
1364
|
+
this.pending = this.pending
|
|
1365
|
+
.then(() => this.poll())
|
|
1366
|
+
.catch(() => undefined);
|
|
1367
|
+
return this.pending;
|
|
1368
|
+
}
|
|
1369
|
+
async poll() {
|
|
1370
|
+
if (this.emitted || !await hasCompleteArchitectArtifactBundle(this.worktreePath, this.workItemId)) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const artifact = await readCompletionArtifact(this.worktreePath, this.workItemId, "architect");
|
|
1374
|
+
if (!artifact) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
this.emitted = true;
|
|
1378
|
+
await emitCompletionArtifactRecorded(this.emit, this.worktreePath, artifact);
|
|
1379
|
+
await emitArchitectStructuredEvents(this.emit, this.worktreePath, this.workItemId, artifact);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
async function hasCompleteArchitectArtifactBundle(worktreePath, workItemId) {
|
|
1383
|
+
const artifactRoot = artifactRootFor(worktreePath, workItemId);
|
|
1384
|
+
for (const fileName of ["implementation.plan.json", "qa.plan.json", "architect.verdict.json", "architect.report.md"]) {
|
|
1385
|
+
try {
|
|
1386
|
+
const stat = await fs.stat(path.join(artifactRoot, fileName));
|
|
1387
|
+
if (!stat.isFile()) {
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
throw error;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
async function emitArchitectStructuredEvents(emit, worktreePath, workItemId, artifact) {
|
|
1401
|
+
const artifactRoot = artifactRootFor(worktreePath, workItemId);
|
|
1402
|
+
const implementationPlan = await readJsonObjectFile(path.join(artifactRoot, "implementation.plan.json"));
|
|
1403
|
+
const qaPlan = await readJsonObjectFile(path.join(artifactRoot, "qa.plan.json"));
|
|
1404
|
+
const verdict = parseJsonLine(artifact.verdictJson) ?? {};
|
|
1405
|
+
const status = stringField(verdict, "status") ?? artifact.status ?? "unknown";
|
|
1406
|
+
const planId = stringField(implementationPlan, "plan_id") ?? "unknown";
|
|
1407
|
+
await emit({
|
|
1408
|
+
role: "worker",
|
|
1409
|
+
kind: "checkpoint",
|
|
1410
|
+
content: `Architect plan recorded: ${planId} with ${arrayField(implementationPlan, "phases").length} phases and ${arrayField(implementationPlan, "tasks").length} tasks.`,
|
|
1411
|
+
eventType: "architect_plan"
|
|
1412
|
+
});
|
|
1413
|
+
await emit({
|
|
1414
|
+
role: "worker",
|
|
1415
|
+
kind: "checkpoint",
|
|
1416
|
+
content: `Architect QA seed recorded with ${arrayField(qaPlan, "final_qa").length} final QA items.`,
|
|
1417
|
+
eventType: "architect_qa_seed"
|
|
1418
|
+
});
|
|
1419
|
+
await emit({
|
|
1420
|
+
role: "hook",
|
|
1421
|
+
kind: status === "pass" ? "checkpoint" : "error",
|
|
1422
|
+
content: `architect_mcp_complete: ${status === "pass" ? "accepted" : "recorded"} - ${stringField(verdict, "summary") ?? "Architect completion payload recorded."}`,
|
|
1423
|
+
eventType: "architect_mcp_complete"
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
811
1426
|
async function readJsonObjectFile(filePath) {
|
|
812
1427
|
const content = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
813
1428
|
return parseJsonLine(content) ?? {};
|
|
@@ -832,19 +1447,36 @@ function summarizeQaObject(source) {
|
|
|
832
1447
|
?? stringField(source, "id")
|
|
833
1448
|
?? JSON.stringify(source), 500);
|
|
834
1449
|
}
|
|
835
|
-
function qaArtifactRelativePath(workItemId, fileName) {
|
|
1450
|
+
function qaArtifactRelativePath(workItemId, fileName, ownerType, ownerId) {
|
|
1451
|
+
if (ownerType && ownerId) {
|
|
1452
|
+
const ownerDirectory = ownerType.trim().toLowerCase() === "story" ? "stories" : "work-items";
|
|
1453
|
+
return [".forge", ownerDirectory, ownerId, "artifacts", fileName].join("/");
|
|
1454
|
+
}
|
|
836
1455
|
return [".forge", "work-items", workItemId, "artifacts", fileName].join("/");
|
|
837
1456
|
}
|
|
838
|
-
function jsonText(value) {
|
|
839
|
-
return `${JSON.stringify(value, null, 2)}\n`;
|
|
840
|
-
}
|
|
841
1457
|
async function emitCompletionArtifactRecorded(emit, worktreePath, artifact) {
|
|
842
1458
|
const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
|
|
1459
|
+
await emit({
|
|
1460
|
+
role: "tool",
|
|
1461
|
+
kind: "checkpoint",
|
|
1462
|
+
content: `Forge completion tool validation passed for ${artifact.phase ?? "unknown"} (${artifact.status}).`,
|
|
1463
|
+
eventType: "completion_tool_validation_passed",
|
|
1464
|
+
data: JSON.stringify({
|
|
1465
|
+
phase: artifact.phase ?? "unknown",
|
|
1466
|
+
status: artifact.status,
|
|
1467
|
+
artifact_relative_paths: [relativePath.split(path.sep).join("/")]
|
|
1468
|
+
})
|
|
1469
|
+
});
|
|
843
1470
|
await emit({
|
|
844
1471
|
role: "tool",
|
|
845
1472
|
kind: "tool_result",
|
|
846
1473
|
content: `Forge completion tool recorded ${artifact.phase ?? "unknown"} artifact ${relativePath} (${artifact.status}).`,
|
|
847
|
-
eventType: "completion_tool_artifact_recorded"
|
|
1474
|
+
eventType: "completion_tool_artifact_recorded",
|
|
1475
|
+
data: JSON.stringify({
|
|
1476
|
+
phase: artifact.phase ?? "unknown",
|
|
1477
|
+
status: artifact.status,
|
|
1478
|
+
artifact_relative_paths: [relativePath.split(path.sep).join("/")]
|
|
1479
|
+
})
|
|
848
1480
|
});
|
|
849
1481
|
}
|
|
850
1482
|
async function emitPhaseCompletionEvent(emit, artifact) {
|
|
@@ -872,7 +1504,15 @@ async function emitPhaseGateEvents(emit, artifact) {
|
|
|
872
1504
|
? "phase_progression: implementation -> self_review"
|
|
873
1505
|
: phase === "self_review"
|
|
874
1506
|
? "phase_progression: self_review -> validation"
|
|
875
|
-
:
|
|
1507
|
+
: phase === "debug_repro"
|
|
1508
|
+
? "phase_progression: debug_repro -> debug_root_cause"
|
|
1509
|
+
: phase === "debug_root_cause"
|
|
1510
|
+
? "phase_progression: debug_root_cause -> debug_fix"
|
|
1511
|
+
: phase === "debug_fix"
|
|
1512
|
+
? "phase_progression: debug_fix -> debug_regression"
|
|
1513
|
+
: phase === "debug_regression"
|
|
1514
|
+
? "phase_progression: debug_regression -> QA ready"
|
|
1515
|
+
: "phase_progression: validation -> QA ready";
|
|
876
1516
|
await emit({
|
|
877
1517
|
role: "hook",
|
|
878
1518
|
kind: "checkpoint",
|
|
@@ -883,6 +1523,7 @@ async function emitPhaseGateEvents(emit, artifact) {
|
|
|
883
1523
|
function completionPhaseFromArtifact(artifact) {
|
|
884
1524
|
const phase = artifact.phase?.trim();
|
|
885
1525
|
return phase === "self_review" || phase === "validation" || phase === "implementation" || phase === "qa"
|
|
1526
|
+
|| phase === "architect" || phase === "debug_repro" || phase === "debug_root_cause" || phase === "debug_fix" || phase === "debug_regression"
|
|
886
1527
|
? phase
|
|
887
1528
|
: "implementation";
|
|
888
1529
|
}
|
|
@@ -894,13 +1535,18 @@ class LiveCompletionArtifactEmitter {
|
|
|
894
1535
|
emitted = new Set();
|
|
895
1536
|
timer = null;
|
|
896
1537
|
pending = Promise.resolve();
|
|
897
|
-
constructor(worktreePath, workItemId, phase, developCycle, emit) {
|
|
1538
|
+
constructor(worktreePath, workItemId, phase, developCycle, debugCycle, emit) {
|
|
898
1539
|
this.worktreePath = worktreePath;
|
|
899
1540
|
this.workItemId = workItemId;
|
|
900
1541
|
this.emit = emit;
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1542
|
+
if (debugCycle && isDebugCompletionPhaseForProvider(phase)) {
|
|
1543
|
+
this.phases = debugCompletionPhaseOrder.slice(debugCompletionPhaseOrder.indexOf(phase));
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
this.phases = developCycle
|
|
1547
|
+
? developCyclePhaseOrder.slice(developCyclePhaseOrder.indexOf(phase))
|
|
1548
|
+
: [phase];
|
|
1549
|
+
}
|
|
904
1550
|
}
|
|
905
1551
|
start() {
|
|
906
1552
|
void this.pollQueued();
|
|
@@ -943,20 +1589,23 @@ class LiveCompletionArtifactEmitter {
|
|
|
943
1589
|
}
|
|
944
1590
|
}
|
|
945
1591
|
}
|
|
946
|
-
async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, developCycle) {
|
|
947
|
-
if (!developCycle) {
|
|
1592
|
+
async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, developCycle, debugCycle) {
|
|
1593
|
+
if (!developCycle && !debugCycle) {
|
|
948
1594
|
const artifact = await readCompletionArtifact(worktreePath, workItemId, phase);
|
|
949
1595
|
if (!artifact) {
|
|
950
1596
|
throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
|
|
951
1597
|
}
|
|
952
1598
|
return [artifact];
|
|
953
1599
|
}
|
|
1600
|
+
const expectedPhases = debugCycle && isDebugCompletionPhaseForProvider(phase)
|
|
1601
|
+
? debugCompletionPhaseOrder.slice(debugCompletionPhaseOrder.indexOf(phase))
|
|
1602
|
+
: ["implementation", "self_review", "validation"];
|
|
954
1603
|
const artifacts = [];
|
|
955
|
-
for (const expectedPhase of
|
|
1604
|
+
for (const expectedPhase of expectedPhases) {
|
|
956
1605
|
const artifact = await readCompletionArtifact(worktreePath, workItemId, expectedPhase);
|
|
957
1606
|
if (!artifact) {
|
|
958
1607
|
if (artifacts.length === 0) {
|
|
959
|
-
throw new Error(
|
|
1608
|
+
throw new Error(`Provider CLI completed but did not call the Forge ${expectedPhase} completion tool.`);
|
|
960
1609
|
}
|
|
961
1610
|
const previous = artifacts[artifacts.length - 1];
|
|
962
1611
|
if ((previous.status ?? "").trim().toLowerCase() === "pass") {
|
|
@@ -971,10 +1620,24 @@ async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, de
|
|
|
971
1620
|
}
|
|
972
1621
|
return artifacts;
|
|
973
1622
|
}
|
|
974
|
-
function
|
|
975
|
-
const env =
|
|
1623
|
+
function providerProcessEnv(dispatch) {
|
|
1624
|
+
const env = {
|
|
1625
|
+
...completionRedactionEnv(exactRedactionSecrets(dispatch)),
|
|
1626
|
+
...providerCredentialEnv(dispatch)
|
|
1627
|
+
};
|
|
976
1628
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
977
1629
|
}
|
|
1630
|
+
function providerCredentialEnv(dispatch) {
|
|
1631
|
+
const secret = dispatch.apiKeySecretGrant?.secret;
|
|
1632
|
+
if (!secret || !dispatch.execution.requiresApiKeySecretGrant) {
|
|
1633
|
+
return {};
|
|
1634
|
+
}
|
|
1635
|
+
const provider = dispatch.execution.provider.trim().toLowerCase();
|
|
1636
|
+
if (provider === "codex" || provider === "openai") {
|
|
1637
|
+
return { OPENAI_API_KEY: secret };
|
|
1638
|
+
}
|
|
1639
|
+
return {};
|
|
1640
|
+
}
|
|
978
1641
|
function exactRedactionSecrets(dispatch) {
|
|
979
1642
|
return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
|
|
980
1643
|
}
|
|
@@ -997,7 +1660,7 @@ export class StreamingProviderProcessRunner {
|
|
|
997
1660
|
return await new Promise((resolve, reject) => {
|
|
998
1661
|
const child = spawn(launch.command, launch.args, {
|
|
999
1662
|
cwd: request.cwd,
|
|
1000
|
-
env: request.env
|
|
1663
|
+
env: buildProviderChildEnvironment(request.env),
|
|
1001
1664
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1002
1665
|
windowsHide: true
|
|
1003
1666
|
});
|
|
@@ -1132,9 +1795,6 @@ function quoteWindowsCommandLineArg(value) {
|
|
|
1132
1795
|
}
|
|
1133
1796
|
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
1134
1797
|
}
|
|
1135
|
-
async function fileExists(inputPath) {
|
|
1136
|
-
return fs.stat(inputPath).then((stat) => stat.isFile()).catch(() => false);
|
|
1137
|
-
}
|
|
1138
1798
|
async function readProcessLines(stream, onLine) {
|
|
1139
1799
|
const reader = readline.createInterface({ input: stream });
|
|
1140
1800
|
try {
|
|
@@ -1152,12 +1812,69 @@ function isFakeWorkerModel(model) {
|
|
|
1152
1812
|
function sha256(content) {
|
|
1153
1813
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
1154
1814
|
}
|
|
1815
|
+
function buildDraftPrompt(dispatch) {
|
|
1816
|
+
const draft = dispatch.draft;
|
|
1817
|
+
if (!draft) {
|
|
1818
|
+
throw new Error("Draft prompt is missing.");
|
|
1819
|
+
}
|
|
1820
|
+
return `${draft.systemPrompt}
|
|
1821
|
+
|
|
1822
|
+
Draft kind: ${draft.kind}
|
|
1823
|
+
|
|
1824
|
+
${draft.userPrompt}
|
|
1825
|
+
|
|
1826
|
+
Return ONLY the requested JSON object. Do not wrap it in markdown and do not modify files.`;
|
|
1827
|
+
}
|
|
1828
|
+
function extractJsonObject(content) {
|
|
1829
|
+
let trimmed = content.trim();
|
|
1830
|
+
if (trimmed.startsWith("```")) {
|
|
1831
|
+
const firstNewline = trimmed.indexOf("\n");
|
|
1832
|
+
const lastFence = trimmed.lastIndexOf("```");
|
|
1833
|
+
if (firstNewline >= 0 && lastFence > firstNewline) {
|
|
1834
|
+
trimmed = trimmed.slice(firstNewline + 1, lastFence).trim();
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const start = trimmed.indexOf("{");
|
|
1838
|
+
const end = trimmed.lastIndexOf("}");
|
|
1839
|
+
return start >= 0 && end >= start ? trimmed.slice(start, end + 1) : trimmed;
|
|
1840
|
+
}
|
|
1841
|
+
function summarizeStderr(stderr) {
|
|
1842
|
+
const trimmed = stderr.trim();
|
|
1843
|
+
return trimmed ? `: ${clip(trimmed, 1000)}` : "";
|
|
1844
|
+
}
|
|
1845
|
+
function deterministicDraftPayload(dispatch) {
|
|
1846
|
+
const kind = dispatch.draft?.kind ?? "work_item";
|
|
1847
|
+
if (kind === "work_order") {
|
|
1848
|
+
return {
|
|
1849
|
+
title: "Deterministic runner Work Order draft",
|
|
1850
|
+
summary: "Forge Runner generated this deterministic Work Order draft.",
|
|
1851
|
+
objective: "Validate runner-backed Draft generation.",
|
|
1852
|
+
planningPacket: "Runner-backed draft generation returned a Work Order JSON payload.",
|
|
1853
|
+
decompositionContext: "Use this deterministic fixture only in bridge tests.",
|
|
1854
|
+
assumptions: ["Forge Runner dispatch is available."],
|
|
1855
|
+
warnings: []
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
if (kind === "story") {
|
|
1859
|
+
return {
|
|
1860
|
+
title: "Deterministic runner Story draft",
|
|
1861
|
+
description: "Forge Runner generated this deterministic Story draft.",
|
|
1862
|
+
acceptanceCriteria: "Given runner-backed Draft generation, when Forge requests a Story draft, then JSON is returned.",
|
|
1863
|
+
points: 3
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
return {
|
|
1867
|
+
title: "Deterministic runner Work Item draft",
|
|
1868
|
+
description: "Forge Runner generated this deterministic Work Item draft.",
|
|
1869
|
+
acceptanceCriteria: "Given runner-backed Draft generation, when Forge requests a draft, then JSON is returned.",
|
|
1870
|
+
reproSteps: "Run Draft with a Forge Runner provider selected.",
|
|
1871
|
+
systemInfo: "forge-bridge deterministic provider",
|
|
1872
|
+
points: 1
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1155
1875
|
export function formatProviderCliCommand(command) {
|
|
1156
1876
|
return [command.command, ...command.args].map(singleLine).join(" ");
|
|
1157
1877
|
}
|
|
1158
|
-
function yamlQuotedScalar(value) {
|
|
1159
|
-
return JSON.stringify(singleLine(value));
|
|
1160
|
-
}
|
|
1161
1878
|
function singleLine(value) {
|
|
1162
1879
|
return value.replace(/\s+/g, " ").trim();
|
|
1163
1880
|
}
|
|
@@ -1166,16 +1883,19 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
1166
1883
|
const normalizedProvider = provider.toLowerCase();
|
|
1167
1884
|
const model = dispatch.execution.model.trim();
|
|
1168
1885
|
if (normalizedProvider === "codex") {
|
|
1169
|
-
const args = ["exec"];
|
|
1886
|
+
const args = run ? ["-s", "workspace-write", "-a", "never", "exec"] : ["exec"];
|
|
1170
1887
|
if (run?.completionTools) {
|
|
1171
1888
|
args.push(...codexMcpConfigArgs(run.completionTools));
|
|
1172
1889
|
}
|
|
1173
1890
|
args.push("--model", model);
|
|
1174
1891
|
if (run) {
|
|
1175
|
-
args.push("--json"
|
|
1892
|
+
args.push("--json");
|
|
1176
1893
|
if (run.worktreePath) {
|
|
1177
1894
|
args.push("--cd", run.worktreePath);
|
|
1178
1895
|
}
|
|
1896
|
+
if (run.skipGitRepoCheck) {
|
|
1897
|
+
args.push("--skip-git-repo-check");
|
|
1898
|
+
}
|
|
1179
1899
|
args.push(run.prompt ?? "");
|
|
1180
1900
|
}
|
|
1181
1901
|
return { command: "codex", args };
|
|
@@ -1196,6 +1916,31 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
1196
1916
|
}
|
|
1197
1917
|
return { command: provider, args };
|
|
1198
1918
|
}
|
|
1919
|
+
function buildArchitectPrompt(dispatch) {
|
|
1920
|
+
return `You are Forge's Architect lane running locally in a prepared git worktree.
|
|
1921
|
+
|
|
1922
|
+
Work item: ${dispatch.workItem.id} — ${dispatch.workItem.title}
|
|
1923
|
+
|
|
1924
|
+
Plan only. Do not implement source changes, do not create Develop phase artifacts, and do not mark the spec approved.
|
|
1925
|
+
Read injected Forge context under .forge plus the repository shape, then produce a repo-grounded implementation DAG with task tests, phase gates, final QA coverage, and a pending-approval spec.
|
|
1926
|
+
${architectRevisionInstruction(dispatch)}
|
|
1927
|
+
Forge owns protocol files: write Architect artifacts only through the completion MCP tool.
|
|
1928
|
+
|
|
1929
|
+
${completionPromptForPhase("architect")}
|
|
1930
|
+
|
|
1931
|
+
Finish only after calling forge.complete_architect_plan with implementation_plan, qa_plan, status, summary, and report/report_markdown.`;
|
|
1932
|
+
}
|
|
1933
|
+
function architectRevisionInstruction(dispatch) {
|
|
1934
|
+
if (!dispatch.architectRevision) {
|
|
1935
|
+
return "";
|
|
1936
|
+
}
|
|
1937
|
+
return `
|
|
1938
|
+
Architect revision context:
|
|
1939
|
+
- Previous plan id: ${dispatch.architectRevision.priorPlanId}
|
|
1940
|
+
- Previous content hash: ${dispatch.architectRevision.priorContentHash}
|
|
1941
|
+
- Reviewer feedback: ${dispatch.architectRevision.feedback}
|
|
1942
|
+
Revise the prior plan in response to the feedback. Preserve accepted scope and only change what the feedback requires.`;
|
|
1943
|
+
}
|
|
1199
1944
|
function buildImplementationPrompt(dispatch, phase = completionPhaseFromDispatch(dispatch)) {
|
|
1200
1945
|
return `You are Forge's ${phase.replace(/_/g, " ")} phase runner running locally in a prepared git worktree.
|
|
1201
1946
|
|
|
@@ -1224,6 +1969,20 @@ ${completionPromptForDevelopCycle(phase)}
|
|
|
1224
1969
|
|
|
1225
1970
|
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
1971
|
}
|
|
1972
|
+
function buildDebugCyclePrompt(dispatch, phase = completionPhaseFromDispatch(dispatch)) {
|
|
1973
|
+
return `You are Forge's Debug lane running locally in a prepared git worktree for a bug work item.
|
|
1974
|
+
|
|
1975
|
+
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.
|
|
1976
|
+
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.
|
|
1977
|
+
${debugQaReworkInstruction(dispatch)}
|
|
1978
|
+
Use the repository's existing conventions. Run focused tests when practical, especially the original repro and required regressions.
|
|
1979
|
+
Forge owns protocol files: worktree prep, injected .forge commands/hooks/skills, Debug verdict artifacts, reports, and commits.
|
|
1980
|
+
Read Forge-provided context under .forge when needed, but do not edit .forge files or create Forge Debug artifacts by hand.
|
|
1981
|
+
|
|
1982
|
+
${completionPromptForDebugCycle(isDebugCompletionPhaseForProvider(phase) ? phase : "debug_repro")}
|
|
1983
|
+
|
|
1984
|
+
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.`;
|
|
1985
|
+
}
|
|
1227
1986
|
function qaReworkImplementationInstruction(dispatch) {
|
|
1228
1987
|
if (!dispatch.qaRework) {
|
|
1229
1988
|
return "";
|
|
@@ -1233,7 +1992,20 @@ QA rework context:
|
|
|
1233
1992
|
- Attempt ${dispatch.qaRework.attempt}/${dispatch.qaRework.maxAttempts}
|
|
1234
1993
|
- Rework packet: ${dispatch.qaRework.packetPath}
|
|
1235
1994
|
- Required QA finding IDs: ${dispatch.qaRework.findingIds.join(", ")}
|
|
1236
|
-
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
|
|
1995
|
+
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.
|
|
1996
|
+
Use only these resolved_findings status values: resolved, disputed, or not_reproduced. Use resolved when the finding was fixed; do not use fixed.`;
|
|
1997
|
+
}
|
|
1998
|
+
function debugQaReworkInstruction(dispatch) {
|
|
1999
|
+
if (!dispatch.qaRework) {
|
|
2000
|
+
return "";
|
|
2001
|
+
}
|
|
2002
|
+
return `
|
|
2003
|
+
QA rework context for Debug:
|
|
2004
|
+
- Attempt ${dispatch.qaRework.attempt}/${dispatch.qaRework.maxAttempts}
|
|
2005
|
+
- Rework packet: ${dispatch.qaRework.packetPath}
|
|
2006
|
+
- Required QA finding IDs: ${dispatch.qaRework.findingIds.join(", ")}
|
|
2007
|
+
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.
|
|
2008
|
+
Use only these resolved_findings status values: resolved, disputed, or not_reproduced. Use resolved when the finding was fixed; do not use fixed.`;
|
|
1237
2009
|
}
|
|
1238
2010
|
function qaReworkRetestInstruction(dispatch) {
|
|
1239
2011
|
if (!dispatch.qaRework) {
|
|
@@ -1270,9 +2042,49 @@ function qaBrowserInstruction(dispatch) {
|
|
|
1270
2042
|
const routes = launchTarget.routes.length > 0 ? launchTarget.routes.join(", ") : "not specified";
|
|
1271
2043
|
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
2044
|
}
|
|
1273
|
-
function
|
|
2045
|
+
function storyQaInstruction(dispatch) {
|
|
2046
|
+
const storyQa = dispatch.storyQa;
|
|
2047
|
+
if (!storyQa) {
|
|
2048
|
+
return "";
|
|
2049
|
+
}
|
|
2050
|
+
const childLines = storyQa.childTasks.map((child) => {
|
|
2051
|
+
const dependencies = child.dependencyWorkItemIds.length > 0
|
|
2052
|
+
? child.dependencyWorkItemIds.join(", ")
|
|
2053
|
+
: "none";
|
|
2054
|
+
const validation = child.validationArtifact
|
|
2055
|
+
? `${child.validationArtifact.status} (${child.validationArtifact.path || "artifact path unavailable"})`
|
|
2056
|
+
: "missing";
|
|
2057
|
+
const childQa = child.qaArtifact
|
|
2058
|
+
? `${child.qaArtifact.status} (${child.qaArtifact.path || "artifact path unavailable"})`
|
|
2059
|
+
: "not present";
|
|
2060
|
+
return `- ${child.title} (${child.workItemId}, architect ${child.architectTaskId}, phase ${child.phaseId}/${child.phaseOrder}); deps: ${dependencies}; validation: ${validation}; child QA: ${childQa}`;
|
|
2061
|
+
}).join("\n");
|
|
2062
|
+
const finalQa = storyQa.finalQaChecks.length > 0
|
|
2063
|
+
? storyQa.finalQaChecks.map((check) => `- ${check}`).join("\n")
|
|
2064
|
+
: "- No final QA checks were provided in qa.plan.final_qa.";
|
|
2065
|
+
return `Story QA integration context:
|
|
2066
|
+
- Story work item: ${storyQa.storyWorkItemId}
|
|
2067
|
+
- Plan: ${storyQa.planId} / ${storyQa.planContentHash}
|
|
2068
|
+
- Story worktree: ${storyQa.worktreeKey}
|
|
2069
|
+
- Branch: ${storyQa.branch}
|
|
2070
|
+
- Architect implementation.plan.json provided: ${storyQa.implementationPlanJson ? "yes" : "no"}
|
|
2071
|
+
- Architect qa.plan.json provided: ${storyQa.qaPlanJson ? "yes" : "no"}
|
|
2072
|
+
Materialized child tasks and evidence:
|
|
2073
|
+
${childLines || "- No child tasks were provided."}
|
|
2074
|
+
Architect final QA checks:
|
|
2075
|
+
${finalQa}
|
|
2076
|
+
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.`;
|
|
2077
|
+
}
|
|
2078
|
+
function buildQaPrompt(dispatch, laneSpec) {
|
|
1274
2079
|
return `You are Forge's QA lane running locally in a prepared git worktree.
|
|
1275
2080
|
|
|
2081
|
+
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.
|
|
2082
|
+
|
|
2083
|
+
Injected QA lane command and work item spec:
|
|
2084
|
+
${laneSpec}
|
|
2085
|
+
|
|
2086
|
+
---
|
|
2087
|
+
|
|
1276
2088
|
Work item: ${dispatch.workItem.id}
|
|
1277
2089
|
Title: ${dispatch.workItem.title}
|
|
1278
2090
|
Description:
|
|
@@ -1287,6 +2099,7 @@ ${dispatch.workItem.reproSteps || "Not provided."}
|
|
|
1287
2099
|
System info / environment notes:
|
|
1288
2100
|
${dispatch.workItem.systemInfo || "Not provided."}
|
|
1289
2101
|
|
|
2102
|
+
${storyQaInstruction(dispatch)}
|
|
1290
2103
|
Inspect the validated implementation and Forge artifacts under .forge/work-items/${dispatch.workItem.id}/artifacts/ when present, especially implementation, self-review, and validation evidence.
|
|
1291
2104
|
${qaBrowserInstruction(dispatch)}
|
|
1292
2105
|
${qaReworkRetestInstruction(dispatch)}
|
|
@@ -1295,9 +2108,9 @@ Record concrete evidence. For UI/browser work, include browser_checks with URLs/
|
|
|
1295
2108
|
Report in-scope defects as actionable findings with repro steps, expected/actual behavior, evidence, likely area, required fix, and required regression.
|
|
1296
2109
|
Out-of-scope or follow-up issues belong in candidate_bugs instead of blocking the current item.
|
|
1297
2110
|
|
|
1298
|
-
${
|
|
2111
|
+
${completionPromptForQaCycle()}
|
|
1299
2112
|
|
|
1300
|
-
Finish with a concise summary only after forge.
|
|
2113
|
+
Finish with a concise summary only after forge.complete_qa_verdict accepts the QA bundle.`;
|
|
1301
2114
|
}
|
|
1302
2115
|
function buildConflictResolvePrompt(dispatch, branchName) {
|
|
1303
2116
|
const targetBranch = dispatch.cycleRun.requestedBase || dispatch.repo.defaultBranch || "main";
|
|
@@ -1312,6 +2125,34 @@ Keep the original work item intent intact:
|
|
|
1312
2125
|
|
|
1313
2126
|
Finish with a concise summary of the conflicts resolved and verification performed.`;
|
|
1314
2127
|
}
|
|
2128
|
+
function buildPullRequestCommentsPrompt(dispatch, branchName) {
|
|
2129
|
+
const pr = dispatch.pullRequest;
|
|
2130
|
+
const targetBranch = pr?.targetBranch || dispatch.cycleRun.requestedBase || dispatch.repo.defaultBranch || "main";
|
|
2131
|
+
const prLocator = pr
|
|
2132
|
+
? `PR #${pr.number}${pr.url ? ` (${pr.url})` : ""}`
|
|
2133
|
+
: "the open pull request for this work item";
|
|
2134
|
+
return `You are Forge's Pull Request Comments lane running locally in a prepared git worktree.
|
|
2135
|
+
|
|
2136
|
+
The current branch is the pull request branch ${branchName}. Address unresolved review comments on ${prLocator}.
|
|
2137
|
+
PR context:
|
|
2138
|
+
- Provider: ${pr?.provider || "not provided"}
|
|
2139
|
+
- Source branch: ${pr?.sourceBranch || branchName}
|
|
2140
|
+
- Target branch: ${targetBranch}
|
|
2141
|
+
- Unresolved comments: ${pr?.unresolvedThreads ?? "unknown"}
|
|
2142
|
+
|
|
2143
|
+
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.
|
|
2144
|
+
After the push succeeds, Forge will resolve the addressed review threads through the pull request API.
|
|
2145
|
+
If the review thread contents are unavailable, stop with a clear summary of what could not be accessed instead of guessing.
|
|
2146
|
+
Keep the original work item intent intact:
|
|
2147
|
+
- Work item: ${dispatch.workItem.id}
|
|
2148
|
+
- Title: ${dispatch.workItem.title}
|
|
2149
|
+
- Acceptance criteria: ${dispatch.workItem.acceptanceCriteria || "Not specified"}
|
|
2150
|
+
|
|
2151
|
+
Finish with a concise summary of comments addressed and verification performed.`;
|
|
2152
|
+
}
|
|
2153
|
+
function isPullRequestCommentsDispatch(dispatch) {
|
|
2154
|
+
return dispatch.cycleRun.kind.trim().toLowerCase() === "pr_comments";
|
|
2155
|
+
}
|
|
1315
2156
|
async function cleanupCompletionTools(completionTools, emit) {
|
|
1316
2157
|
try {
|
|
1317
2158
|
await completionTools.cleanup();
|
|
@@ -1369,31 +2210,31 @@ class CursorStreamJsonState {
|
|
|
1369
2210
|
consume(line) {
|
|
1370
2211
|
const event = parseJsonLine(line);
|
|
1371
2212
|
if (!event) {
|
|
1372
|
-
return
|
|
2213
|
+
return [];
|
|
1373
2214
|
}
|
|
1374
2215
|
const type = stringField(event, "type");
|
|
1375
2216
|
if (type === "system" && stringField(event, "subtype") === "init") {
|
|
1376
2217
|
this.sessionId = stringField(event, "session_id");
|
|
1377
2218
|
if (this.sessionId && !this.emittedSession) {
|
|
1378
2219
|
this.emittedSession = true;
|
|
1379
|
-
return {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
2220
|
+
return [{
|
|
2221
|
+
role: "runner",
|
|
2222
|
+
kind: "checkpoint",
|
|
2223
|
+
content: `Cursor session started: ${this.sessionId}.`,
|
|
2224
|
+
eventType: "provider_thread_started"
|
|
2225
|
+
}];
|
|
1385
2226
|
}
|
|
1386
2227
|
}
|
|
1387
2228
|
if (type === "assistant") {
|
|
1388
2229
|
const text = assistantMessageText(event);
|
|
1389
2230
|
if (text) {
|
|
1390
2231
|
this.finalMessage = clip(text, 4000);
|
|
1391
|
-
return {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
2232
|
+
return [{
|
|
2233
|
+
role: "assistant",
|
|
2234
|
+
kind: "message",
|
|
2235
|
+
content: this.finalMessage,
|
|
2236
|
+
eventType: "provider_message"
|
|
2237
|
+
}];
|
|
1397
2238
|
}
|
|
1398
2239
|
}
|
|
1399
2240
|
if (type === "tool_call") {
|
|
@@ -1402,23 +2243,23 @@ class CursorStreamJsonState {
|
|
|
1402
2243
|
if (subtype === "started") {
|
|
1403
2244
|
const summary = formatCursorToolCallStarted(toolCall);
|
|
1404
2245
|
if (summary) {
|
|
1405
|
-
return {
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
2246
|
+
return [{
|
|
2247
|
+
role: "worker",
|
|
2248
|
+
kind: "checkpoint",
|
|
2249
|
+
content: summary,
|
|
2250
|
+
eventType: "provider_tool_call"
|
|
2251
|
+
}];
|
|
1411
2252
|
}
|
|
1412
2253
|
}
|
|
1413
2254
|
if (subtype === "completed") {
|
|
1414
2255
|
const completed = formatCursorToolCallCompleted(toolCall);
|
|
1415
2256
|
if (completed) {
|
|
1416
|
-
return {
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
2257
|
+
return [{
|
|
2258
|
+
role: "worker",
|
|
2259
|
+
kind: completed.kind,
|
|
2260
|
+
content: completed.content,
|
|
2261
|
+
eventType: completed.kind === "file_edit" ? "provider_file_edit" : "provider_tool_call"
|
|
2262
|
+
}];
|
|
1422
2263
|
}
|
|
1423
2264
|
}
|
|
1424
2265
|
}
|
|
@@ -1430,14 +2271,14 @@ class CursorStreamJsonState {
|
|
|
1430
2271
|
this.accumulateUsage(event);
|
|
1431
2272
|
}
|
|
1432
2273
|
if (type === "error" || (type === "result" && event.is_error === true)) {
|
|
1433
|
-
return {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
2274
|
+
return [{
|
|
2275
|
+
role: "runner",
|
|
2276
|
+
kind: "error",
|
|
2277
|
+
content: clip(line.trim(), 1000),
|
|
2278
|
+
eventType: "provider_cli_error"
|
|
2279
|
+
}];
|
|
1439
2280
|
}
|
|
1440
|
-
return
|
|
2281
|
+
return [];
|
|
1441
2282
|
}
|
|
1442
2283
|
accumulateUsage(event) {
|
|
1443
2284
|
const usage = objectField(event, "usage");
|
|
@@ -1568,37 +2409,37 @@ class CodexJsonState {
|
|
|
1568
2409
|
consume(line) {
|
|
1569
2410
|
const event = parseJsonLine(line);
|
|
1570
2411
|
if (!event) {
|
|
1571
|
-
return
|
|
2412
|
+
return [];
|
|
1572
2413
|
}
|
|
1573
2414
|
const type = stringField(event, "type") ?? "";
|
|
1574
2415
|
if (type === "thread.started") {
|
|
1575
2416
|
this.threadId = stringField(event, "thread_id");
|
|
1576
2417
|
if (this.threadId && !this.emittedThread) {
|
|
1577
2418
|
this.emittedThread = true;
|
|
1578
|
-
return {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2419
|
+
return [{
|
|
2420
|
+
role: "runner",
|
|
2421
|
+
kind: "checkpoint",
|
|
2422
|
+
content: `Codex thread started: ${this.threadId}.`,
|
|
2423
|
+
eventType: "provider_thread_started"
|
|
2424
|
+
}];
|
|
1584
2425
|
}
|
|
1585
2426
|
}
|
|
1586
2427
|
if (type === "turn.completed") {
|
|
1587
2428
|
this.accumulateUsage(event);
|
|
1588
|
-
return {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2429
|
+
return [{
|
|
2430
|
+
role: "runner",
|
|
2431
|
+
kind: "checkpoint",
|
|
2432
|
+
content: "Codex turn completed.",
|
|
2433
|
+
eventType: "provider_turn_completed"
|
|
2434
|
+
}];
|
|
1594
2435
|
}
|
|
1595
2436
|
if (type === "turn.failed" || type === "error") {
|
|
1596
|
-
return {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
2437
|
+
return [{
|
|
2438
|
+
role: "runner",
|
|
2439
|
+
kind: "error",
|
|
2440
|
+
content: clip(line.trim(), 1000),
|
|
2441
|
+
eventType: "provider_cli_error"
|
|
2442
|
+
}];
|
|
1602
2443
|
}
|
|
1603
2444
|
if (type === "item.completed" || type === "item.updated") {
|
|
1604
2445
|
const item = objectField(event, "item");
|
|
@@ -1607,28 +2448,32 @@ class CodexJsonState {
|
|
|
1607
2448
|
const text = stringField(item, "text");
|
|
1608
2449
|
if (text) {
|
|
1609
2450
|
this.finalMessage = clip(text, 4000);
|
|
1610
|
-
return {
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
2451
|
+
return [{
|
|
2452
|
+
role: "assistant",
|
|
2453
|
+
kind: "message",
|
|
2454
|
+
content: this.finalMessage,
|
|
2455
|
+
eventType: "provider_message"
|
|
2456
|
+
}];
|
|
1616
2457
|
}
|
|
1617
2458
|
}
|
|
1618
2459
|
if (itemType === "command_execution") {
|
|
1619
2460
|
const command = stringField(item, "command");
|
|
1620
2461
|
const status = stringField(item, "status");
|
|
1621
2462
|
if (command) {
|
|
1622
|
-
return {
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2463
|
+
return [{
|
|
2464
|
+
role: "worker",
|
|
2465
|
+
kind: "checkpoint",
|
|
2466
|
+
content: `Codex command ${status || "completed"}: ${clip(command, 500)}`,
|
|
2467
|
+
eventType: "provider_command"
|
|
2468
|
+
}];
|
|
1628
2469
|
}
|
|
1629
2470
|
}
|
|
1630
2471
|
}
|
|
1631
|
-
|
|
2472
|
+
const mcpEvents = formatCodexMcpToolCallEvents(type, event);
|
|
2473
|
+
if (mcpEvents.length > 0) {
|
|
2474
|
+
return mcpEvents;
|
|
2475
|
+
}
|
|
2476
|
+
return [];
|
|
1632
2477
|
}
|
|
1633
2478
|
accumulateUsage(event) {
|
|
1634
2479
|
const usage = objectField(event, "usage");
|
|
@@ -1644,6 +2489,113 @@ class CodexJsonState {
|
|
|
1644
2489
|
});
|
|
1645
2490
|
}
|
|
1646
2491
|
}
|
|
2492
|
+
function formatCodexMcpToolCallEvents(type, event) {
|
|
2493
|
+
if (type !== "item.started" && type !== "item.updated" && type !== "item.completed") {
|
|
2494
|
+
return [];
|
|
2495
|
+
}
|
|
2496
|
+
const item = objectField(event, "item");
|
|
2497
|
+
const itemType = stringField(item, "type") ?? "";
|
|
2498
|
+
const serverName = mcpServerName(item);
|
|
2499
|
+
const toolName = mcpToolName(item);
|
|
2500
|
+
const isMcpItem = itemType.toLowerCase().includes("mcp")
|
|
2501
|
+
|| Boolean(serverName)
|
|
2502
|
+
|| (toolName ? isForgeCompletionTool(toolName) || toolName.toLowerCase().includes("mcp") : false);
|
|
2503
|
+
if (!isMcpItem || !toolName) {
|
|
2504
|
+
return [];
|
|
2505
|
+
}
|
|
2506
|
+
const status = normalizedString(stringField(item, "status")
|
|
2507
|
+
?? stringField(event, "status")
|
|
2508
|
+
?? (type === "item.started" ? "started" : type === "item.completed" ? "completed" : "updated"));
|
|
2509
|
+
const failed = status === "failed" || status === "error" || status === "cancelled" || Boolean(item.error);
|
|
2510
|
+
const completed = failed || type === "item.completed" || status === "completed" || status === "succeeded" || status === "success";
|
|
2511
|
+
const started = type === "item.started" || status === "started" || status === "running" || status === "in_progress";
|
|
2512
|
+
const data = safeProviderMcpData(item, serverName ?? "unknown", toolName, status);
|
|
2513
|
+
const label = `${serverName ?? "unknown"}.${toolName}`;
|
|
2514
|
+
if (failed) {
|
|
2515
|
+
const summary = mcpErrorSummary(item) ?? "MCP tool call failed.";
|
|
2516
|
+
const events = [{
|
|
2517
|
+
role: "tool",
|
|
2518
|
+
kind: "error",
|
|
2519
|
+
content: `MCP tool call failed: ${label} - ${summary}`,
|
|
2520
|
+
eventType: "provider_mcp_call_failed",
|
|
2521
|
+
data
|
|
2522
|
+
}];
|
|
2523
|
+
if (isForgeCompletionTool(toolName)) {
|
|
2524
|
+
events.push({
|
|
2525
|
+
role: "tool",
|
|
2526
|
+
kind: "error",
|
|
2527
|
+
content: `Forge completion tool validation failed: ${label} - ${summary}`,
|
|
2528
|
+
eventType: "completion_tool_validation_failed",
|
|
2529
|
+
data
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
return events;
|
|
2533
|
+
}
|
|
2534
|
+
if (completed) {
|
|
2535
|
+
return [{
|
|
2536
|
+
role: "tool",
|
|
2537
|
+
kind: "checkpoint",
|
|
2538
|
+
content: `MCP tool call completed: ${label}.`,
|
|
2539
|
+
eventType: "provider_mcp_call_completed",
|
|
2540
|
+
data
|
|
2541
|
+
}];
|
|
2542
|
+
}
|
|
2543
|
+
if (started) {
|
|
2544
|
+
return [{
|
|
2545
|
+
role: "tool",
|
|
2546
|
+
kind: "checkpoint",
|
|
2547
|
+
content: `MCP tool call started: ${label}.`,
|
|
2548
|
+
eventType: "provider_mcp_call_started",
|
|
2549
|
+
data
|
|
2550
|
+
}];
|
|
2551
|
+
}
|
|
2552
|
+
return [];
|
|
2553
|
+
}
|
|
2554
|
+
function mcpServerName(item) {
|
|
2555
|
+
return stringField(item, "server")
|
|
2556
|
+
?? stringField(item, "server_name")
|
|
2557
|
+
?? stringField(item, "mcp_server")
|
|
2558
|
+
?? stringField(objectField(item, "mcp"), "server")
|
|
2559
|
+
?? stringField(objectField(item, "mcp"), "server_name");
|
|
2560
|
+
}
|
|
2561
|
+
function mcpToolName(item) {
|
|
2562
|
+
return stringField(item, "tool")
|
|
2563
|
+
?? stringField(item, "tool_name")
|
|
2564
|
+
?? stringField(item, "name")
|
|
2565
|
+
?? stringField(objectField(item, "function"), "name")
|
|
2566
|
+
?? stringField(objectField(item, "mcp"), "tool")
|
|
2567
|
+
?? stringField(objectField(item, "mcp"), "tool_name");
|
|
2568
|
+
}
|
|
2569
|
+
function mcpErrorSummary(item) {
|
|
2570
|
+
const direct = stringField(item, "error")
|
|
2571
|
+
?? stringField(item, "error_message")
|
|
2572
|
+
?? stringField(item, "message");
|
|
2573
|
+
if (direct) {
|
|
2574
|
+
return clip(direct, 600);
|
|
2575
|
+
}
|
|
2576
|
+
const error = objectField(item, "error");
|
|
2577
|
+
return stringField(error, "message")
|
|
2578
|
+
?? stringField(error, "summary")
|
|
2579
|
+
?? stringField(objectField(item, "result"), "error")
|
|
2580
|
+
?? null;
|
|
2581
|
+
}
|
|
2582
|
+
function safeProviderMcpData(item, serverName, toolName, status) {
|
|
2583
|
+
const elapsedMs = numericField(item, "elapsed_ms") ?? numericField(item, "duration_ms");
|
|
2584
|
+
return JSON.stringify(stripUndefined({
|
|
2585
|
+
server_name: serverName,
|
|
2586
|
+
tool_name: toolName,
|
|
2587
|
+
status,
|
|
2588
|
+
elapsed_ms: elapsedMs,
|
|
2589
|
+
error_summary: mcpErrorSummary(item)
|
|
2590
|
+
}));
|
|
2591
|
+
}
|
|
2592
|
+
function isForgeCompletionTool(toolName) {
|
|
2593
|
+
const normalized = toolName.trim().toLowerCase();
|
|
2594
|
+
return normalized.startsWith("forge.complete_")
|
|
2595
|
+
|| normalized.startsWith("complete_")
|
|
2596
|
+
|| normalized.includes("forge.complete_")
|
|
2597
|
+
|| normalized.includes("complete_architect_plan");
|
|
2598
|
+
}
|
|
1647
2599
|
function objectField(source, key) {
|
|
1648
2600
|
const value = source[key];
|
|
1649
2601
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -1654,6 +2606,13 @@ function stringField(source, key) {
|
|
|
1654
2606
|
const value = source[key];
|
|
1655
2607
|
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
1656
2608
|
}
|
|
2609
|
+
function numericField(source, key) {
|
|
2610
|
+
const value = source[key];
|
|
2611
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2612
|
+
}
|
|
2613
|
+
function stripUndefined(value) {
|
|
2614
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
2615
|
+
}
|
|
1657
2616
|
function normalizedString(value) {
|
|
1658
2617
|
return value.trim().toLowerCase().replace(/-/g, "_");
|
|
1659
2618
|
}
|