diff --git a/src/workflow.ts b/src/workflow.ts index 2adfc03b..c067cd3d 100644 --- a/src/workflow.ts +++ b/src/workflow.ts @@ -293,7 +293,7 @@ async function executeIssue( await linear.markStage(state.issue.id, "implementing"); await linear.comment( state.issue.id, - buildPlanComment(state.issue.key, state.planSummary), + buildPlanComment(state.issue.key, state.planSummary, result.usage), ); logger.info(buildIssueJobLogFields(state, "planning"), "Plan completed"); } @@ -335,7 +335,10 @@ async function executeIssue( await linear.applyStageLabel(state.issue.id, "pr_created"); await linear.comment( state.issue.id, - `Implementation completed. Draft PR: ${state.pullRequest.url ?? "(created)"}`, + [ + `Implementation completed. Draft PR: ${state.pullRequest.url ?? "(created)"}`, + formatCodexUsageLine(result.usage), + ].join("\n"), ); logger.info( buildIssueJobLogFields(state, "implementing"), @@ -376,6 +379,8 @@ async function executeIssue( "", `Result: ${outcome.passed ? "PASS" : "FAIL"}`, "", + formatCodexUsageLine(review.usage), + "", outcome.summary, "", outcome.bugs.length > 0 @@ -457,6 +462,11 @@ export interface ReviewOutcome { export function buildPlanComment( issueKey: string, planSummary: string, + usage?: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + }, ): string { const maxSummaryLength = 6000; const normalized = planSummary.trim(); @@ -470,11 +480,32 @@ export function buildPlanComment( "", "Planning completed; implementation started.", "", + formatCodexUsageLine(usage), + "", "Plan:", truncated || "(No plan summary returned by planning agent.)", ].join("\n"); } +export function formatCodexUsageLine(usage?: { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; +}): string { + if (!usage) { + return "Token usage: unknown"; + } + const input = usage.inputTokens ?? "unknown"; + const output = usage.outputTokens ?? "unknown"; + const total = + usage.totalTokens ?? + (typeof usage.inputTokens === "number" && + typeof usage.outputTokens === "number" + ? usage.inputTokens + usage.outputTokens + : "unknown"); + return `Token usage: input ${input}, output ${output}, total ${total}`; +} + export function parseReviewOutcome(text: string): ReviewOutcome { const upper = text.toUpperCase(); const passed = diff --git a/tests/workflow.test.ts b/tests/workflow.test.ts index e2409da7..dc5f9ef2 100644 --- a/tests/workflow.test.ts +++ b/tests/workflow.test.ts @@ -4,6 +4,7 @@ import { appendCodexUsage, buildIssueJobLogFields, buildPlanComment, + formatCodexUsageLine, parseReviewOutcome, resolvePollingSettings, shouldStopPolling, @@ -38,15 +39,48 @@ BUGS_JSON: describe("buildPlanComment", () => { it("includes header and plan summary", () => { - const comment = buildPlanComment("ENG-1", "1. Do A\n2. Do B"); + const comment = buildPlanComment("ENG-1", "1. Do A\n2. Do B", { + inputTokens: 12, + outputTokens: 8, + }); expect(comment).toContain("PIV loop plan for ENG-1"); expect(comment).toContain("Planning completed; implementation started."); + expect(comment).toContain("Token usage: input 12, output 8, total 20"); expect(comment).toContain("1. Do A"); }); it("uses fallback when no summary is returned", () => { const comment = buildPlanComment("ENG-1", " "); expect(comment).toContain("(No plan summary returned by planning agent.)"); + expect(comment).toContain("Token usage: unknown"); + }); +}); + +describe("formatCodexUsageLine", () => { + it("formats full usage values", () => { + expect( + formatCodexUsageLine({ + inputTokens: 3, + outputTokens: 7, + totalTokens: 10, + }), + ).toBe("Token usage: input 3, output 7, total 10"); + }); + + it("derives total when missing", () => { + expect( + formatCodexUsageLine({ + inputTokens: 9, + outputTokens: 4, + }), + ).toBe("Token usage: input 9, output 4, total 13"); + }); + + it("handles missing fields", () => { + expect(formatCodexUsageLine({ inputTokens: 5 })).toBe( + "Token usage: input 5, output unknown, total unknown", + ); + expect(formatCodexUsageLine()).toBe("Token usage: unknown"); }); });