diff --git a/apps/cli/src/commands/gain.ts b/apps/cli/src/commands/gain.ts index 5e8e0c7..3864a6e 100644 --- a/apps/cli/src/commands/gain.ts +++ b/apps/cli/src/commands/gain.ts @@ -28,6 +28,7 @@ interface GainOptions { sessionLimit?: string; inputCostPer1m?: string; outputCostPer1m?: string; + cost?: boolean; reference?: boolean; honest?: boolean; recentHours?: string; @@ -105,6 +106,7 @@ export function registerGainCommand(program: Command): void { '--output-cost-per-1m ', 'USD rate per 1M output tokens; env COLONY_MCP_OUTPUT_USD_PER_1M', ) + .option('--no-cost', 'suppress USD cost output even when cost rates are configured') .action(async (opts: GainOptions) => { const settings = loadSettings(); const hoursArg = opts.hours ? Number(opts.hours) : undefined; @@ -133,12 +135,13 @@ export function registerGainCommand(program: Command): void { settings, (storage) => { const sessionLimit = parseSessionLimit(opts.sessionLimit); + const cost = costOptionsFromCli(opts); const fullAgg = storage.aggregateMcpMetrics({ since, until: now, ...(opts.operation !== undefined ? { operation: opts.operation } : {}), ...(sessionLimit !== undefined ? { sessionLimit } : {}), - cost: costOptionsFromCli(opts), + cost, }); const recentAgg = moversEnabled && recentSince !== null && recentSince > since @@ -146,7 +149,7 @@ export function registerGainCommand(program: Command): void { since: recentSince, until: now, ...(opts.operation !== undefined ? { operation: opts.operation } : {}), - cost: costOptionsFromCli(opts), + cost, }) : null; const dailyRows = summaryRequested @@ -222,6 +225,7 @@ export function registerGainCommand(program: Command): void { comparison, windowHours, operationFilter: opts.operation, + costBasis: live.cost_basis, days: dailyDays, topOps: topOpsLimit, showGraph: opts.daily !== true || opts.graph === true, @@ -1016,6 +1020,7 @@ function costOptionsFromCli(opts: GainOptions): { input_usd_per_1m_tokens?: number; output_usd_per_1m_tokens?: number; } { + if (opts.cost === false) return {}; const inputRate = parseCostRate(opts.inputCostPer1m, process.env.COLONY_MCP_INPUT_USD_PER_1M); const outputRate = parseCostRate(opts.outputCostPer1m, process.env.COLONY_MCP_OUTPUT_USD_PER_1M); return { @@ -1244,6 +1249,7 @@ export interface SummaryReportInput { comparison: SavingsLiveComparison; windowHours: number; operationFilter: string | undefined; + costBasis: McpMetricsCostBasis; days: number; topOps: number; showGraph: boolean; @@ -1275,6 +1281,7 @@ export function writeSummaryReport(input: SummaryReportInput): void { comparison, windowHours, operationFilter, + costBasis, days, topOps, showGraph, @@ -1286,7 +1293,7 @@ export function writeSummaryReport(input: SummaryReportInput): void { const filter = operationFilter ? ` (op=${operationFilter})` : ''; w.write(`${kleur.bold(`Colony Token Savings (last ${formatHoursLabel(windowHours)}${filter})`)}\n`); w.write(`${HEAVY_RULE}\n`); - writeSummaryHeadline(totals, comparison); + writeSummaryHeadline(totals, comparison, costBasis); if (totals.calls === 0) { w.write( @@ -1304,7 +1311,7 @@ export function writeSummaryReport(input: SummaryReportInput): void { ); } w.write('\n'); - writeSummaryByOperation(operations, comparison, topOps); + writeSummaryByOperation(operations, comparison, costBasis, topOps); } if (showGraph) { @@ -1321,6 +1328,7 @@ export function writeSummaryReport(input: SummaryReportInput): void { function writeSummaryHeadline( totals: McpMetricsAggregateRow, comparison: SavingsLiveComparison, + costBasis: McpMetricsCostBasis, ): void { const w = process.stdout; const calls = totals.calls; @@ -1343,6 +1351,12 @@ function writeSummaryHeadline( ['Output tokens:', formatTokens(outputTokens)], ['Total tokens:', formatTokens(totalTokens)], ]; + if (costBasis.configured) { + lines.push( + ['Total cost:', formatUsdConfigured(totals.total_cost_usd)], + ['Avg cost/call:', formatUsdConfigured(totals.avg_cost_usd)], + ); + } if (savingsPct !== null) { const savedLabel = savedTokens >= 0 ? `${formatTokens(savedTokens)} (${formatPctSigned(savingsPct)})` @@ -1381,6 +1395,7 @@ function writeSummaryHeadline( function writeSummaryByOperation( operations: ReadonlyArray, comparison: SavingsLiveComparison, + costBasis: McpMetricsCostBasis, topOps: number, ): void { const w = process.stdout; @@ -1426,6 +1441,7 @@ function writeSummaryByOperation( w.write(`${kleur.bold('By Operation')}\n`); w.write(`${kleur.dim('-'.repeat(SUMMARY_TABLE_WIDTH))}\n`); + const showCost = costBasis.configured; const header = [ padVisible(' #', 3), padVisible('Operation', 26), @@ -1433,6 +1449,7 @@ function writeSummaryByOperation( padVisibleRight('Saved', 8), padVisibleRight('Share', 6), padVisibleRight('Avg ms', 7), + ...(showCost ? [padVisibleRight('Cost', 11)] : []), padVisible('Impact', SUMMARY_IMPACT_WIDTH), ].join(' '); w.write(`${kleur.dim(header)}\n`); @@ -1456,6 +1473,7 @@ function writeSummaryByOperation( padVisibleRight(savedCell, 8), padVisibleRight(formatPctValue(sharePct), 6), padVisibleRight(String(row.avg_duration_ms), 7), + ...(showCost ? [padVisibleRight(formatUsdConfigured(row.total_cost_usd), 11)] : []), renderImpactBar(weight, maxImpact, SUMMARY_IMPACT_WIDTH), ]; w.write(`${cells.join(' ')}\n`); diff --git a/apps/cli/test/gain.test.ts b/apps/cli/test/gain.test.ts index 1a09ad3..e639134 100644 --- a/apps/cli/test/gain.test.ts +++ b/apps/cli/test/gain.test.ts @@ -1,15 +1,18 @@ import { SAVINGS_REFERENCE_ROWS, savingsLiveComparison, savingsReferenceTotals } from '@colony/core'; import type { McpMetricsAggregateRow, + McpMetricsCostBasis, McpMetricsDailyRow, McpMetricsSessionAggregateRow, McpMetricsSessionSummary, } from '@colony/storage'; +import { Command } from 'commander'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { buildMoversReport, fillDailyWindow, formatDurationMs, + registerGainCommand, renderImpactBar, writeGainReport, writeLiveSection, @@ -29,7 +32,13 @@ const COST_BASIS = { input_usd_per_1m_tokens: 1, output_usd_per_1m_tokens: 2, configured: true, -}; +} satisfies McpMetricsCostBasis; + +const DISABLED_COST_BASIS = { + input_usd_per_1m_tokens: 0, + output_usd_per_1m_tokens: 0, + configured: false, +} satisfies McpMetricsCostBasis; const SESSION_SUMMARY: McpMetricsSessionSummary = { session_count: 1, @@ -540,6 +549,15 @@ describe('gain command output', () => { expect(output).not.toContain('Cuts: re-reading PR threads + scrollback'); }); + it('registers the --no-cost escape hatch', () => { + const program = new Command(); + + registerGainCommand(program); + + const gain = program.commands.find((command) => command.name() === 'gain'); + expect(gain?.helpInformation()).toContain('--no-cost'); + }); + it('includes the expanded reference operation catalog', () => { const operations = SAVINGS_REFERENCE_ROWS.map((row) => row.operation); expect(operations).toEqual( @@ -873,6 +891,10 @@ describe('rtk-style summary helpers', () => { input_tokens: 80, output_tokens: 400, total_tokens: 480, + input_cost_usd: 0.00008, + output_cost_usd: 0.0008, + total_cost_usd: 0.00088, + avg_cost_usd: 0.00022, avg_input_tokens: 20, avg_output_tokens: 100, total_duration_ms: 800, @@ -887,6 +909,10 @@ describe('rtk-style summary helpers', () => { input_tokens: 60, output_tokens: 60, total_tokens: 120, + input_cost_usd: 0.00006, + output_cost_usd: 0.00012, + total_cost_usd: 0.00018, + avg_cost_usd: 0.00009, avg_input_tokens: 30, avg_output_tokens: 30, total_duration_ms: 400, @@ -902,6 +928,10 @@ describe('rtk-style summary helpers', () => { input_tokens: 140, output_tokens: 460, total_tokens: 600, + input_cost_usd: 0.00014, + output_cost_usd: 0.00092, + total_cost_usd: 0.00106, + avg_cost_usd: 0.000177, total_duration_ms: 1_200, avg_duration_ms: 200, last_ts: recentTs, @@ -925,6 +955,7 @@ describe('rtk-style summary helpers', () => { comparison, windowHours: 24, operationFilter: undefined, + costBasis: COST_BASIS, days: 3, topOps: 10, showGraph: true, @@ -937,9 +968,13 @@ describe('rtk-style summary helpers', () => { expect(output).toContain('6'); expect(output).toContain('Input tokens:'); expect(output).toContain('Output tokens:'); + expect(output).toContain('Total cost:'); + expect(output).toContain('Avg cost/call:'); + expect(output).toContain('$0.001060'); expect(output).toContain('Total exec time:'); expect(output).toContain('Efficiency meter:'); expect(output).toContain('By Operation'); + expect(output).toContain('Cost'); expect(output).toContain('search'); expect(output).toContain('Daily Activity (last 3 days)'); expect(output).toContain('Daily Breakdown'); @@ -991,6 +1026,7 @@ describe('rtk-style summary helpers', () => { comparison: savingsLiveComparison([], SAVINGS_REFERENCE_ROWS), windowHours: 168, operationFilter: undefined, + costBasis: DISABLED_COST_BASIS, days: 2, topOps: 10, showGraph: true, @@ -1048,6 +1084,7 @@ describe('rtk-style summary helpers', () => { comparison: savingsLiveComparison([], SAVINGS_REFERENCE_ROWS), windowHours: 24, operationFilter: undefined, + costBasis: DISABLED_COST_BASIS, days: 7, topOps: 10, showGraph: true, @@ -1056,5 +1093,7 @@ describe('rtk-style summary helpers', () => { }); expect(output).toContain('No mcp_metrics receipts in window'); + expect(output).not.toContain('Total cost:'); + expect(output).not.toContain('Avg cost/call:'); }); }); diff --git a/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/.openspec.yaml b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/.openspec.yaml new file mode 100644 index 0000000..9f70866 --- /dev/null +++ b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-15 diff --git a/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/proposal.md b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/proposal.md new file mode 100644 index 0000000..255bb04 --- /dev/null +++ b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/proposal.md @@ -0,0 +1,24 @@ +## Why + +Operators configure `COLONY_MCP_INPUT_USD_PER_1M` and +`COLONY_MCP_OUTPUT_USD_PER_1M` so Colony can show live spend. The regular +`colony gain` table already carries cost fields, but the compact summary view +only reported token volume and savings. This makes cost-aware runs surface USD +by default across the gain views while keeping an explicit opt-out for token-only +output. + +## What Changes + +- Add `colony gain --no-cost` as the escape hatch for suppressing USD cost + output even when CLI flags or environment rates are configured. +- Thread the active cost basis into `colony gain --summary`. +- Show total and average USD cost in the summary headline, plus a Cost column in + the summary By Operation table, when rates are configured. + +## Impact + +- Affected surface: `apps/cli/src/commands/gain.ts`. +- Existing token-only behavior is preserved when no rates are configured, or + when `--no-cost` is used. +- JSON keeps using the aggregate cost basis returned by storage; `--no-cost` + sends no cost rates, so JSON payloads report `configured: false`. diff --git a/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/specs/usd-by-default-in-colony-gain/spec.md b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/specs/usd-by-default-in-colony-gain/spec.md new file mode 100644 index 0000000..080cff1 --- /dev/null +++ b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/specs/usd-by-default-in-colony-gain/spec.md @@ -0,0 +1,17 @@ +## ADDED Requirements + +### Requirement: `colony gain` renders configured USD costs by default +The system SHALL render USD cost information in `colony gain` default live +table and compact summary output when input/output USD-per-1M-token rates are +configured through CLI flags or environment variables, without requiring an +additional flag. + +#### Scenario: Summary view includes configured USD cost +- **WHEN** `colony gain --summary` runs with configured input and output cost rates +- **THEN** the headline includes total and average USD cost +- **AND** the By Operation table includes per-operation USD cost. + +#### Scenario: No-cost escape hatch suppresses configured USD cost +- **WHEN** `colony gain --no-cost` runs while cost rates are configured +- **THEN** the command does not pass cost rates into the metrics aggregate +- **AND** rendered output follows the token-only, unconfigured-cost path. diff --git a/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/tasks.md b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/tasks.md new file mode 100644 index 0000000..d79cd73 --- /dev/null +++ b/openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/tasks.md @@ -0,0 +1,36 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## Handoff + +- Handoff: change=`agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02`; branch=`agent/codex/usd-by-default-in-colony-gain-2026-05-15-16-02`; scope=`apps/cli/src/commands/gain.ts`; action=`finish PR handoff after verification`. +- Copy prompt: Continue `agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02` on branch `agent/codex/usd-by-default-in-colony-gain-2026-05-15-16-02`. Work inside the existing sandbox, review `openspec/changes/agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/usd-by-default-in-colony-gain-2026-05-15-16-02 --base main --via-pr --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02`. +- [x] 1.2 Define normative requirements in `specs/usd-by-default-in-colony-gain/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. + - `pnpm --filter colonyq test -- gain.test.ts` + - `pnpm --filter colonyq typecheck` +- [x] 3.2 Run `openspec validate agent-codex-usd-by-default-in-colony-gain-2026-05-15-16-02 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).