@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.
@@ -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
- const artifactRoot = path.join(metadataRoot, "artifacts");
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
- const codexState = new CodexJsonState();
65
- const result = await this.processRunner.run({
66
- command: command.command,
67
- args: command.args,
68
- cwd: worktree.worktreePath
69
- }, {
70
- onStdoutLine: async (line) => {
71
- const event = codexState.consume(line);
72
- if (event) {
73
- await context.emit(event);
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
- }, context.signal);
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}${codexState.threadId ? ` thread ${codexState.threadId}` : ""}.`,
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 artifactPath = path.join(artifactRoot, "implementation.verdict.json");
89
- const reportPath = path.join(artifactRoot, "implementation.report.md");
90
- const report = buildImplementationReport(dispatch, command, committed, codexState);
91
- const verdict = {
92
- schema_version: 1,
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: codexState.finalMessage,
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 codexState = new CodexJsonState();
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 = codexState.consume(line);
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}${codexState.threadId ? ` thread ${codexState.threadId}` : ""}.`,
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 codexState = new CodexJsonState();
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 = codexState.consume(line);
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: codexState.finalMessage || `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`,
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
- checks: [{ name: "forge_bridge_run_once", status: "pass" }],
270
- metrics: { files_changed: 1 },
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 = path.join(artifactRoot, "implementation.verdict.json");
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(path.join(artifactRoot, "implementation.report.md"), report, "utf8");
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", "--model", model];
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 Develop lane running locally in a prepared git worktree.
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, implement the work item, and keep the change focused.
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
- The runner owns Forge protocol files: worktree prep, injected .forge commands/hooks/skills, phase verdict artifacts, reports, and commits.
551
- Only read the work item spec under .forge; do not edit .forge files or create Forge verdict/report artifacts.
552
- Finish with a concise summary of changed files and verification.`;
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 buildImplementationReport(dispatch, command, committed, codexState) {
590
- const files = committed.changedFiles.length > 0
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
- class CodexJsonState {
617
- threadId = null;
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
- emittedThread = false;
628
+ emittedSession = false;
629
+ get sessionLabel() {
630
+ return this.sessionId ? `session ${this.sessionId}` : null;
631
+ }
620
632
  consume(line) {
621
- const trimmed = line.trim();
622
- if (!trimmed.startsWith("{")) {
633
+ const event = parseJsonLine(line);
634
+ if (!event) {
623
635
  return null;
624
636
  }
625
- let parsed;
626
- try {
627
- parsed = JSON.parse(trimmed);
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
- catch {
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
- if (!parsed || typeof parsed !== "object") {
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 event = parsed;
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(trimmed, 1000),
841
+ content: clip(line.trim(), 1000),
662
842
  eventType: "provider_cli_error"
663
843
  };
664
844
  }
@@ -10,4 +10,5 @@ export interface ProviderCliRunResult {
10
10
  readonly summary: string;
11
11
  readonly spec?: RunnerDispatchSpec | null;
12
12
  readonly artifact?: RunnerDispatchArtifact | null;
13
+ readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
13
14
  }