@loucompanion/forge-bridge 0.1.1-dev.03b8ba168599 → 0.1.1-dev.2bc1e7f19b3a
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 +86 -0
- package/dist/session/phase-completion.js +1018 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +3 -0
- package/dist/session/provider-cli/provider-cli.service.js +482 -151
- 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 +3 -0
- package/dist/session/session.types.js +11 -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 +9 -9
- package/package.json +1 -1
|
@@ -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, completionPhaseFromDispatch, completionPromptForDevelopCycle, completionPromptForPhase, completionRedactionEnv, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDevelopCycleCompletion, writeCompletionArtifact } 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,23 @@ export class ProcessBackedProviderCliService {
|
|
|
40
42
|
}
|
|
41
43
|
async runImplementation(context) {
|
|
42
44
|
const { dispatch, worktree } = context;
|
|
45
|
+
const phase = completionPhaseFromDispatch(dispatch);
|
|
46
|
+
const developCycle = usesDevelopCycleCompletion(dispatch);
|
|
43
47
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
44
|
-
|
|
45
|
-
await fs.mkdir(artifactRoot, { recursive: true });
|
|
48
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
46
49
|
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
47
50
|
if (!laneCommand) {
|
|
48
51
|
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
49
52
|
}
|
|
50
|
-
const specBody = buildSpecBody(laneCommand, dispatch.workItem);
|
|
53
|
+
const specBody = buildSpecBody(laneCommand, dispatch.workItem, phase);
|
|
51
54
|
const specPath = path.join(metadataRoot, "spec.md");
|
|
52
55
|
await fs.writeFile(specPath, specBody, "utf8");
|
|
53
56
|
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
57
|
+
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
|
|
54
58
|
const command = buildProviderCliCommand(dispatch, {
|
|
55
59
|
worktreePath: worktree.worktreePath,
|
|
56
|
-
prompt: buildImplementationPrompt(dispatch)
|
|
60
|
+
prompt: developCycle ? buildDevelopCyclePrompt(dispatch, phase) : buildImplementationPrompt(dispatch, phase),
|
|
61
|
+
completionTools
|
|
57
62
|
});
|
|
58
63
|
await context.emit({
|
|
59
64
|
role: "runner",
|
|
@@ -61,19 +66,34 @@ export class ProcessBackedProviderCliService {
|
|
|
61
66
|
content: `Starting provider CLI: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
|
|
62
67
|
eventType: "provider_cli_started"
|
|
63
68
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
await context.emit({
|
|
70
|
+
role: "runner",
|
|
71
|
+
kind: "checkpoint",
|
|
72
|
+
content: developCycle
|
|
73
|
+
? "Forge completion tools configured for implementation -> self_review -> validation; provider must call each MCP completion tool before exit."
|
|
74
|
+
: `Forge completion tools configured for ${phase}; provider must call the MCP completion tool before exit.`,
|
|
75
|
+
eventType: "completion_tools_configured"
|
|
76
|
+
});
|
|
77
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
78
|
+
let result;
|
|
79
|
+
try {
|
|
80
|
+
result = await this.processRunner.run({
|
|
81
|
+
command: command.command,
|
|
82
|
+
args: command.args,
|
|
83
|
+
cwd: worktree.worktreePath,
|
|
84
|
+
env: providerRedactionEnv(dispatch)
|
|
85
|
+
}, {
|
|
86
|
+
onStdoutLine: async (line) => {
|
|
87
|
+
const event = outputState.consume(line);
|
|
88
|
+
if (event) {
|
|
89
|
+
await context.emit(event);
|
|
90
|
+
}
|
|
74
91
|
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
92
|
+
}, context.signal);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await cleanupCompletionTools(completionTools, context.emit);
|
|
96
|
+
}
|
|
77
97
|
if (result.exitCode !== 0) {
|
|
78
98
|
const stderr = clip(result.stderr.trim() || result.stdout.trim(), 1200);
|
|
79
99
|
throw new Error(`${command.command} exited ${result.exitCode}${stderr ? `: ${stderr}` : ""}`);
|
|
@@ -81,43 +101,22 @@ export class ProcessBackedProviderCliService {
|
|
|
81
101
|
await context.emit({
|
|
82
102
|
role: "runner",
|
|
83
103
|
kind: "checkpoint",
|
|
84
|
-
content: `Provider CLI completed: ${command.command} exit ${result.exitCode}${
|
|
104
|
+
content: `Provider CLI completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
85
105
|
eventType: "provider_cli_completed"
|
|
86
106
|
});
|
|
87
107
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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) {
|
|
108
|
+
const completionArtifacts = await readCompletionArtifactsForRun(worktree.worktreePath, dispatch.workItem.id, phase, developCycle);
|
|
109
|
+
for (const artifact of completionArtifacts) {
|
|
110
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, artifact);
|
|
111
|
+
await emitPhaseCompletionEvent(context.emit, artifact);
|
|
112
|
+
}
|
|
113
|
+
const completionArtifact = completionArtifacts[0];
|
|
114
|
+
if (outputState.finalMessage) {
|
|
116
115
|
await context.emit({
|
|
117
116
|
role: "assistant",
|
|
118
117
|
kind: "message",
|
|
119
|
-
content:
|
|
120
|
-
eventType: "
|
|
118
|
+
content: outputState.finalMessage,
|
|
119
|
+
eventType: "provider_final_message"
|
|
121
120
|
});
|
|
122
121
|
}
|
|
123
122
|
else {
|
|
@@ -125,11 +124,11 @@ export class ProcessBackedProviderCliService {
|
|
|
125
124
|
role: "assistant",
|
|
126
125
|
kind: "message",
|
|
127
126
|
content: `${dispatch.execution.provider} CLI completed for ${worktree.branchName}.`,
|
|
128
|
-
eventType: "
|
|
127
|
+
eventType: "provider_final_message"
|
|
129
128
|
});
|
|
130
129
|
}
|
|
131
130
|
return {
|
|
132
|
-
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName}.`,
|
|
131
|
+
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}, ${committed.commitSha.slice(0, 12)}).`,
|
|
133
132
|
spec: {
|
|
134
133
|
repoPath: worktree.worktreePath,
|
|
135
134
|
path: specPath,
|
|
@@ -137,14 +136,8 @@ export class ProcessBackedProviderCliService {
|
|
|
137
136
|
producedBy: "forge-lane-command",
|
|
138
137
|
status: "approved"
|
|
139
138
|
},
|
|
140
|
-
artifact:
|
|
141
|
-
|
|
142
|
-
path: artifactPath,
|
|
143
|
-
verdictJson,
|
|
144
|
-
report,
|
|
145
|
-
producedBy: `${dispatch.execution.provider}-cli`,
|
|
146
|
-
status: "pass"
|
|
147
|
-
}
|
|
139
|
+
artifact: completionArtifact,
|
|
140
|
+
artifacts: completionArtifacts
|
|
148
141
|
};
|
|
149
142
|
}
|
|
150
143
|
async runQa(dispatch, emit, signal) {
|
|
@@ -157,14 +150,14 @@ export class ProcessBackedProviderCliService {
|
|
|
157
150
|
content: `Starting provider CLI QA: ${formatProviderCliCommand(command)}`,
|
|
158
151
|
eventType: "provider_cli_started"
|
|
159
152
|
});
|
|
160
|
-
const
|
|
153
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
161
154
|
const result = await this.processRunner.run({
|
|
162
155
|
command: command.command,
|
|
163
156
|
args: command.args,
|
|
164
157
|
cwd: process.cwd()
|
|
165
158
|
}, {
|
|
166
159
|
onStdoutLine: async (line) => {
|
|
167
|
-
const event =
|
|
160
|
+
const event = outputState.consume(line);
|
|
168
161
|
if (event) {
|
|
169
162
|
await emit(event);
|
|
170
163
|
}
|
|
@@ -177,7 +170,7 @@ export class ProcessBackedProviderCliService {
|
|
|
177
170
|
await emit({
|
|
178
171
|
role: "runner",
|
|
179
172
|
kind: "checkpoint",
|
|
180
|
-
content: `Provider CLI QA completed: ${command.command} exit ${result.exitCode}${
|
|
173
|
+
content: `Provider CLI QA completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
181
174
|
eventType: "provider_cli_completed"
|
|
182
175
|
});
|
|
183
176
|
return {
|
|
@@ -196,14 +189,14 @@ export class ProcessBackedProviderCliService {
|
|
|
196
189
|
content: `Starting provider CLI conflict resolve: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
|
|
197
190
|
eventType: "provider_cli_started"
|
|
198
191
|
});
|
|
199
|
-
const
|
|
192
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
200
193
|
const result = await this.processRunner.run({
|
|
201
194
|
command: command.command,
|
|
202
195
|
args: command.args,
|
|
203
196
|
cwd: worktree.worktreePath
|
|
204
197
|
}, {
|
|
205
198
|
onStdoutLine: async (line) => {
|
|
206
|
-
const event =
|
|
199
|
+
const event = outputState.consume(line);
|
|
207
200
|
if (event) {
|
|
208
201
|
await context.emit(event);
|
|
209
202
|
}
|
|
@@ -218,7 +211,7 @@ export class ProcessBackedProviderCliService {
|
|
|
218
211
|
await context.emit({
|
|
219
212
|
role: "assistant",
|
|
220
213
|
kind: "message",
|
|
221
|
-
content:
|
|
214
|
+
content: outputState.finalMessage || `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`,
|
|
222
215
|
eventType: "conflict_resolve_complete"
|
|
223
216
|
});
|
|
224
217
|
return {
|
|
@@ -233,6 +226,8 @@ export class DeterministicProviderCliService {
|
|
|
233
226
|
}
|
|
234
227
|
async runImplementation(context) {
|
|
235
228
|
const { dispatch, worktree } = context;
|
|
229
|
+
const phase = completionPhaseFromDispatch(dispatch);
|
|
230
|
+
const developCycle = usesDevelopCycleCompletion(dispatch);
|
|
236
231
|
const providerCommand = buildProviderCliCommand(dispatch);
|
|
237
232
|
const providerCommandText = formatProviderCliCommand(providerCommand);
|
|
238
233
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
@@ -242,7 +237,7 @@ export class DeterministicProviderCliService {
|
|
|
242
237
|
if (!laneCommand) {
|
|
243
238
|
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
244
239
|
}
|
|
245
|
-
const specBody = `${buildSpecBody(laneCommand, dispatch.workItem)}
|
|
240
|
+
const specBody = `${buildSpecBody(laneCommand, dispatch.workItem, phase)}
|
|
246
241
|
## Execution
|
|
247
242
|
|
|
248
243
|
- Provider: ${singleLine(dispatch.execution.provider)}
|
|
@@ -263,27 +258,33 @@ export class DeterministicProviderCliService {
|
|
|
263
258
|
content: `Created bridge changeset at ${changesetPath}`,
|
|
264
259
|
eventType: "changeset"
|
|
265
260
|
});
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
261
|
+
const phases = developCycle
|
|
262
|
+
? ["implementation", "self_review", "validation"]
|
|
263
|
+
: [phase];
|
|
264
|
+
const completionArtifacts = [];
|
|
265
|
+
for (const completionPhase of phases) {
|
|
266
|
+
await writeCompletionArtifact({
|
|
267
|
+
worktreePath: worktree.worktreePath,
|
|
268
|
+
workItemId: dispatch.workItem.id,
|
|
269
|
+
phase: completionPhase,
|
|
270
|
+
producedBy: "forge-bridge",
|
|
271
|
+
redactionSecrets: exactRedactionSecrets(dispatch)
|
|
272
|
+
}, expectedCompletionToolName(completionPhase), deterministicCompletionPayload(completionPhase, dispatch, changesetBody));
|
|
273
|
+
const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, completionPhase);
|
|
274
|
+
if (!completionArtifact) {
|
|
275
|
+
throw new Error(`Deterministic provider did not produce the Forge ${completionPhase} completion artifact.`);
|
|
276
|
+
}
|
|
277
|
+
completionArtifacts.push(completionArtifact);
|
|
278
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, completionArtifact);
|
|
279
|
+
await context.emit({
|
|
280
|
+
role: "assistant",
|
|
281
|
+
kind: "message",
|
|
282
|
+
content: `${completionPhase.replace(/_/g, " ")} complete. forge-bridge produced deterministic phase artifacts.`,
|
|
283
|
+
eventType: phaseCompleteEventType(completionPhase)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
285
286
|
return {
|
|
286
|
-
summary: `forge-bridge completed ${worktree.branchName}.`,
|
|
287
|
+
summary: `forge-bridge completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}).`,
|
|
287
288
|
spec: {
|
|
288
289
|
repoPath: worktree.worktreePath,
|
|
289
290
|
path: specPath,
|
|
@@ -291,14 +292,8 @@ export class DeterministicProviderCliService {
|
|
|
291
292
|
producedBy: "forge-lane-command",
|
|
292
293
|
status: "approved"
|
|
293
294
|
},
|
|
294
|
-
artifact:
|
|
295
|
-
|
|
296
|
-
path: artifactPath,
|
|
297
|
-
verdictJson,
|
|
298
|
-
report,
|
|
299
|
-
producedBy: "forge-bridge",
|
|
300
|
-
status: "pass"
|
|
301
|
-
}
|
|
295
|
+
artifact: completionArtifacts[0],
|
|
296
|
+
artifacts: completionArtifacts
|
|
302
297
|
};
|
|
303
298
|
}
|
|
304
299
|
async runQa(dispatch, emit) {
|
|
@@ -332,12 +327,157 @@ export class DeterministicProviderCliService {
|
|
|
332
327
|
};
|
|
333
328
|
}
|
|
334
329
|
}
|
|
330
|
+
function phaseCompleteEventType(phase) {
|
|
331
|
+
return phase === "implementation" ? "implementation_complete" : `${phase}_complete`;
|
|
332
|
+
}
|
|
333
|
+
function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
334
|
+
const changesetHash = sha256(changesetBody);
|
|
335
|
+
const criterion = dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists";
|
|
336
|
+
if (phase === "implementation") {
|
|
337
|
+
return {
|
|
338
|
+
status: "pass",
|
|
339
|
+
summary: "forge-bridge produced a deterministic implementation changeset.",
|
|
340
|
+
plan: [{ step: "Create deterministic changeset", status: "done" }],
|
|
341
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic bridge evidence." }],
|
|
342
|
+
tests_added_or_updated: [{ name: "forge_bridge_run_once", evidence: "The bridge committed FORGE_PHASE5_CHANGESET.md." }],
|
|
343
|
+
docs_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic evidence document created for the run." }],
|
|
344
|
+
acceptance_criteria: [
|
|
345
|
+
{
|
|
346
|
+
criterion,
|
|
347
|
+
evidence: `FORGE_PHASE5_CHANGESET.md was committed with content hash ${changesetHash}.`,
|
|
348
|
+
status: "satisfied"
|
|
349
|
+
}
|
|
350
|
+
],
|
|
351
|
+
known_gaps: [],
|
|
352
|
+
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation."
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (phase === "self_review") {
|
|
356
|
+
return {
|
|
357
|
+
status: "pass",
|
|
358
|
+
summary: "forge-bridge fake worker completed deterministic self-review.",
|
|
359
|
+
reviewed_diff: true,
|
|
360
|
+
findings: [],
|
|
361
|
+
security_review: {
|
|
362
|
+
status: "pass",
|
|
363
|
+
notes: "No secret handling, auth, or destructive behavior changes were introduced by the deterministic changeset."
|
|
364
|
+
},
|
|
365
|
+
edge_case_review: {
|
|
366
|
+
status: "pass",
|
|
367
|
+
notes: "The deterministic fake-worker changeset is intentionally narrow and repeatable."
|
|
368
|
+
},
|
|
369
|
+
design_review: {
|
|
370
|
+
status: "not_applicable",
|
|
371
|
+
screenshots: [],
|
|
372
|
+
notes: "No UI changes."
|
|
373
|
+
},
|
|
374
|
+
deterministic_comparison: {
|
|
375
|
+
status: "pass",
|
|
376
|
+
command_or_tool: "git diff --stat HEAD~1..HEAD",
|
|
377
|
+
notes: `Reviewed deterministic changeset with content hash ${changesetHash}.`
|
|
378
|
+
},
|
|
379
|
+
fixes_applied: [],
|
|
380
|
+
remaining_risks: []
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
status: "pass",
|
|
385
|
+
summary: "forge-bridge fake worker completed deterministic validation.",
|
|
386
|
+
commands: [
|
|
387
|
+
{
|
|
388
|
+
command: "git status --short && git diff --check",
|
|
389
|
+
exit_code: 0,
|
|
390
|
+
status: "pass",
|
|
391
|
+
summary: "Deterministic validation command completed for the fake-worker changeset."
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
acceptance_criteria: [
|
|
395
|
+
{
|
|
396
|
+
criterion,
|
|
397
|
+
evidence: `FORGE_PHASE5_CHANGESET.md exists with content hash ${changesetHash}.`,
|
|
398
|
+
status: "satisfied"
|
|
399
|
+
}
|
|
400
|
+
],
|
|
401
|
+
build_status: "pass",
|
|
402
|
+
test_status: "pass",
|
|
403
|
+
lint_status: "not_applicable",
|
|
404
|
+
design_status: "not_applicable",
|
|
405
|
+
drift_checks: [
|
|
406
|
+
{
|
|
407
|
+
command_or_tool: "git diff --check",
|
|
408
|
+
status: "pass",
|
|
409
|
+
summary: "No deterministic drift or whitespace errors were reported for the fake-worker changeset."
|
|
410
|
+
}
|
|
411
|
+
],
|
|
412
|
+
ready_for_dev_test: true
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async function emitCompletionArtifactRecorded(emit, worktreePath, artifact) {
|
|
416
|
+
const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
|
|
417
|
+
await emit({
|
|
418
|
+
role: "tool",
|
|
419
|
+
kind: "tool_result",
|
|
420
|
+
content: `Forge completion tool recorded ${artifact.phase ?? "unknown"} artifact ${relativePath} (${artifact.status}).`,
|
|
421
|
+
eventType: "completion_tool_artifact_recorded"
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
async function emitPhaseCompletionEvent(emit, artifact) {
|
|
425
|
+
const phase = completionPhaseFromArtifact(artifact);
|
|
426
|
+
await emit({
|
|
427
|
+
role: "assistant",
|
|
428
|
+
kind: "message",
|
|
429
|
+
content: `${phase.replace(/_/g, " ")} completion artifact recorded with status ${artifact.status}.`,
|
|
430
|
+
eventType: phaseCompleteEventType(phase)
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function completionPhaseFromArtifact(artifact) {
|
|
434
|
+
const phase = artifact.phase?.trim();
|
|
435
|
+
return phase === "self_review" || phase === "validation" || phase === "implementation"
|
|
436
|
+
? phase
|
|
437
|
+
: "implementation";
|
|
438
|
+
}
|
|
439
|
+
async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, developCycle) {
|
|
440
|
+
if (!developCycle) {
|
|
441
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, phase);
|
|
442
|
+
if (!artifact) {
|
|
443
|
+
throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
|
|
444
|
+
}
|
|
445
|
+
return [artifact];
|
|
446
|
+
}
|
|
447
|
+
const artifacts = [];
|
|
448
|
+
for (const expectedPhase of ["implementation", "self_review", "validation"]) {
|
|
449
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, expectedPhase);
|
|
450
|
+
if (!artifact) {
|
|
451
|
+
if (artifacts.length === 0) {
|
|
452
|
+
throw new Error("Provider CLI completed but did not call the Forge implementation completion tool.");
|
|
453
|
+
}
|
|
454
|
+
const previous = artifacts[artifacts.length - 1];
|
|
455
|
+
if ((previous.status ?? "").trim().toLowerCase() === "pass") {
|
|
456
|
+
throw new Error(`Provider CLI completed after ${previous.phase} passed but did not call the Forge ${expectedPhase} completion tool.`);
|
|
457
|
+
}
|
|
458
|
+
return artifacts;
|
|
459
|
+
}
|
|
460
|
+
artifacts.push(artifact);
|
|
461
|
+
if ((artifact.status ?? "").trim().toLowerCase() !== "pass") {
|
|
462
|
+
return artifacts;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return artifacts;
|
|
466
|
+
}
|
|
467
|
+
function providerRedactionEnv(dispatch) {
|
|
468
|
+
const env = completionRedactionEnv(exactRedactionSecrets(dispatch));
|
|
469
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
470
|
+
}
|
|
471
|
+
function exactRedactionSecrets(dispatch) {
|
|
472
|
+
return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
|
|
473
|
+
}
|
|
335
474
|
export class StreamingProviderProcessRunner {
|
|
336
475
|
async run(request, handlers = {}, signal) {
|
|
337
476
|
const launch = await resolveProviderProcessLaunch(request.command, request.args);
|
|
338
477
|
return await new Promise((resolve, reject) => {
|
|
339
478
|
const child = spawn(launch.command, launch.args, {
|
|
340
479
|
cwd: request.cwd,
|
|
480
|
+
env: request.env ? { ...process.env, ...request.env } : undefined,
|
|
341
481
|
stdio: ["ignore", "pipe", "pipe"],
|
|
342
482
|
windowsHide: true
|
|
343
483
|
});
|
|
@@ -406,6 +546,10 @@ async function resolveProviderProcessLaunch(command, args) {
|
|
|
406
546
|
if (process.platform !== "win32") {
|
|
407
547
|
return { command, args };
|
|
408
548
|
}
|
|
549
|
+
const cursorDirect = await resolveCursorAgentDirectLaunch(command, args);
|
|
550
|
+
if (cursorDirect) {
|
|
551
|
+
return cursorDirect;
|
|
552
|
+
}
|
|
409
553
|
const resolved = await resolveWindowsCommand(command);
|
|
410
554
|
return resolved ? buildWindowsLaunchCommand(resolved, args) : { command, args };
|
|
411
555
|
}
|
|
@@ -413,10 +557,6 @@ async function resolveWindowsCommand(command) {
|
|
|
413
557
|
if (path.isAbsolute(command)) {
|
|
414
558
|
return command;
|
|
415
559
|
}
|
|
416
|
-
const cursorScript = await resolveCursorAgentPowerShellScript(command);
|
|
417
|
-
if (cursorScript) {
|
|
418
|
-
return cursorScript;
|
|
419
|
-
}
|
|
420
560
|
try {
|
|
421
561
|
const result = await execFileAsync("where.exe", [command], { timeout: 2000 });
|
|
422
562
|
return chooseWindowsCommandCandidate(result.stdout.split(/\r?\n/));
|
|
@@ -425,18 +565,6 @@ async function resolveWindowsCommand(command) {
|
|
|
425
565
|
return null;
|
|
426
566
|
}
|
|
427
567
|
}
|
|
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
568
|
export function chooseWindowsCommandCandidate(candidates) {
|
|
441
569
|
const normalized = candidates
|
|
442
570
|
.map((candidate) => candidate.trim())
|
|
@@ -518,7 +646,11 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
518
646
|
const normalizedProvider = provider.toLowerCase();
|
|
519
647
|
const model = dispatch.execution.model.trim();
|
|
520
648
|
if (normalizedProvider === "codex") {
|
|
521
|
-
const args = ["exec"
|
|
649
|
+
const args = ["exec"];
|
|
650
|
+
if (run?.completionTools) {
|
|
651
|
+
args.push(...codexMcpConfigArgs(run.completionTools));
|
|
652
|
+
}
|
|
653
|
+
args.push("--model", model);
|
|
522
654
|
if (run) {
|
|
523
655
|
args.push("--json", "--dangerously-bypass-approvals-and-sandbox");
|
|
524
656
|
if (run.worktreePath) {
|
|
@@ -529,10 +661,12 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
529
661
|
return { command: "codex", args };
|
|
530
662
|
}
|
|
531
663
|
if (normalizedProvider === "cursor") {
|
|
532
|
-
const args = ["--model", model];
|
|
664
|
+
const args = ["--print", "--output-format", "stream-json", "--model", model];
|
|
665
|
+
if (run?.worktreePath) {
|
|
666
|
+
args.push("--workspace", run.worktreePath);
|
|
667
|
+
}
|
|
533
668
|
if (run?.prompt) {
|
|
534
|
-
args.push("--yolo");
|
|
535
|
-
args.push(run.prompt);
|
|
669
|
+
args.push("--yolo", "--approve-mcps", "--trust", run.prompt);
|
|
536
670
|
}
|
|
537
671
|
return { command: "cursor-agent", args };
|
|
538
672
|
}
|
|
@@ -542,14 +676,40 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
542
676
|
}
|
|
543
677
|
return { command: provider, args };
|
|
544
678
|
}
|
|
545
|
-
function buildImplementationPrompt(dispatch) {
|
|
546
|
-
return `You are Forge's
|
|
679
|
+
function buildImplementationPrompt(dispatch, phase = completionPhaseFromDispatch(dispatch)) {
|
|
680
|
+
return `You are Forge's ${phase.replace(/_/g, " ")} phase runner running locally in a prepared git worktree.
|
|
547
681
|
|
|
548
|
-
Read .forge/work-items/${dispatch.workItem.id}/spec.md
|
|
682
|
+
Read .forge/work-items/${dispatch.workItem.id}/spec.md and any prior phase artifacts required by this phase.
|
|
683
|
+
${phaseWorkInstruction(phase)}
|
|
549
684
|
Use the repository's existing conventions. Run relevant tests when practical.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
685
|
+
Forge owns protocol files: worktree prep, injected .forge commands/hooks/skills, phase verdict artifacts, reports, and commits.
|
|
686
|
+
Read Forge-provided context under .forge when needed, but do not edit .forge files or create Forge verdict/report artifacts by hand.
|
|
687
|
+
|
|
688
|
+
${completionPromptForPhase(phase)}
|
|
689
|
+
|
|
690
|
+
Finish with a concise summary of changed files and verification after calling the completion tool.`;
|
|
691
|
+
}
|
|
692
|
+
function buildDevelopCyclePrompt(dispatch, phase = completionPhaseFromDispatch(dispatch)) {
|
|
693
|
+
return `You are Forge's develop-cycle runner running locally in a prepared git worktree.
|
|
694
|
+
|
|
695
|
+
Read .forge/work-items/${dispatch.workItem.id}/spec.md, implement the work item, self-review the diff, then validate it in this same provider session.
|
|
696
|
+
Start in the ${phase.replace(/_/g, " ")} phase. Do not stop after implementation if it passes; continue through self-review and validation using the next instruction returned by each completion tool.
|
|
697
|
+
Use the repository's existing conventions. Run relevant tests when practical.
|
|
698
|
+
Forge owns protocol files: worktree prep, injected .forge commands/hooks/skills, phase verdict artifacts, reports, and commits.
|
|
699
|
+
Read Forge-provided context under .forge when needed, but do not edit .forge files or create Forge verdict/report artifacts by hand.
|
|
700
|
+
|
|
701
|
+
${completionPromptForDevelopCycle(phase)}
|
|
702
|
+
|
|
703
|
+
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.`;
|
|
704
|
+
}
|
|
705
|
+
function phaseWorkInstruction(phase) {
|
|
706
|
+
if (phase === "self_review") {
|
|
707
|
+
return "Review the implementation diff, apply safe fixes when needed, and record remaining risks explicitly.";
|
|
708
|
+
}
|
|
709
|
+
if (phase === "validation") {
|
|
710
|
+
return "Run the validation commands and deterministic drift checks needed to prove the work is ready for Dev Test.";
|
|
711
|
+
}
|
|
712
|
+
return "Implement the work item and keep the change focused.";
|
|
553
713
|
}
|
|
554
714
|
function buildQaPrompt(dispatch) {
|
|
555
715
|
return `You are Forge's QA lane running locally.
|
|
@@ -570,6 +730,19 @@ Keep the original work item intent intact:
|
|
|
570
730
|
|
|
571
731
|
Finish with a concise summary of the conflicts resolved and verification performed.`;
|
|
572
732
|
}
|
|
733
|
+
async function cleanupCompletionTools(completionTools, emit) {
|
|
734
|
+
try {
|
|
735
|
+
await completionTools.cleanup();
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
await emit({
|
|
739
|
+
role: "runner",
|
|
740
|
+
kind: "checkpoint",
|
|
741
|
+
content: `Forge completion tool cleanup failed and was ignored: ${clip(messageFromError(error), 600)}`,
|
|
742
|
+
eventType: "completion_tools_cleanup_failed"
|
|
743
|
+
}).catch(() => undefined);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
573
746
|
async function commitWorktreeChanges(git, worktreePath, workItemId) {
|
|
574
747
|
await git.run(worktreePath, ["add", "-A"]);
|
|
575
748
|
await git.run(worktreePath, ["reset", "--", ".forge"]).catch(() => "");
|
|
@@ -586,26 +759,8 @@ async function commitWorktreeChanges(git, worktreePath, workItemId) {
|
|
|
586
759
|
const commitSha = (await git.run(worktreePath, ["rev-parse", "HEAD"])).trim();
|
|
587
760
|
return { commitSha, changedFiles };
|
|
588
761
|
}
|
|
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
|
-
`;
|
|
762
|
+
function messageFromError(error) {
|
|
763
|
+
return error instanceof Error ? error.message : String(error);
|
|
609
764
|
}
|
|
610
765
|
function clip(value, maxLength) {
|
|
611
766
|
if (value.length <= maxLength) {
|
|
@@ -613,27 +768,203 @@ function clip(value, maxLength) {
|
|
|
613
768
|
}
|
|
614
769
|
return `${value.slice(0, maxLength)}...`;
|
|
615
770
|
}
|
|
616
|
-
|
|
617
|
-
|
|
771
|
+
function createProviderOutputState(provider) {
|
|
772
|
+
return provider.trim().toLowerCase() === "cursor"
|
|
773
|
+
? new CursorStreamJsonState()
|
|
774
|
+
: new CodexJsonState();
|
|
775
|
+
}
|
|
776
|
+
class CursorStreamJsonState {
|
|
777
|
+
sessionId = null;
|
|
618
778
|
finalMessage = null;
|
|
619
|
-
|
|
779
|
+
emittedSession = false;
|
|
780
|
+
get sessionLabel() {
|
|
781
|
+
return this.sessionId ? `session ${this.sessionId}` : null;
|
|
782
|
+
}
|
|
620
783
|
consume(line) {
|
|
621
|
-
const
|
|
622
|
-
if (!
|
|
784
|
+
const event = parseJsonLine(line);
|
|
785
|
+
if (!event) {
|
|
623
786
|
return null;
|
|
624
787
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
788
|
+
const type = stringField(event, "type");
|
|
789
|
+
if (type === "system" && stringField(event, "subtype") === "init") {
|
|
790
|
+
this.sessionId = stringField(event, "session_id");
|
|
791
|
+
if (this.sessionId && !this.emittedSession) {
|
|
792
|
+
this.emittedSession = true;
|
|
793
|
+
return {
|
|
794
|
+
role: "runner",
|
|
795
|
+
kind: "checkpoint",
|
|
796
|
+
content: `Cursor session started: ${this.sessionId}.`,
|
|
797
|
+
eventType: "provider_thread_started"
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (type === "assistant") {
|
|
802
|
+
const text = assistantMessageText(event);
|
|
803
|
+
if (text) {
|
|
804
|
+
this.finalMessage = clip(text, 4000);
|
|
805
|
+
return {
|
|
806
|
+
role: "assistant",
|
|
807
|
+
kind: "message",
|
|
808
|
+
content: this.finalMessage,
|
|
809
|
+
eventType: "provider_message"
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (type === "tool_call") {
|
|
814
|
+
const subtype = stringField(event, "subtype");
|
|
815
|
+
const toolCall = objectField(event, "tool_call");
|
|
816
|
+
if (subtype === "started") {
|
|
817
|
+
const summary = formatCursorToolCallStarted(toolCall);
|
|
818
|
+
if (summary) {
|
|
819
|
+
return {
|
|
820
|
+
role: "worker",
|
|
821
|
+
kind: "checkpoint",
|
|
822
|
+
content: summary,
|
|
823
|
+
eventType: "provider_tool_call"
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (subtype === "completed") {
|
|
828
|
+
const completed = formatCursorToolCallCompleted(toolCall);
|
|
829
|
+
if (completed) {
|
|
830
|
+
return {
|
|
831
|
+
role: "worker",
|
|
832
|
+
kind: completed.kind,
|
|
833
|
+
content: completed.content,
|
|
834
|
+
eventType: completed.kind === "file_edit" ? "provider_file_edit" : "provider_tool_call"
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (type === "result" && stringField(event, "subtype") === "success") {
|
|
840
|
+
const result = stringField(event, "result");
|
|
841
|
+
if (result) {
|
|
842
|
+
this.finalMessage = clip(result, 4000);
|
|
843
|
+
}
|
|
628
844
|
}
|
|
629
|
-
|
|
845
|
+
if (type === "error" || (type === "result" && event.is_error === true)) {
|
|
846
|
+
return {
|
|
847
|
+
role: "runner",
|
|
848
|
+
kind: "error",
|
|
849
|
+
content: clip(line.trim(), 1000),
|
|
850
|
+
eventType: "provider_cli_error"
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function formatCursorToolCallStarted(toolCall) {
|
|
857
|
+
const readArgs = objectField(objectField(toolCall, "readToolCall"), "args");
|
|
858
|
+
const readPath = stringField(readArgs, "path");
|
|
859
|
+
if (readPath) {
|
|
860
|
+
return `Reading ${readPath}`;
|
|
861
|
+
}
|
|
862
|
+
const writeArgs = objectField(objectField(toolCall, "writeToolCall"), "args");
|
|
863
|
+
const writePath = stringField(writeArgs, "path");
|
|
864
|
+
if (writePath) {
|
|
865
|
+
return `Writing ${writePath}`;
|
|
866
|
+
}
|
|
867
|
+
const shellArgs = objectField(objectField(toolCall, "shellToolCall"), "args");
|
|
868
|
+
const command = stringField(shellArgs, "command");
|
|
869
|
+
if (command) {
|
|
870
|
+
return `Shell command started: ${clip(command, 500)}`;
|
|
871
|
+
}
|
|
872
|
+
const fn = objectField(toolCall, "function");
|
|
873
|
+
const name = stringField(fn, "name");
|
|
874
|
+
if (name) {
|
|
875
|
+
return `Tool call started: ${name}`;
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
function formatCursorToolCallCompleted(toolCall) {
|
|
880
|
+
const writeArgs = objectField(objectField(toolCall, "writeToolCall"), "args");
|
|
881
|
+
const writePath = stringField(writeArgs, "path");
|
|
882
|
+
const writeResult = objectField(objectField(objectField(toolCall, "writeToolCall"), "result"), "success");
|
|
883
|
+
const writtenPath = stringField(writeResult, "path") ?? writePath;
|
|
884
|
+
if (writtenPath) {
|
|
885
|
+
return {
|
|
886
|
+
kind: "file_edit",
|
|
887
|
+
content: `Wrote ${writtenPath}`
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const readArgs = objectField(objectField(toolCall, "readToolCall"), "args");
|
|
891
|
+
const readPath = stringField(readArgs, "path");
|
|
892
|
+
if (readPath) {
|
|
893
|
+
const readResult = objectField(objectField(objectField(toolCall, "readToolCall"), "result"), "success");
|
|
894
|
+
const totalLines = readResult.totalLines;
|
|
895
|
+
const lineSummary = typeof totalLines === "number" ? ` (${totalLines} lines)` : "";
|
|
896
|
+
return {
|
|
897
|
+
kind: "checkpoint",
|
|
898
|
+
content: `Read ${readPath}${lineSummary}`
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const shellArgs = objectField(objectField(toolCall, "shellToolCall"), "args");
|
|
902
|
+
const command = stringField(shellArgs, "command");
|
|
903
|
+
if (command) {
|
|
904
|
+
return {
|
|
905
|
+
kind: "checkpoint",
|
|
906
|
+
content: `Shell command completed: ${clip(command, 500)}`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const fn = objectField(toolCall, "function");
|
|
910
|
+
const name = stringField(fn, "name");
|
|
911
|
+
if (name) {
|
|
912
|
+
return {
|
|
913
|
+
kind: "checkpoint",
|
|
914
|
+
content: `Tool call completed: ${name}`
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
function assistantMessageText(event) {
|
|
920
|
+
const message = objectField(event, "message");
|
|
921
|
+
const content = message.content;
|
|
922
|
+
if (!Array.isArray(content)) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
const parts = content
|
|
926
|
+
.map((entry) => {
|
|
927
|
+
if (!entry || typeof entry !== "object") {
|
|
630
928
|
return null;
|
|
631
929
|
}
|
|
632
|
-
|
|
930
|
+
const block = entry;
|
|
931
|
+
return stringField(block, "text");
|
|
932
|
+
})
|
|
933
|
+
.filter((text) => Boolean(text));
|
|
934
|
+
if (parts.length === 0) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
return parts.join("");
|
|
938
|
+
}
|
|
939
|
+
function parseJsonLine(line) {
|
|
940
|
+
const trimmed = line.trim();
|
|
941
|
+
if (!trimmed.startsWith("{")) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
let parsed;
|
|
945
|
+
try {
|
|
946
|
+
parsed = JSON.parse(trimmed);
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
952
|
+
? parsed
|
|
953
|
+
: null;
|
|
954
|
+
}
|
|
955
|
+
class CodexJsonState {
|
|
956
|
+
threadId = null;
|
|
957
|
+
finalMessage = null;
|
|
958
|
+
emittedThread = false;
|
|
959
|
+
get sessionLabel() {
|
|
960
|
+
return this.threadId ? `thread ${this.threadId}` : null;
|
|
961
|
+
}
|
|
962
|
+
consume(line) {
|
|
963
|
+
const event = parseJsonLine(line);
|
|
964
|
+
if (!event) {
|
|
633
965
|
return null;
|
|
634
966
|
}
|
|
635
|
-
const
|
|
636
|
-
const type = typeof event.type === "string" ? event.type : "";
|
|
967
|
+
const type = stringField(event, "type") ?? "";
|
|
637
968
|
if (type === "thread.started") {
|
|
638
969
|
this.threadId = stringField(event, "thread_id");
|
|
639
970
|
if (this.threadId && !this.emittedThread) {
|
|
@@ -658,7 +989,7 @@ class CodexJsonState {
|
|
|
658
989
|
return {
|
|
659
990
|
role: "runner",
|
|
660
991
|
kind: "error",
|
|
661
|
-
content: clip(
|
|
992
|
+
content: clip(line.trim(), 1000),
|
|
662
993
|
eventType: "provider_cli_error"
|
|
663
994
|
};
|
|
664
995
|
}
|