@loucompanion/forge-bridge 0.1.1-dev.32d4dd5874ee → 0.1.1-dev.4f31cdd58cac
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/session/phase-completion.d.ts +32 -0
- package/dist/session/phase-completion.js +360 -79
- package/dist/session/provider-cli/provider-cli.service.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.service.js +351 -54
- package/dist/session/provider-cli/provider-usage.d.ts +25 -0
- package/dist/session/provider-cli/provider-usage.js +64 -0
- package/dist/session/session.types.d.ts +5 -0
- package/dist/session/session.types.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +1 -4
- package/package.json +1 -1
|
@@ -7,9 +7,11 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { GitClient } from "../../shared/git/git.js";
|
|
8
8
|
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
9
9
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
10
|
-
import { codexMcpConfigArgs,
|
|
10
|
+
import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDevelopCycle, completionPromptForPhase, completionRedactionEnv, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDevelopCycleCompletion, writeCompletionArtifact } from "../phase-completion.js";
|
|
11
|
+
import { addUsage, buildProviderUsageEvent, emptyUsage, hasUsage, readUsageNumber } from "./provider-usage.js";
|
|
11
12
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
14
|
+
const developCyclePhaseOrder = ["implementation", "self_review", "validation"];
|
|
13
15
|
export class RoutingProviderCliService {
|
|
14
16
|
realProvider;
|
|
15
17
|
deterministicProvider;
|
|
@@ -43,6 +45,7 @@ export class ProcessBackedProviderCliService {
|
|
|
43
45
|
async runImplementation(context) {
|
|
44
46
|
const { dispatch, worktree } = context;
|
|
45
47
|
const phase = completionPhaseFromDispatch(dispatch);
|
|
48
|
+
const developCycle = usesDevelopCycleCompletion(dispatch);
|
|
46
49
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
47
50
|
await fs.mkdir(metadataRoot, { recursive: true });
|
|
48
51
|
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
@@ -56,7 +59,7 @@ export class ProcessBackedProviderCliService {
|
|
|
56
59
|
const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree, phase });
|
|
57
60
|
const command = buildProviderCliCommand(dispatch, {
|
|
58
61
|
worktreePath: worktree.worktreePath,
|
|
59
|
-
prompt: buildImplementationPrompt(dispatch, phase),
|
|
62
|
+
prompt: developCycle ? buildDevelopCyclePrompt(dispatch, phase) : buildImplementationPrompt(dispatch, phase),
|
|
60
63
|
completionTools
|
|
61
64
|
});
|
|
62
65
|
await context.emit({
|
|
@@ -68,16 +71,21 @@ export class ProcessBackedProviderCliService {
|
|
|
68
71
|
await context.emit({
|
|
69
72
|
role: "runner",
|
|
70
73
|
kind: "checkpoint",
|
|
71
|
-
content:
|
|
74
|
+
content: developCycle
|
|
75
|
+
? "Forge completion tools configured for implementation -> self_review -> validation; provider must call each MCP completion tool before exit."
|
|
76
|
+
: `Forge completion tools configured for ${phase}; provider must call the MCP completion tool before exit.`,
|
|
72
77
|
eventType: "completion_tools_configured"
|
|
73
78
|
});
|
|
74
79
|
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
80
|
+
const liveCompletionEvents = new LiveCompletionArtifactEmitter(worktree.worktreePath, dispatch.workItem.id, phase, developCycle, context.emit);
|
|
75
81
|
let result;
|
|
76
82
|
try {
|
|
83
|
+
liveCompletionEvents.start();
|
|
77
84
|
result = await this.processRunner.run({
|
|
78
85
|
command: command.command,
|
|
79
86
|
args: command.args,
|
|
80
|
-
cwd: worktree.worktreePath
|
|
87
|
+
cwd: worktree.worktreePath,
|
|
88
|
+
env: providerRedactionEnv(dispatch)
|
|
81
89
|
}, {
|
|
82
90
|
onStdoutLine: async (line) => {
|
|
83
91
|
const event = outputState.consume(line);
|
|
@@ -88,6 +96,7 @@ export class ProcessBackedProviderCliService {
|
|
|
88
96
|
}, context.signal);
|
|
89
97
|
}
|
|
90
98
|
finally {
|
|
99
|
+
await liveCompletionEvents.stop();
|
|
91
100
|
await cleanupCompletionTools(completionTools, context.emit);
|
|
92
101
|
}
|
|
93
102
|
if (result.exitCode !== 0) {
|
|
@@ -100,17 +109,23 @@ export class ProcessBackedProviderCliService {
|
|
|
100
109
|
content: `Provider CLI completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
101
110
|
eventType: "provider_cli_completed"
|
|
102
111
|
});
|
|
112
|
+
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
103
113
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
const completionArtifacts = await readCompletionArtifactsForRun(worktree.worktreePath, dispatch.workItem.id, phase, developCycle);
|
|
115
|
+
for (const artifact of completionArtifacts) {
|
|
116
|
+
if (!liveCompletionEvents.hasEmitted(completionPhaseFromArtifact(artifact))) {
|
|
117
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, artifact);
|
|
118
|
+
await emitPhaseCompletionEvent(context.emit, artifact);
|
|
119
|
+
await emitPhaseGateEvents(context.emit, artifact);
|
|
120
|
+
}
|
|
107
121
|
}
|
|
122
|
+
const completionArtifact = completionArtifacts[0];
|
|
108
123
|
if (outputState.finalMessage) {
|
|
109
124
|
await context.emit({
|
|
110
125
|
role: "assistant",
|
|
111
126
|
kind: "message",
|
|
112
127
|
content: outputState.finalMessage,
|
|
113
|
-
eventType: "
|
|
128
|
+
eventType: "provider_final_message"
|
|
114
129
|
});
|
|
115
130
|
}
|
|
116
131
|
else {
|
|
@@ -118,11 +133,11 @@ export class ProcessBackedProviderCliService {
|
|
|
118
133
|
role: "assistant",
|
|
119
134
|
kind: "message",
|
|
120
135
|
content: `${dispatch.execution.provider} CLI completed for ${worktree.branchName}.`,
|
|
121
|
-
eventType: "
|
|
136
|
+
eventType: "provider_final_message"
|
|
122
137
|
});
|
|
123
138
|
}
|
|
124
139
|
return {
|
|
125
|
-
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${phase}, ${committed.commitSha.slice(0, 12)}).`,
|
|
140
|
+
summary: `${dispatch.execution.provider} CLI completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}, ${committed.commitSha.slice(0, 12)}).`,
|
|
126
141
|
spec: {
|
|
127
142
|
repoPath: worktree.worktreePath,
|
|
128
143
|
path: specPath,
|
|
@@ -130,7 +145,8 @@ export class ProcessBackedProviderCliService {
|
|
|
130
145
|
producedBy: "forge-lane-command",
|
|
131
146
|
status: "approved"
|
|
132
147
|
},
|
|
133
|
-
artifact: completionArtifact
|
|
148
|
+
artifact: completionArtifact,
|
|
149
|
+
artifacts: completionArtifacts
|
|
134
150
|
};
|
|
135
151
|
}
|
|
136
152
|
async runQa(dispatch, emit, signal) {
|
|
@@ -166,6 +182,7 @@ export class ProcessBackedProviderCliService {
|
|
|
166
182
|
content: `Provider CLI QA completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
167
183
|
eventType: "provider_cli_completed"
|
|
168
184
|
});
|
|
185
|
+
await emitProviderUsageEvent(emit, dispatch, outputState);
|
|
169
186
|
return {
|
|
170
187
|
summary: `${dispatch.execution.provider} CLI QA passed.`
|
|
171
188
|
};
|
|
@@ -201,6 +218,7 @@ export class ProcessBackedProviderCliService {
|
|
|
201
218
|
}
|
|
202
219
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
203
220
|
await this.git.run(worktree.worktreePath, ["push", "origin", `HEAD:${worktree.branchName}`]);
|
|
221
|
+
await emitProviderUsageEvent(context.emit, dispatch, outputState);
|
|
204
222
|
await context.emit({
|
|
205
223
|
role: "assistant",
|
|
206
224
|
kind: "message",
|
|
@@ -220,6 +238,7 @@ export class DeterministicProviderCliService {
|
|
|
220
238
|
async runImplementation(context) {
|
|
221
239
|
const { dispatch, worktree } = context;
|
|
222
240
|
const phase = completionPhaseFromDispatch(dispatch);
|
|
241
|
+
const developCycle = usesDevelopCycleCompletion(dispatch);
|
|
223
242
|
const providerCommand = buildProviderCliCommand(dispatch);
|
|
224
243
|
const providerCommandText = formatProviderCliCommand(providerCommand);
|
|
225
244
|
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
@@ -250,41 +269,34 @@ export class DeterministicProviderCliService {
|
|
|
250
269
|
content: `Created bridge changeset at ${changesetPath}`,
|
|
251
270
|
eventType: "changeset"
|
|
252
271
|
});
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await
|
|
279
|
-
await fs.writeFile(reportPath, report, "utf8");
|
|
280
|
-
await context.emit({
|
|
281
|
-
role: "assistant",
|
|
282
|
-
kind: "message",
|
|
283
|
-
content: "Implementation complete. forge-bridge produced deterministic phase artifacts.",
|
|
284
|
-
eventType: "implementation_complete"
|
|
285
|
-
});
|
|
272
|
+
const phases = developCycle
|
|
273
|
+
? ["implementation", "self_review", "validation"]
|
|
274
|
+
: [phase];
|
|
275
|
+
const completionArtifacts = [];
|
|
276
|
+
for (const completionPhase of phases) {
|
|
277
|
+
await writeCompletionArtifact({
|
|
278
|
+
worktreePath: worktree.worktreePath,
|
|
279
|
+
workItemId: dispatch.workItem.id,
|
|
280
|
+
phase: completionPhase,
|
|
281
|
+
producedBy: "forge-bridge",
|
|
282
|
+
redactionSecrets: exactRedactionSecrets(dispatch)
|
|
283
|
+
}, expectedCompletionToolName(completionPhase), deterministicCompletionPayload(completionPhase, dispatch, changesetBody));
|
|
284
|
+
const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, completionPhase);
|
|
285
|
+
if (!completionArtifact) {
|
|
286
|
+
throw new Error(`Deterministic provider did not produce the Forge ${completionPhase} completion artifact.`);
|
|
287
|
+
}
|
|
288
|
+
completionArtifacts.push(completionArtifact);
|
|
289
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, completionArtifact);
|
|
290
|
+
await context.emit({
|
|
291
|
+
role: "assistant",
|
|
292
|
+
kind: "message",
|
|
293
|
+
content: `${completionPhase.replace(/_/g, " ")} complete. forge-bridge produced deterministic phase artifacts.`,
|
|
294
|
+
eventType: phaseCompleteEventType(completionPhase)
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
286
298
|
return {
|
|
287
|
-
summary: `forge-bridge completed ${worktree.branchName}.`,
|
|
299
|
+
summary: `forge-bridge completed ${worktree.branchName} (${developCycle ? "develop cycle" : phase}).`,
|
|
288
300
|
spec: {
|
|
289
301
|
repoPath: worktree.worktreePath,
|
|
290
302
|
path: specPath,
|
|
@@ -292,15 +304,8 @@ export class DeterministicProviderCliService {
|
|
|
292
304
|
producedBy: "forge-lane-command",
|
|
293
305
|
status: "approved"
|
|
294
306
|
},
|
|
295
|
-
artifact:
|
|
296
|
-
|
|
297
|
-
path: artifactPath,
|
|
298
|
-
verdictJson,
|
|
299
|
-
report,
|
|
300
|
-
producedBy: "forge-bridge",
|
|
301
|
-
status: "pass",
|
|
302
|
-
phase: "implementation"
|
|
303
|
-
}
|
|
307
|
+
artifact: completionArtifacts[0],
|
|
308
|
+
artifacts: completionArtifacts
|
|
304
309
|
};
|
|
305
310
|
}
|
|
306
311
|
async runQa(dispatch, emit) {
|
|
@@ -316,6 +321,7 @@ export class DeterministicProviderCliService {
|
|
|
316
321
|
content: "All checks passed. forge-bridge QA completed.",
|
|
317
322
|
eventType: "qa_complete"
|
|
318
323
|
});
|
|
324
|
+
await emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
319
325
|
return {
|
|
320
326
|
summary: "forge-bridge QA passed."
|
|
321
327
|
};
|
|
@@ -323,6 +329,7 @@ export class DeterministicProviderCliService {
|
|
|
323
329
|
async runConflictResolve(context) {
|
|
324
330
|
const { dispatch, worktree } = context;
|
|
325
331
|
await this.git.run(worktree.worktreePath, ["commit", "--allow-empty", "-m", `forge bridge conflict resolve for ${dispatch.workItem.id.slice(0, 8)}`]);
|
|
332
|
+
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
326
333
|
await context.emit({
|
|
327
334
|
role: "assistant",
|
|
328
335
|
kind: "message",
|
|
@@ -334,12 +341,251 @@ export class DeterministicProviderCliService {
|
|
|
334
341
|
};
|
|
335
342
|
}
|
|
336
343
|
}
|
|
344
|
+
function phaseCompleteEventType(phase) {
|
|
345
|
+
return phase === "implementation" ? "implementation_complete" : `${phase}_complete`;
|
|
346
|
+
}
|
|
347
|
+
function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
348
|
+
const changesetHash = sha256(changesetBody);
|
|
349
|
+
const criterion = dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists";
|
|
350
|
+
if (phase === "implementation") {
|
|
351
|
+
return {
|
|
352
|
+
status: "pass",
|
|
353
|
+
summary: "forge-bridge produced a deterministic implementation changeset.",
|
|
354
|
+
plan: [{ step: "Create deterministic changeset", status: "done" }],
|
|
355
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic bridge evidence." }],
|
|
356
|
+
tests_added_or_updated: [{ name: "forge_bridge_run_once", evidence: "The bridge committed FORGE_PHASE5_CHANGESET.md." }],
|
|
357
|
+
docs_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic evidence document created for the run." }],
|
|
358
|
+
acceptance_criteria: [
|
|
359
|
+
{
|
|
360
|
+
criterion,
|
|
361
|
+
evidence: `FORGE_PHASE5_CHANGESET.md was committed with content hash ${changesetHash}.`,
|
|
362
|
+
status: "satisfied"
|
|
363
|
+
}
|
|
364
|
+
],
|
|
365
|
+
known_gaps: [],
|
|
366
|
+
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation."
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (phase === "self_review") {
|
|
370
|
+
return {
|
|
371
|
+
status: "pass",
|
|
372
|
+
summary: "forge-bridge fake worker completed deterministic self-review.",
|
|
373
|
+
reviewed_diff: true,
|
|
374
|
+
findings: [],
|
|
375
|
+
security_review: {
|
|
376
|
+
status: "pass",
|
|
377
|
+
notes: "No secret handling, auth, or destructive behavior changes were introduced by the deterministic changeset."
|
|
378
|
+
},
|
|
379
|
+
edge_case_review: {
|
|
380
|
+
status: "pass",
|
|
381
|
+
notes: "The deterministic fake-worker changeset is intentionally narrow and repeatable."
|
|
382
|
+
},
|
|
383
|
+
design_review: {
|
|
384
|
+
status: "not_applicable",
|
|
385
|
+
screenshots: [],
|
|
386
|
+
notes: "No UI changes."
|
|
387
|
+
},
|
|
388
|
+
deterministic_comparison: {
|
|
389
|
+
status: "pass",
|
|
390
|
+
command_or_tool: "git diff --stat HEAD~1..HEAD",
|
|
391
|
+
notes: `Reviewed deterministic changeset with content hash ${changesetHash}.`
|
|
392
|
+
},
|
|
393
|
+
fixes_applied: [],
|
|
394
|
+
remaining_risks: []
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
status: "pass",
|
|
399
|
+
summary: "forge-bridge fake worker completed deterministic validation.",
|
|
400
|
+
commands: [
|
|
401
|
+
{
|
|
402
|
+
command: "git status --short && git diff --check",
|
|
403
|
+
exit_code: 0,
|
|
404
|
+
status: "pass",
|
|
405
|
+
summary: "Deterministic validation command completed for the fake-worker changeset."
|
|
406
|
+
}
|
|
407
|
+
],
|
|
408
|
+
acceptance_criteria: [
|
|
409
|
+
{
|
|
410
|
+
criterion,
|
|
411
|
+
evidence: `FORGE_PHASE5_CHANGESET.md exists with content hash ${changesetHash}.`,
|
|
412
|
+
status: "satisfied"
|
|
413
|
+
}
|
|
414
|
+
],
|
|
415
|
+
build_status: "pass",
|
|
416
|
+
test_status: "pass",
|
|
417
|
+
lint_status: "not_applicable",
|
|
418
|
+
design_status: "not_applicable",
|
|
419
|
+
drift_checks: [
|
|
420
|
+
{
|
|
421
|
+
command_or_tool: "git diff --check",
|
|
422
|
+
status: "pass",
|
|
423
|
+
summary: "No deterministic drift or whitespace errors were reported for the fake-worker changeset."
|
|
424
|
+
}
|
|
425
|
+
],
|
|
426
|
+
ready_for_dev_test: true
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async function emitCompletionArtifactRecorded(emit, worktreePath, artifact) {
|
|
430
|
+
const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
|
|
431
|
+
await emit({
|
|
432
|
+
role: "tool",
|
|
433
|
+
kind: "tool_result",
|
|
434
|
+
content: `Forge completion tool recorded ${artifact.phase ?? "unknown"} artifact ${relativePath} (${artifact.status}).`,
|
|
435
|
+
eventType: "completion_tool_artifact_recorded"
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
async function emitPhaseCompletionEvent(emit, artifact) {
|
|
439
|
+
const phase = completionPhaseFromArtifact(artifact);
|
|
440
|
+
await emit({
|
|
441
|
+
role: "assistant",
|
|
442
|
+
kind: "message",
|
|
443
|
+
content: `${phase.replace(/_/g, " ")} completion artifact recorded with status ${artifact.status}.`,
|
|
444
|
+
eventType: phaseCompleteEventType(phase)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async function emitPhaseGateEvents(emit, artifact) {
|
|
448
|
+
const phase = completionPhaseFromArtifact(artifact);
|
|
449
|
+
const status = (artifact.status ?? "").trim().toLowerCase() || "unknown";
|
|
450
|
+
await emit({
|
|
451
|
+
role: "hook",
|
|
452
|
+
kind: "checkpoint",
|
|
453
|
+
content: `phase_gate: ${phase} artifact accepted with status ${status}.`,
|
|
454
|
+
eventType: "phase_gate"
|
|
455
|
+
});
|
|
456
|
+
if (status !== "pass") {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const progression = phase === "implementation"
|
|
460
|
+
? "phase_progression: implementation -> self_review"
|
|
461
|
+
: phase === "self_review"
|
|
462
|
+
? "phase_progression: self_review -> validation"
|
|
463
|
+
: "phase_progression: validation -> Dev Test ready";
|
|
464
|
+
await emit({
|
|
465
|
+
role: "hook",
|
|
466
|
+
kind: "checkpoint",
|
|
467
|
+
content: progression,
|
|
468
|
+
eventType: "phase_gate"
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function completionPhaseFromArtifact(artifact) {
|
|
472
|
+
const phase = artifact.phase?.trim();
|
|
473
|
+
return phase === "self_review" || phase === "validation" || phase === "implementation"
|
|
474
|
+
? phase
|
|
475
|
+
: "implementation";
|
|
476
|
+
}
|
|
477
|
+
class LiveCompletionArtifactEmitter {
|
|
478
|
+
worktreePath;
|
|
479
|
+
workItemId;
|
|
480
|
+
emit;
|
|
481
|
+
phases;
|
|
482
|
+
emitted = new Set();
|
|
483
|
+
timer = null;
|
|
484
|
+
pending = Promise.resolve();
|
|
485
|
+
constructor(worktreePath, workItemId, phase, developCycle, emit) {
|
|
486
|
+
this.worktreePath = worktreePath;
|
|
487
|
+
this.workItemId = workItemId;
|
|
488
|
+
this.emit = emit;
|
|
489
|
+
this.phases = developCycle
|
|
490
|
+
? developCyclePhaseOrder.slice(developCyclePhaseOrder.indexOf(phase))
|
|
491
|
+
: [phase];
|
|
492
|
+
}
|
|
493
|
+
start() {
|
|
494
|
+
void this.pollQueued();
|
|
495
|
+
this.timer = setInterval(() => {
|
|
496
|
+
void this.pollQueued();
|
|
497
|
+
}, 250);
|
|
498
|
+
}
|
|
499
|
+
async stop() {
|
|
500
|
+
if (this.timer) {
|
|
501
|
+
clearInterval(this.timer);
|
|
502
|
+
this.timer = null;
|
|
503
|
+
}
|
|
504
|
+
await this.pollQueued();
|
|
505
|
+
}
|
|
506
|
+
hasEmitted(phase) {
|
|
507
|
+
return this.emitted.has(phase);
|
|
508
|
+
}
|
|
509
|
+
pollQueued() {
|
|
510
|
+
this.pending = this.pending
|
|
511
|
+
.then(() => this.poll())
|
|
512
|
+
.catch(() => undefined);
|
|
513
|
+
return this.pending;
|
|
514
|
+
}
|
|
515
|
+
async poll() {
|
|
516
|
+
for (const phase of this.phases) {
|
|
517
|
+
if (this.emitted.has(phase)) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const artifact = await readCompletionArtifact(this.worktreePath, this.workItemId, phase);
|
|
521
|
+
if (!artifact) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
this.emitted.add(phase);
|
|
525
|
+
await emitCompletionArtifactRecorded(this.emit, this.worktreePath, artifact);
|
|
526
|
+
await emitPhaseCompletionEvent(this.emit, artifact);
|
|
527
|
+
await emitPhaseGateEvents(this.emit, artifact);
|
|
528
|
+
if ((artifact.status ?? "").trim().toLowerCase() !== "pass") {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function readCompletionArtifactsForRun(worktreePath, workItemId, phase, developCycle) {
|
|
535
|
+
if (!developCycle) {
|
|
536
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, phase);
|
|
537
|
+
if (!artifact) {
|
|
538
|
+
throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
|
|
539
|
+
}
|
|
540
|
+
return [artifact];
|
|
541
|
+
}
|
|
542
|
+
const artifacts = [];
|
|
543
|
+
for (const expectedPhase of ["implementation", "self_review", "validation"]) {
|
|
544
|
+
const artifact = await readCompletionArtifact(worktreePath, workItemId, expectedPhase);
|
|
545
|
+
if (!artifact) {
|
|
546
|
+
if (artifacts.length === 0) {
|
|
547
|
+
throw new Error("Provider CLI completed but did not call the Forge implementation completion tool.");
|
|
548
|
+
}
|
|
549
|
+
const previous = artifacts[artifacts.length - 1];
|
|
550
|
+
if ((previous.status ?? "").trim().toLowerCase() === "pass") {
|
|
551
|
+
throw new Error(`Provider CLI completed after ${previous.phase} passed but did not call the Forge ${expectedPhase} completion tool.`);
|
|
552
|
+
}
|
|
553
|
+
return artifacts;
|
|
554
|
+
}
|
|
555
|
+
artifacts.push(artifact);
|
|
556
|
+
if ((artifact.status ?? "").trim().toLowerCase() !== "pass") {
|
|
557
|
+
return artifacts;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return artifacts;
|
|
561
|
+
}
|
|
562
|
+
function providerRedactionEnv(dispatch) {
|
|
563
|
+
const env = completionRedactionEnv(exactRedactionSecrets(dispatch));
|
|
564
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
565
|
+
}
|
|
566
|
+
function exactRedactionSecrets(dispatch) {
|
|
567
|
+
return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
|
|
568
|
+
}
|
|
569
|
+
async function emitProviderUsageEvent(emit, dispatch, outputState) {
|
|
570
|
+
const usage = outputState.usage;
|
|
571
|
+
if (hasUsage(usage)) {
|
|
572
|
+
await emit(buildProviderUsageEvent(dispatch, usage));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function estimatedDeterministicUsage() {
|
|
576
|
+
return addUsage(emptyUsage("estimated"), {
|
|
577
|
+
inputTokens: 1000,
|
|
578
|
+
outputTokens: 400,
|
|
579
|
+
source: "estimated"
|
|
580
|
+
});
|
|
581
|
+
}
|
|
337
582
|
export class StreamingProviderProcessRunner {
|
|
338
583
|
async run(request, handlers = {}, signal) {
|
|
339
584
|
const launch = await resolveProviderProcessLaunch(request.command, request.args);
|
|
340
585
|
return await new Promise((resolve, reject) => {
|
|
341
586
|
const child = spawn(launch.command, launch.args, {
|
|
342
587
|
cwd: request.cwd,
|
|
588
|
+
env: request.env ? { ...process.env, ...request.env } : undefined,
|
|
343
589
|
stdio: ["ignore", "pipe", "pipe"],
|
|
344
590
|
windowsHide: true
|
|
345
591
|
});
|
|
@@ -551,6 +797,19 @@ ${completionPromptForPhase(phase)}
|
|
|
551
797
|
|
|
552
798
|
Finish with a concise summary of changed files and verification after calling the completion tool.`;
|
|
553
799
|
}
|
|
800
|
+
function buildDevelopCyclePrompt(dispatch, phase = completionPhaseFromDispatch(dispatch)) {
|
|
801
|
+
return `You are Forge's develop-cycle runner running locally in a prepared git worktree.
|
|
802
|
+
|
|
803
|
+
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.
|
|
804
|
+
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.
|
|
805
|
+
Use the repository's existing conventions. Run relevant tests when practical.
|
|
806
|
+
Forge owns protocol files: worktree prep, injected .forge commands/hooks/skills, phase verdict artifacts, reports, and commits.
|
|
807
|
+
Read Forge-provided context under .forge when needed, but do not edit .forge files or create Forge verdict/report artifacts by hand.
|
|
808
|
+
|
|
809
|
+
${completionPromptForDevelopCycle(phase)}
|
|
810
|
+
|
|
811
|
+
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.`;
|
|
812
|
+
}
|
|
554
813
|
function phaseWorkInstruction(phase) {
|
|
555
814
|
if (phase === "self_review") {
|
|
556
815
|
return "Review the implementation diff, apply safe fixes when needed, and record remaining risks explicitly.";
|
|
@@ -626,9 +885,13 @@ class CursorStreamJsonState {
|
|
|
626
885
|
sessionId = null;
|
|
627
886
|
finalMessage = null;
|
|
628
887
|
emittedSession = false;
|
|
888
|
+
accumulatedUsage = null;
|
|
629
889
|
get sessionLabel() {
|
|
630
890
|
return this.sessionId ? `session ${this.sessionId}` : null;
|
|
631
891
|
}
|
|
892
|
+
get usage() {
|
|
893
|
+
return this.accumulatedUsage;
|
|
894
|
+
}
|
|
632
895
|
consume(line) {
|
|
633
896
|
const event = parseJsonLine(line);
|
|
634
897
|
if (!event) {
|
|
@@ -690,6 +953,7 @@ class CursorStreamJsonState {
|
|
|
690
953
|
if (result) {
|
|
691
954
|
this.finalMessage = clip(result, 4000);
|
|
692
955
|
}
|
|
956
|
+
this.accumulateUsage(event);
|
|
693
957
|
}
|
|
694
958
|
if (type === "error" || (type === "result" && event.is_error === true)) {
|
|
695
959
|
return {
|
|
@@ -701,6 +965,21 @@ class CursorStreamJsonState {
|
|
|
701
965
|
}
|
|
702
966
|
return null;
|
|
703
967
|
}
|
|
968
|
+
accumulateUsage(event) {
|
|
969
|
+
const usage = objectField(event, "usage");
|
|
970
|
+
const durationMs = readUsageNumber(event, "duration_ms") || readUsageNumber(event, "duration_api_ms");
|
|
971
|
+
if (Object.keys(usage).length === 0 && durationMs === 0) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
|
|
975
|
+
inputTokens: readUsageNumber(usage, "inputTokens"),
|
|
976
|
+
outputTokens: readUsageNumber(usage, "outputTokens"),
|
|
977
|
+
cacheReadTokens: readUsageNumber(usage, "cacheReadTokens"),
|
|
978
|
+
cacheCreationTokens: readUsageNumber(usage, "cacheWriteTokens"),
|
|
979
|
+
providerDurationMs: durationMs > 0 ? durationMs : null,
|
|
980
|
+
source: "provider_reported"
|
|
981
|
+
});
|
|
982
|
+
}
|
|
704
983
|
}
|
|
705
984
|
function formatCursorToolCallStarted(toolCall) {
|
|
706
985
|
const readArgs = objectField(objectField(toolCall, "readToolCall"), "args");
|
|
@@ -805,9 +1084,13 @@ class CodexJsonState {
|
|
|
805
1084
|
threadId = null;
|
|
806
1085
|
finalMessage = null;
|
|
807
1086
|
emittedThread = false;
|
|
1087
|
+
accumulatedUsage = null;
|
|
808
1088
|
get sessionLabel() {
|
|
809
1089
|
return this.threadId ? `thread ${this.threadId}` : null;
|
|
810
1090
|
}
|
|
1091
|
+
get usage() {
|
|
1092
|
+
return this.accumulatedUsage;
|
|
1093
|
+
}
|
|
811
1094
|
consume(line) {
|
|
812
1095
|
const event = parseJsonLine(line);
|
|
813
1096
|
if (!event) {
|
|
@@ -827,6 +1110,7 @@ class CodexJsonState {
|
|
|
827
1110
|
}
|
|
828
1111
|
}
|
|
829
1112
|
if (type === "turn.completed") {
|
|
1113
|
+
this.accumulateUsage(event);
|
|
830
1114
|
return {
|
|
831
1115
|
role: "runner",
|
|
832
1116
|
kind: "checkpoint",
|
|
@@ -872,6 +1156,19 @@ class CodexJsonState {
|
|
|
872
1156
|
}
|
|
873
1157
|
return null;
|
|
874
1158
|
}
|
|
1159
|
+
accumulateUsage(event) {
|
|
1160
|
+
const usage = objectField(event, "usage");
|
|
1161
|
+
if (Object.keys(usage).length === 0) {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
this.accumulatedUsage = addUsage(this.accumulatedUsage ?? emptyUsage("provider_reported"), {
|
|
1165
|
+
inputTokens: readUsageNumber(usage, "input_tokens"),
|
|
1166
|
+
outputTokens: readUsageNumber(usage, "output_tokens"),
|
|
1167
|
+
cacheReadTokens: readUsageNumber(usage, "cached_input_tokens"),
|
|
1168
|
+
reasoningOutputTokens: readUsageNumber(usage, "reasoning_output_tokens"),
|
|
1169
|
+
source: "provider_reported"
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
875
1172
|
}
|
|
876
1173
|
function objectField(source, key) {
|
|
877
1174
|
const value = source[key];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RunOnceDispatchPacket, RunnerDispatchEvent } from "../session.types.js";
|
|
2
|
+
export type UsageSource = "provider_reported" | "cli_reported" | "estimated" | "unknown";
|
|
3
|
+
/**
|
|
4
|
+
* Normalized, provider-agnostic token usage accumulated for a dispatch. Cost is never computed
|
|
5
|
+
* here; the API recomputes cost from these tokens x model rates (ADR-0006).
|
|
6
|
+
*/
|
|
7
|
+
export interface ProviderUsage {
|
|
8
|
+
readonly inputTokens: number;
|
|
9
|
+
readonly outputTokens: number;
|
|
10
|
+
readonly cacheReadTokens: number;
|
|
11
|
+
readonly cacheCreationTokens: number;
|
|
12
|
+
readonly reasoningOutputTokens: number;
|
|
13
|
+
readonly providerDurationMs: number | null;
|
|
14
|
+
readonly source: UsageSource;
|
|
15
|
+
}
|
|
16
|
+
export declare function emptyUsage(source?: UsageSource): ProviderUsage;
|
|
17
|
+
export declare function addUsage(base: ProviderUsage, next: Partial<ProviderUsage>): ProviderUsage;
|
|
18
|
+
export declare function totalTokens(usage: ProviderUsage): number;
|
|
19
|
+
export declare function hasUsage(usage: ProviderUsage | null): usage is ProviderUsage;
|
|
20
|
+
/**
|
|
21
|
+
* Builds the durable `provider_usage` event. Token numbers travel as a structured `data` JSON
|
|
22
|
+
* string (snake_case) that the API parses into a usage_event row.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildProviderUsageEvent(dispatch: RunOnceDispatchPacket, usage: ProviderUsage): RunnerDispatchEvent;
|
|
25
|
+
export declare function readUsageNumber(source: Record<string, unknown>, key: string): number;
|