@loucompanion/forge-bridge 0.1.1-dev.03b8ba168599 → 0.1.1-dev.32d4dd5874ee
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/cli/cli.service.d.ts +4 -0
- package/dist/cli/cli.service.js +21 -1
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/internals.d.ts +2 -0
- package/dist/session/internals.js +1 -0
- package/dist/session/phase-completion.d.ts +54 -0
- package/dist/session/phase-completion.js +737 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.service.js +306 -126
- package/dist/session/provider-cli/provider-cli.types.d.ts +1 -0
- package/dist/session/session.service.js +3 -1
- package/dist/session/session.types.d.ts +2 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +1 -1
- package/dist/worktree/artifact-injector.js +8 -5
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GitClient } from "../../shared/git/git.js";
|
|
2
|
+
import { type CompletionToolRuntime } from "../phase-completion.js";
|
|
2
3
|
import type { ProviderCliService } from "./provider-cli.base.js";
|
|
3
4
|
import type { ProviderCliRunContext, ProviderCliRunResult } from "./provider-cli.types.js";
|
|
4
5
|
import type { RunOnceDispatchPacket, RunnerDispatchEvent } from "../session.types.js";
|
|
@@ -55,4 +56,5 @@ export declare function formatProviderCliCommand(command: ProviderCliCommand): s
|
|
|
55
56
|
export declare function buildProviderCliCommand(dispatch: RunOnceDispatchPacket, run?: {
|
|
56
57
|
readonly prompt?: string;
|
|
57
58
|
readonly worktreePath?: string;
|
|
59
|
+
readonly completionTools?: CompletionToolRuntime;
|
|
58
60
|
}): ProviderCliCommand;
|
|
@@ -5,7 +5,9 @@ import { execFile, spawn } from "node:child_process";
|
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { GitClient } from "../../shared/git/git.js";
|
|
8
|
+
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
8
9
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
10
|
+
import { codexMcpConfigArgs, completionArtifactJsonPath, completionArtifactReportPath, completionPhaseFromDispatch, completionPromptForPhase, prepareCompletionToolRuntime, readCompletionArtifact } from "../phase-completion.js";
|
|
9
11
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
10
12
|
const execFileAsync = promisify(execFile);
|
|
11
13
|
export class RoutingProviderCliService {
|
|
@@ -40,20 +42,22 @@ export class ProcessBackedProviderCliService {
|
|
|
40
42
|
}
|
|
41
43
|
async runImplementation(context) {
|
|
42
44
|
const { dispatch, worktree } = context;
|
|
45
|
+
const phase = completionPhaseFromDispatch(dispatch);
|
|
43
46
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
44
|
-
|
|
45
|
-
await fs.mkdir(artifactRoot, { recursive: true });
|
|
47
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
46
48
|
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
47
49
|
if (!laneCommand) {
|
|
48
50
|
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
49
51
|
}
|
|
50
|
-
const specBody = buildSpecBody(laneCommand, dispatch.workItem);
|
|
52
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, phase);
|
|
51
53
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
52
54
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
53
55
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
56
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
|
|
54
57
|
const command = buildProviderCliCommand(dispatch, {
|
|
55
58
|
worktreePath: worktree.worktreePath,
|
|
56
|
-
prompt: buildImplementationPrompt(dispatch)
|
|
59
|
+
prompt: buildImplementationPrompt(dispatch, phase),
|
|
60
|
+
completionTools
|
|
57
61
|
});
|
|
58
62
|
await context.emit({
|
|
59
63
|
role: "runner",
|
|
@@ -61,19 +65,31 @@ export class ProcessBackedProviderCliService {
|
|
|
61
65
|
content: `Starting provider CLI: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
|
|
62
66
|
eventType: "provider_cli_started"
|
|
63
67
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
await context.emit({
|
|
69
|
+
role: "runner",
|
|
70
|
+
kind: "checkpoint",
|
|
71
|
+
content: `Forge completion tools configured for ${phase}; provider must call the MCP completion tool before exit.`,
|
|
72
|
+
eventType: "completion_tools_configured"
|
|
73
|
+
});
|
|
74
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await this.processRunner.run({
|
|
78
|
+
command: command.command,
|
|
79
|
+
args: command.args,
|
|
80
|
+
cwd: worktree.worktreePath
|
|
81
|
+
}, {
|
|
82
|
+
onStdoutLine: async (line) => {
|
|
83
|
+
const event = outputState.consume(line);
|
|
84
|
+
if (event) {
|
|
85
|
+
await context.emit(event);
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
88
|
+
}, context.signal);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
92
|
+
}
|
|
77
93
|
if (result.exitCode !== 0) {
|
|
78
94
|
const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
|
|
79
95
|
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
@@ -81,42 +97,19 @@ export class ProcessBackedProviderCliService {
|
|
|
81
97
|
await context.emit({
|
|
82
98
|
role: "runner",
|
|
83
99
|
kind: "checkpoint",
|
|
84
|
-
content: `Provider CLI completed: ${command.command} exit ${result.exitCode}${
|
|
100
|
+
content: `Provider CLI completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
85
101
|
eventType: "provider_cli_completed"
|
|
86
102
|
});
|
|
87
103
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
status: "pass",
|
|
94
|
-
checks: [
|
|
95
|
-
{
|
|
96
|
-
name: "provider_cli_exit",
|
|
97
|
-
status: "pass",
|
|
98
|
-
command: formatProviderCliCommand(command),
|
|
99
|
-
exit_code: result.exitCode
|
|
100
|
-
}
|
|
101
|
-
],
|
|
102
|
-
metrics: {
|
|
103
|
-
files_changed: committed.changedFiles.length
|
|
104
|
-
},
|
|
105
|
-
provider: dispatch.execution.provider,
|
|
106
|
-
model: dispatch.execution.model,
|
|
107
|
-
produced_by: `${dispatch.execution.provider}-cli`,
|
|
108
|
-
thread_id: codexState.threadId,
|
|
109
|
-
commit_sha: committed.commitSha,
|
|
110
|
-
content_hash: sha256(report)
|
|
111
|
-
};
|
|
112
|
-
const verdictJson = JSON.stringify(verdict);
|
|
113
|
-
await fs.writeFile(artifactPath, `${verdictJson}\n`, "utf8");
|
|
114
|
-
await fs.writeFile(reportPath, report, "utf8");
|
|
115
|
-
if (codexState.finalMessage) {
|
|
104
|
+
const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, phase);
|
|
105
|
+
if (!completionArtifact) {
|
|
106
|
+
throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
|
|
107
|
+
}
|
|
108
|
+
if (outputState.finalMessage) {
|
|
116
109
|
await context.emit({
|
|
117
110
|
role: "assistant",
|
|
118
111
|
kind: "message",
|
|
119
|
-
content:
|
|
112
|
+
content: outputState.finalMessage,
|
|
120
113
|
eventType: "implementation_complete"
|
|
121
114
|
});
|
|
122
115
|
}
|
|
@@ -129,7 +122,7 @@ export class ProcessBackedProviderCliService {
|
|
|
129
122
|
});
|
|
130
123
|
}
|
|
131
124
|
return {
|
|
132
|
-
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName}.`,
|
|
125
|
+
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${phase}, ${committed.commitSha.slice(0, 12)}).`,
|
|
133
126
|
spec: {
|
|
134
127
|
repoPath: worktree.worktreePath,
|
|
135
128
|
path: specPath,
|
|
@@ -137,14 +130,7 @@ export class ProcessBackedProviderCliService {
|
|
|
137
130
|
producedBy: "forge-lane-command",
|
|
138
131
|
status: "approved"
|
|
139
132
|
},
|
|
140
|
-
artifact:
|
|
141
|
-
repoPath: worktree.worktreePath,
|
|
142
|
-
path: artifactPath,
|
|
143
|
-
verdictJson,
|
|
144
|
-
report,
|
|
145
|
-
producedBy: `${dispatch.execution.provider}-cli`,
|
|
146
|
-
status: "pass"
|
|
147
|
-
}
|
|
133
|
+
artifact: completionArtifact
|
|
148
134
|
};
|
|
149
135
|
}
|
|
150
136
|
async runQa(dispatch, emit, signal) {
|
|
@@ -157,14 +143,14 @@ export class ProcessBackedProviderCliService {
|
|
|
157
143
|
content: `Starting provider CLI QA: ${formatProviderCliCommand(command)}`,
|
|
158
144
|
eventType: "provider_cli_started"
|
|
159
145
|
});
|
|
160
|
-
const
|
|
146
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
161
147
|
const result = await this.processRunner.run({
|
|
162
148
|
command: command.command,
|
|
163
149
|
args: command.args,
|
|
164
150
|
cwd: process.cwd()
|
|
165
151
|
}, {
|
|
166
152
|
onStdoutLine: async (line) => {
|
|
167
|
-
const event =
|
|
153
|
+
const event = outputState.consume(line);
|
|
168
154
|
if (event) {
|
|
169
155
|
await emit(event);
|
|
170
156
|
}
|
|
@@ -177,7 +163,7 @@ export class ProcessBackedProviderCliService {
|
|
|
177
163
|
await emit({
|
|
178
164
|
role: "runner",
|
|
179
165
|
kind: "checkpoint",
|
|
180
|
-
content: `Provider CLI QA completed: ${command.command} exit ${result.exitCode}${
|
|
166
|
+
content: `Provider CLI QA completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
181
167
|
eventType: "provider_cli_completed"
|
|
182
168
|
});
|
|
183
169
|
return {
|
|
@@ -196,14 +182,14 @@ export class ProcessBackedProviderCliService {
|
|
|
196
182
|
content: `Starting provider CLI conflict resolve: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
|
|
197
183
|
eventType: "provider_cli_started"
|
|
198
184
|
});
|
|
199
|
-
const
|
|
185
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
200
186
|
const result = await this.processRunner.run({
|
|
201
187
|
command: command.command,
|
|
202
188
|
args: command.args,
|
|
203
189
|
cwd: worktree.worktreePath
|
|
204
190
|
}, {
|
|
205
191
|
onStdoutLine: async (line) => {
|
|
206
|
-
const event =
|
|
192
|
+
const event = outputState.consume(line);
|
|
207
193
|
if (event) {
|
|
208
194
|
await context.emit(event);
|
|
209
195
|
}
|
|
@@ -218,7 +204,7 @@ export class ProcessBackedProviderCliService {
|
|
|
218
204
|
await context.emit({
|
|
219
205
|
role: "assistant",
|
|
220
206
|
kind: "message",
|
|
221
|
-
content:
|
|
207
|
+
content: outputState.finalMessage || `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`,
|
|
222
208
|
eventType: "conflict_resolve_complete"
|
|
223
209
|
});
|
|
224
210
|
return {
|
|
@@ -233,6 +219,7 @@ export class DeterministicProviderCliService {
|
|
|
233
219
|
}
|
|
234
220
|
async runImplementation(context) {
|
|
235
221
|
const { dispatch, worktree } = context;
|
|
222
|
+
const phase = completionPhaseFromDispatch(dispatch);
|
|
236
223
|
const providerCommand = buildProviderCliCommand(dispatch);
|
|
237
224
|
const providerCommandText = formatProviderCliCommand(providerCommand);
|
|
238
225
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
@@ -242,7 +229,7 @@ export class DeterministicProviderCliService {
|
|
|
242
229
|
if (!laneCommand) {
|
|
243
230
|
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
244
231
|
}
|
|
245
|
-
const specBody = `${buildSpecBody(laneCommand, dispatch.workItem)}
|
|
232
|
+
const specBody = `${buildSpecBody(laneCommand, dispatch.workItem, phase)}
|
|
246
233
|
## Execution
|
|
247
234
|
|
|
248
235
|
- Provider: ${singleLine(dispatch.execution.provider)}
|
|
@@ -265,17 +252,31 @@ export class DeterministicProviderCliService {
|
|
|
265
252
|
});
|
|
266
253
|
const verdict = {
|
|
267
254
|
schema_version: 1,
|
|
255
|
+
phase: "implementation",
|
|
268
256
|
status: "pass",
|
|
269
|
-
|
|
270
|
-
|
|
257
|
+
summary: "forge-bridge produced a deterministic implementation changeset.",
|
|
258
|
+
plan: [{ step: "Create deterministic changeset", status: "done" }],
|
|
259
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic bridge evidence." }],
|
|
260
|
+
tests_added_or_updated: [{ name: "forge_bridge_run_once", evidence: "The bridge committed FORGE_PHASE5_CHANGESET.md." }],
|
|
261
|
+
docs_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic evidence document created for the run." }],
|
|
262
|
+
acceptance_criteria: [
|
|
263
|
+
{
|
|
264
|
+
criterion: dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists",
|
|
265
|
+
evidence: `FORGE_PHASE5_CHANGESET.md was committed with content hash ${sha256(changesetBody)}.`,
|
|
266
|
+
status: "satisfied"
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
known_gaps: [],
|
|
270
|
+
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation.",
|
|
271
271
|
produced_by: "forge-bridge",
|
|
272
272
|
content_hash: sha256(changesetBody)
|
|
273
273
|
};
|
|
274
274
|
const verdictJson = JSON.stringify(verdict);
|
|
275
275
|
const report = `# Implementation report\n\nforge-bridge prepared \`${worktree.branchName}\` and created \`${changesetPath}\`.\n`;
|
|
276
|
-
const artifactPath =
|
|
276
|
+
const artifactPath = completionArtifactJsonPath(worktree.worktreePath, dispatch.workItem.id, "implementation");
|
|
277
|
+
const reportPath = completionArtifactReportPath(worktree.worktreePath, dispatch.workItem.id, "implementation");
|
|
277
278
|
await fs.writeFile(artifactPath, verdictJson + "\n", "utf8");
|
|
278
|
-
await fs.writeFile(
|
|
279
|
+
await fs.writeFile(reportPath, report, "utf8");
|
|
279
280
|
await context.emit({
|
|
280
281
|
role: "assistant",
|
|
281
282
|
kind: "message",
|
|
@@ -297,7 +298,8 @@ export class DeterministicProviderCliService {
|
|
|
297
298
|
verdictJson,
|
|
298
299
|
report,
|
|
299
300
|
producedBy: "forge-bridge",
|
|
300
|
-
status: "pass"
|
|
301
|
+
status: "pass",
|
|
302
|
+
phase: "implementation"
|
|
301
303
|
}
|
|
302
304
|
};
|
|
303
305
|
}
|
|
@@ -406,6 +408,10 @@ async function resolveProviderProcessLaunch(command, args) {
|
|
|
406
408
|
if (process.platform !== "win32") {
|
|
407
409
|
return { command, args };
|
|
408
410
|
}
|
|
411
|
+
const cursorDirect = await resolveCursorAgentDirectLaunch(command, args);
|
|
412
|
+
if (cursorDirect) {
|
|
413
|
+
return cursorDirect;
|
|
414
|
+
}
|
|
409
415
|
const resolved = await resolveWindowsCommand(command);
|
|
410
416
|
return resolved ? buildWindowsLaunchCommand(resolved, args) : { command, args };
|
|
411
417
|
}
|
|
@@ -413,10 +419,6 @@ async function resolveWindowsCommand(command) {
|
|
|
413
419
|
if (path.isAbsolute(command)) {
|
|
414
420
|
return command;
|
|
415
421
|
}
|
|
416
|
-
const cursorScript = await resolveCursorAgentPowerShellScript(command);
|
|
417
|
-
if (cursorScript) {
|
|
418
|
-
return cursorScript;
|
|
419
|
-
}
|
|
420
422
|
try {
|
|
421
423
|
const result = await execFileAsync("where.exe", [command], { timeout: 2000 });
|
|
422
424
|
return chooseWindowsCommandCandidate(result.stdout.split(/\r?\n/));
|
|
@@ -425,18 +427,6 @@ async function resolveWindowsCommand(command) {
|
|
|
425
427
|
return null;
|
|
426
428
|
}
|
|
427
429
|
}
|
|
428
|
-
async function resolveCursorAgentPowerShellScript(command) {
|
|
429
|
-
const normalized = command.trim().toLowerCase();
|
|
430
|
-
if (normalized !== "cursor-agent" && normalized !== "agent") {
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
const localAppData = process.env.LOCALAPPDATA;
|
|
434
|
-
if (!localAppData) {
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
const script = path.join(localAppData, "cursor-agent", "cursor-agent.ps1");
|
|
438
|
-
return await fileExists(script) ? script : null;
|
|
439
|
-
}
|
|
440
430
|
export function chooseWindowsCommandCandidate(candidates) {
|
|
441
431
|
const normalized = candidates
|
|
442
432
|
.map((candidate) => candidate.trim())
|
|
@@ -518,7 +508,11 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
518
508
|
const normalizedProvider = provider.toLowerCase();
|
|
519
509
|
const model = dispatch.execution.model.trim();
|
|
520
510
|
if (normalizedProvider === "codex") {
|
|
521
|
-
const args = ["exec"
|
|
511
|
+
const args = ["exec"];
|
|
512
|
+
if (run?.completionTools) {
|
|
513
|
+
args.push(...codexMcpConfigArgs(run.completionTools));
|
|
514
|
+
}
|
|
515
|
+
args.push("--model", model);
|
|
522
516
|
if (run) {
|
|
523
517
|
args.push("--json", "--dangerously-bypass-approvals-and-sandbox");
|
|
524
518
|
if (run.worktreePath) {
|
|
@@ -529,10 +523,12 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
529
523
|
return { command: "codex", args };
|
|
530
524
|
}
|
|
531
525
|
if (normalizedProvider === "cursor") {
|
|
532
|
-
const args = ["--model", model];
|
|
526
|
+
const args = ["--print", "--output-format", "stream-json", "--model", model];
|
|
527
|
+
if (run?.worktreePath) {
|
|
528
|
+
args.push("--workspace", run.worktreePath);
|
|
529
|
+
}
|
|
533
530
|
if (run?.prompt) {
|
|
534
|
-
args.push("--yolo");
|
|
535
|
-
args.push(run.prompt);
|
|
531
|
+
args.push("--yolo", "--approve-mcps", "--trust", run.prompt);
|
|
536
532
|
}
|
|
537
533
|
return { command: "cursor-agent", args };
|
|
538
534
|
}
|
|
@@ -542,14 +538,27 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
542
538
|
}
|
|
543
539
|
return { command: provider, args };
|
|
544
540
|
}
|
|
545
|
-
function buildImplementationPrompt(dispatch) {
|
|
546
|
-
return `You are Forge's
|
|
541
|
+
function buildImplementationPrompt(dispatch, phase = completionPhaseFromDispatch(dispatch)) {
|
|
542
|
+
return `You are Forge's ${phase.replace(/_/g, " ")} phase runner running locally in a prepared git worktree.
|
|
547
543
|
|
|
548
|
-
Read .forge/work-items/${dispatch.workItem.id}/spec.md
|
|
544
|
+
Read .forge/work-items/${dispatch.workItem.id}/spec.md and any prior phase artifacts required by this phase.
|
|
545
|
+
${phaseWorkInstruction(phase)}
|
|
549
546
|
Use the repository's existing conventions. Run relevant tests when practical.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
547
|
+
Forge owns protocol files: worktree prep, injected .forge commands/hooks/skills, phase verdict artifacts, reports, and commits.
|
|
548
|
+
Read Forge-provided context under .forge when needed, but do not edit .forge files or create Forge verdict/report artifacts by hand.
|
|
549
|
+
|
|
550
|
+
${completionPromptForPhase(phase)}
|
|
551
|
+
|
|
552
|
+
Finish with a concise summary of changed files and verification after calling the completion tool.`;
|
|
553
|
+
}
|
|
554
|
+
function phaseWorkInstruction(phase) {
|
|
555
|
+
if (phase === "self_review") {
|
|
556
|
+
return "Review the implementation diff, apply safe fixes when needed, and record remaining risks explicitly.";
|
|
557
|
+
}
|
|
558
|
+
if (phase === "validation") {
|
|
559
|
+
return "Run the validation commands and deterministic drift checks needed to prove the work is ready for Dev Test.";
|
|
560
|
+
}
|
|
561
|
+
return "Implement the work item and keep the change focused.";
|
|
553
562
|
}
|
|
554
563
|
function buildQaPrompt(dispatch) {
|
|
555
564
|
return `You are Forge's QA lane running locally.
|
|
@@ -570,6 +579,19 @@ Keep the original work item intent intact:
|
|
|
570
579
|
|
|
571
580
|
Finish with a concise summary of the conflicts resolved and verification performed.`;
|
|
572
581
|
}
|
|
582
|
+
async function cleanupCompletionTools(completionTools, emit) {
|
|
583
|
+
try {
|
|
584
|
+
await completionTools.cleanup();
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
await emit({
|
|
588
|
+
role: "runner",
|
|
589
|
+
kind: "checkpoint",
|
|
590
|
+
content: `Forge completion tool cleanup failed and was ignored: ${clip(messageFromError(error), 600)}`,
|
|
591
|
+
eventType: "completion_tools_cleanup_failed"
|
|
592
|
+
}).catch(() => undefined);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
573
595
|
async function commitWorktreeChanges(git, worktreePath, workItemId) {
|
|
574
596
|
await git.run(worktreePath, ["add", "-A"]);
|
|
575
597
|
await git.run(worktreePath, ["reset", "--", ".forge"]).catch(() => "");
|
|
@@ -586,26 +608,8 @@ async function commitWorktreeChanges(git, worktreePath, workItemId) {
|
|
|
586
608
|
const commitSha = (await git.run(worktreePath, ["rev-parse", "HEAD"])).trim();
|
|
587
609
|
return { commitSha, changedFiles };
|
|
588
610
|
}
|
|
589
|
-
function
|
|
590
|
-
|
|
591
|
-
? committed.changedFiles.map((file) => `- ${file}`).join("\n")
|
|
592
|
-
: "- No staged repository files changed; Forge recorded an empty completion commit.";
|
|
593
|
-
return `# Implementation report
|
|
594
|
-
|
|
595
|
-
Provider: ${dispatch.execution.provider}
|
|
596
|
-
Model: ${dispatch.execution.model}
|
|
597
|
-
Command: ${formatProviderCliCommand(command)}
|
|
598
|
-
Thread: ${codexState.threadId ?? "n/a"}
|
|
599
|
-
Commit: ${committed.commitSha}
|
|
600
|
-
|
|
601
|
-
## Changed files
|
|
602
|
-
|
|
603
|
-
${files}
|
|
604
|
-
|
|
605
|
-
## Final message
|
|
606
|
-
|
|
607
|
-
${codexState.finalMessage ?? "Provider CLI completed without a final assistant message."}
|
|
608
|
-
`;
|
|
611
|
+
function messageFromError(error) {
|
|
612
|
+
return error instanceof Error ? error.message : String(error);
|
|
609
613
|
}
|
|
610
614
|
function clip(value, maxLength) {
|
|
611
615
|
if (value.length <= maxLength) {
|
|
@@ -613,27 +617,203 @@ function clip(value, maxLength) {
|
|
|
613
617
|
}
|
|
614
618
|
return `${value.slice(0, maxLength)}...`;
|
|
615
619
|
}
|
|
616
|
-
|
|
617
|
-
|
|
620
|
+
function createProviderOutputState(provider) {
|
|
621
|
+
return provider.trim().toLowerCase() === "cursor"
|
|
622
|
+
? new CursorStreamJsonState()
|
|
623
|
+
: new CodexJsonState();
|
|
624
|
+
}
|
|
625
|
+
class CursorStreamJsonState {
|
|
626
|
+
sessionId = null;
|
|
618
627
|
finalMessage = null;
|
|
619
|
-
|
|
628
|
+
emittedSession = false;
|
|
629
|
+
get sessionLabel() {
|
|
630
|
+
return this.sessionId ? `session ${this.sessionId}` : null;
|
|
631
|
+
}
|
|
620
632
|
consume(line) {
|
|
621
|
-
const
|
|
622
|
-
if (!
|
|
633
|
+
const event = parseJsonLine(line);
|
|
634
|
+
if (!event) {
|
|
623
635
|
return null;
|
|
624
636
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
637
|
+
const type = stringField(event, "type");
|
|
638
|
+
if (type === "system" && stringField(event, "subtype") === "init") {
|
|
639
|
+
this.sessionId = stringField(event, "session_id");
|
|
640
|
+
if (this.sessionId && !this.emittedSession) {
|
|
641
|
+
this.emittedSession = true;
|
|
642
|
+
return {
|
|
643
|
+
role: "runner",
|
|
644
|
+
kind: "checkpoint",
|
|
645
|
+
content: `Cursor session started: ${this.sessionId}.`,
|
|
646
|
+
eventType: "provider_thread_started"
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (type === "assistant") {
|
|
651
|
+
const text = assistantMessageText(event);
|
|
652
|
+
if (text) {
|
|
653
|
+
this.finalMessage = clip(text, 4000);
|
|
654
|
+
return {
|
|
655
|
+
role: "assistant",
|
|
656
|
+
kind: "message",
|
|
657
|
+
content: this.finalMessage,
|
|
658
|
+
eventType: "provider_message"
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (type === "tool_call") {
|
|
663
|
+
const subtype = stringField(event, "subtype");
|
|
664
|
+
const toolCall = objectField(event, "tool_call");
|
|
665
|
+
if (subtype === "started") {
|
|
666
|
+
const summary = formatCursorToolCallStarted(toolCall);
|
|
667
|
+
if (summary) {
|
|
668
|
+
return {
|
|
669
|
+
role: "worker",
|
|
670
|
+
kind: "checkpoint",
|
|
671
|
+
content: summary,
|
|
672
|
+
eventType: "provider_tool_call"
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (subtype === "completed") {
|
|
677
|
+
const completed = formatCursorToolCallCompleted(toolCall);
|
|
678
|
+
if (completed) {
|
|
679
|
+
return {
|
|
680
|
+
role: "worker",
|
|
681
|
+
kind: completed.kind,
|
|
682
|
+
content: completed.content,
|
|
683
|
+
eventType: completed.kind === "file_edit" ? "provider_file_edit" : "provider_tool_call"
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (type === "result" && stringField(event, "subtype") === "success") {
|
|
689
|
+
const result = stringField(event, "result");
|
|
690
|
+
if (result) {
|
|
691
|
+
this.finalMessage = clip(result, 4000);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (type === "error" || (type === "result" && event.is_error === true)) {
|
|
695
|
+
return {
|
|
696
|
+
role: "runner",
|
|
697
|
+
kind: "error",
|
|
698
|
+
content: clip(line.trim(), 1000),
|
|
699
|
+
eventType: "provider_cli_error"
|
|
700
|
+
};
|
|
628
701
|
}
|
|
629
|
-
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function formatCursorToolCallStarted(toolCall) {
|
|
706
|
+
const readArgs = objectField(objectField(toolCall, "readToolCall"), "args");
|
|
707
|
+
const readPath = stringField(readArgs, "path");
|
|
708
|
+
if (readPath) {
|
|
709
|
+
return `Reading ${readPath}`;
|
|
710
|
+
}
|
|
711
|
+
const writeArgs = objectField(objectField(toolCall, "writeToolCall"), "args");
|
|
712
|
+
const writePath = stringField(writeArgs, "path");
|
|
713
|
+
if (writePath) {
|
|
714
|
+
return `Writing ${writePath}`;
|
|
715
|
+
}
|
|
716
|
+
const shellArgs = objectField(objectField(toolCall, "shellToolCall"), "args");
|
|
717
|
+
const command = stringField(shellArgs, "command");
|
|
718
|
+
if (command) {
|
|
719
|
+
return `Shell command started: ${clip(command, 500)}`;
|
|
720
|
+
}
|
|
721
|
+
const fn = objectField(toolCall, "function");
|
|
722
|
+
const name = stringField(fn, "name");
|
|
723
|
+
if (name) {
|
|
724
|
+
return `Tool call started: ${name}`;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
function formatCursorToolCallCompleted(toolCall) {
|
|
729
|
+
const writeArgs = objectField(objectField(toolCall, "writeToolCall"), "args");
|
|
730
|
+
const writePath = stringField(writeArgs, "path");
|
|
731
|
+
const writeResult = objectField(objectField(objectField(toolCall, "writeToolCall"), "result"), "success");
|
|
732
|
+
const writtenPath = stringField(writeResult, "path") ?? writePath;
|
|
733
|
+
if (writtenPath) {
|
|
734
|
+
return {
|
|
735
|
+
kind: "file_edit",
|
|
736
|
+
content: `Wrote ${writtenPath}`
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const readArgs = objectField(objectField(toolCall, "readToolCall"), "args");
|
|
740
|
+
const readPath = stringField(readArgs, "path");
|
|
741
|
+
if (readPath) {
|
|
742
|
+
const readResult = objectField(objectField(objectField(toolCall, "readToolCall"), "result"), "success");
|
|
743
|
+
const totalLines = readResult.totalLines;
|
|
744
|
+
const lineSummary = typeof totalLines === "number" ? ` (${totalLines} lines)` : "";
|
|
745
|
+
return {
|
|
746
|
+
kind: "checkpoint",
|
|
747
|
+
content: `Read ${readPath}${lineSummary}`
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const shellArgs = objectField(objectField(toolCall, "shellToolCall"), "args");
|
|
751
|
+
const command = stringField(shellArgs, "command");
|
|
752
|
+
if (command) {
|
|
753
|
+
return {
|
|
754
|
+
kind: "checkpoint",
|
|
755
|
+
content: `Shell command completed: ${clip(command, 500)}`
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const fn = objectField(toolCall, "function");
|
|
759
|
+
const name = stringField(fn, "name");
|
|
760
|
+
if (name) {
|
|
761
|
+
return {
|
|
762
|
+
kind: "checkpoint",
|
|
763
|
+
content: `Tool call completed: ${name}`
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
function assistantMessageText(event) {
|
|
769
|
+
const message = objectField(event, "message");
|
|
770
|
+
const content = message.content;
|
|
771
|
+
if (!Array.isArray(content)) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const parts = content
|
|
775
|
+
.map((entry) => {
|
|
776
|
+
if (!entry || typeof entry !== "object") {
|
|
630
777
|
return null;
|
|
631
778
|
}
|
|
632
|
-
|
|
779
|
+
const block = entry;
|
|
780
|
+
return stringField(block, "text");
|
|
781
|
+
})
|
|
782
|
+
.filter((text) => Boolean(text));
|
|
783
|
+
if (parts.length === 0) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
return parts.join("");
|
|
787
|
+
}
|
|
788
|
+
function parseJsonLine(line) {
|
|
789
|
+
const trimmed = line.trim();
|
|
790
|
+
if (!trimmed.startsWith("{")) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
let parsed;
|
|
794
|
+
try {
|
|
795
|
+
parsed = JSON.parse(trimmed);
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
801
|
+
? parsed
|
|
802
|
+
: null;
|
|
803
|
+
}
|
|
804
|
+
class CodexJsonState {
|
|
805
|
+
threadId = null;
|
|
806
|
+
finalMessage = null;
|
|
807
|
+
emittedThread = false;
|
|
808
|
+
get sessionLabel() {
|
|
809
|
+
return this.threadId ? `thread ${this.threadId}` : null;
|
|
810
|
+
}
|
|
811
|
+
consume(line) {
|
|
812
|
+
const event = parseJsonLine(line);
|
|
813
|
+
if (!event) {
|
|
633
814
|
return null;
|
|
634
815
|
}
|
|
635
|
-
const
|
|
636
|
-
const type = typeof event.type === "string" ? event.type : "";
|
|
816
|
+
const type = stringField(event, "type") ?? "";
|
|
637
817
|
if (type === "thread.started") {
|
|
638
818
|
this.threadId = stringField(event, "thread_id");
|
|
639
819
|
if (this.threadId && !this.emittedThread) {
|
|
@@ -658,7 +838,7 @@ class CodexJsonState {
|
|
|
658
838
|
return {
|
|
659
839
|
role: "runner",
|
|
660
840
|
kind: "error",
|
|
661
|
-
content: clip(
|
|
841
|
+
content: clip(line.trim(), 1000),
|
|
662
842
|
eventType: "provider_cli_error"
|
|
663
843
|
};
|
|
664
844
|
}
|