Skip to content

Commit 08482b1

Browse files
NagyViktNagyVikt
andauthored
feat(cli): flag dominant operations and clean labels in colony gain (#540)
Top spend now reports its share of total tokens, and a `Hot loop:` callout fires when a single op holds >=70% of token spend across >=100 calls (e.g. `task_plan_list` at 98%). The `Saved:` / `USD saved:` prefix labels are renamed to `Net:` / `Net USD:` so the line no longer reads "Saved: X saved". The live sessions header drops its trailing `, -` when cost isn't configured. Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 489a9ff commit 08482b1

5 files changed

Lines changed: 163 additions & 8 deletions

File tree

.changeset/spotty-spoons-pick.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'colonyq': patch
3+
---
4+
5+
Surface hot-loop dominance and drop double-"saved" labels in `colony gain`.
6+
Top spend now reports the operation's share of total tokens, and a `Hot loop:`
7+
callout fires when one operation owns ≥70% of token spend across ≥100 calls.
8+
The "Saved:" / "USD saved:" labels are renamed to "Net:" / "Net USD:" so the
9+
phrase no longer reads "Saved: X saved", and the live sessions header drops the
10+
trailing `, -` when cost isn't configured.

apps/cli/src/commands/gain.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -445,12 +445,21 @@ function writeLiveOverview(
445445
}
446446
const topSpend = findTopTokenSpend(rows);
447447
if (topSpend !== null) {
448+
const share = totals.total_tokens > 0 ? topSpend.total_tokens / totals.total_tokens : 0;
449+
const shareLabel = formatShare(share);
448450
w.write(
449451
`${kleur.dim('Top spend:')} ${topSpend.operation} ${formatTokens(
450452
topSpend.total_tokens,
451-
)} tokens across ${topSpend.calls} call${topSpend.calls === 1 ? '' : 's'} ` +
452-
`(avg ${formatTokens(avgTokens(topSpend))}/call)\n`,
453+
)} tokens (${shareLabel} of total) across ${topSpend.calls} call${
454+
topSpend.calls === 1 ? '' : 's'
455+
} (avg ${formatTokens(avgTokens(topSpend))}/call)\n`,
453456
);
457+
if (share >= 0.7 && topSpend.calls >= 100) {
458+
w.write(
459+
`${kleur.yellow('Hot loop:')} ${topSpend.operation} dominates token spend ` +
460+
`(${shareLabel}); narrow filters, raise compact mode, or cache the result.\n`,
461+
);
462+
}
454463
}
455464
}
456465

@@ -464,11 +473,14 @@ function writeLiveSessionSection(
464473
w.write('\n');
465474
w.write(`${kleur.bold('Live sessions')}\n`);
466475
const truncation = summary.sessions_truncated ? `; showing ${sessions.length}` : '';
476+
const costSuffix = costBasis.configured
477+
? `, ${formatUsdConfigured(summary.avg_total_cost_usd)}`
478+
: '';
467479
w.write(
468480
kleur.dim(
469481
`Sessions with receipts: ${summary.session_count}${truncation}; avg/session: ${summary.avg_calls} calls, ${formatTokens(
470482
summary.avg_total_tokens,
471-
)} tokens, ${formatUsd(summary.avg_total_cost_usd, costBasis)}.\n`,
483+
)} tokens${costSuffix}.\n`,
472484
),
473485
);
474486
const head = padRow(
@@ -585,7 +597,7 @@ function writeGainFocus(
585597
matchedCalls,
586598
totalCalls,
587599
)})`,
588-
`${kleur.dim('Saved:')} ${formatTokenDelta(savedTokens)}`,
600+
`${kleur.dim('Net:')} ${formatTokenDelta(savedTokens)}`,
589601
`${kleur.dim('Next:')} ${next}`,
590602
].join(' '),
591603
);
@@ -600,7 +612,7 @@ function writeGainFocus(
600612
if (comparisonCost !== null && comparisonCost.totals.calls > 0) {
601613
process.stdout.write(
602614
[
603-
`${kleur.dim('USD saved:')} ${formatUsdDelta(comparisonCost.totals.saved_cost_usd)}`,
615+
`${kleur.dim('Net USD:')} ${formatUsdDelta(comparisonCost.totals.saved_cost_usd)}`,
604616
`${kleur.dim('Colony spent:')} ${formatUsdConfigured(
605617
comparisonCost.totals.colony_cost_usd,
606618
)}`,
@@ -737,6 +749,13 @@ function formatPercent(part: number, whole: number): string {
737749
return `${value.toFixed(1)}%`;
738750
}
739751

752+
function formatShare(ratio: number): string {
753+
if (!Number.isFinite(ratio) || ratio <= 0) return '0%';
754+
const pct = ratio * 100;
755+
if (pct >= 10) return `${Math.round(pct)}%`;
756+
return `${pct.toFixed(1)}%`;
757+
}
758+
740759
function avgTokens(row: Pick<McpMetricsAggregateRow, 'calls' | 'total_tokens'>): number {
741760
return row.calls <= 0 ? 0 : Math.round(row.total_tokens / row.calls);
742761
}

apps/cli/test/gain.test.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ describe('gain command output', () => {
107107
expect(output).toContain('Errors: 1 (50%)');
108108
expect(output).toContain('Cost total: $0.000125');
109109
expect(output).toContain('Needs attention: 1x search TASK_NOT_FOUND - task 6 not found');
110-
expect(output).toContain('Top spend: search 75 tokens across 2 calls');
110+
expect(output).toContain('Top spend: search 75 tokens (100% of total) across 2 calls');
111111
expect(output).toContain('Operations');
112112
expect(output).toContain('OK');
113113
expect(output).toContain('Tok total');
@@ -252,6 +252,104 @@ describe('gain command output', () => {
252252
);
253253
});
254254

255+
it('flags a hot loop when one operation dominates token spend at high call volume', () => {
256+
let output = '';
257+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => {
258+
output += String(chunk);
259+
return true;
260+
});
261+
262+
const dominantRow: McpMetricsAggregateRow = {
263+
operation: 'task_plan_list',
264+
calls: 7597,
265+
ok_count: 7597,
266+
error_count: 0,
267+
error_reasons: [],
268+
...METRIC_DETAIL,
269+
input_bytes: 0,
270+
output_bytes: 0,
271+
total_bytes: 0,
272+
input_tokens: 92_800,
273+
output_tokens: 34_480_000,
274+
total_tokens: 34_580_000,
275+
input_cost_usd: 0,
276+
output_cost_usd: 0,
277+
total_cost_usd: 0,
278+
avg_cost_usd: 0,
279+
avg_input_tokens: 12,
280+
avg_output_tokens: 4_500,
281+
total_duration_ms: 7597 * 55,
282+
avg_duration_ms: 55,
283+
last_ts: Date.now(),
284+
};
285+
const totals: McpMetricsAggregateRow = {
286+
...dominantRow,
287+
operation: '',
288+
total_tokens: 35_260_000,
289+
input_tokens: 171_500,
290+
output_tokens: 35_080_000,
291+
};
292+
293+
writeLiveSection(
294+
[dominantRow],
295+
totals,
296+
SESSION_SUMMARY,
297+
[SESSION_ROW],
298+
{ input_usd_per_1m_tokens: 0, output_usd_per_1m_tokens: 0, configured: false },
299+
168,
300+
undefined,
301+
);
302+
303+
expect(output).toContain('Top spend: task_plan_list 34.58M tokens (98% of total)');
304+
expect(output).toContain('Hot loop:');
305+
expect(output).toContain('task_plan_list dominates token spend');
306+
});
307+
308+
it('omits cost suffix from live sessions header when cost is not configured', () => {
309+
let output = '';
310+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => {
311+
output += String(chunk);
312+
return true;
313+
});
314+
315+
const row: McpMetricsAggregateRow = {
316+
operation: 'search',
317+
calls: 1,
318+
ok_count: 1,
319+
error_count: 0,
320+
error_reasons: [],
321+
...METRIC_DETAIL,
322+
input_bytes: 0,
323+
output_bytes: 0,
324+
total_bytes: 0,
325+
input_tokens: 25,
326+
output_tokens: 50,
327+
total_tokens: 75,
328+
input_cost_usd: 0,
329+
output_cost_usd: 0,
330+
total_cost_usd: 0,
331+
avg_cost_usd: 0,
332+
avg_input_tokens: 25,
333+
avg_output_tokens: 50,
334+
total_duration_ms: 40,
335+
avg_duration_ms: 40,
336+
last_ts: Date.now(),
337+
};
338+
339+
writeLiveSection(
340+
[row],
341+
row,
342+
SESSION_SUMMARY,
343+
[SESSION_ROW],
344+
{ input_usd_per_1m_tokens: 0, output_usd_per_1m_tokens: 0, configured: false },
345+
168,
346+
undefined,
347+
);
348+
349+
expect(output).toContain('Sessions with receipts: 1');
350+
expect(output).not.toContain('tokens, -.');
351+
});
352+
255353
it('prints live metrics before the live comparison model', () => {
256354
let output = '';
257355
vi.spyOn(process.stdout, 'write').mockImplementation((chunk: string | Uint8Array) => {
@@ -315,9 +413,9 @@ describe('gain command output', () => {
315413
expect(output).toContain('Search result shape');
316414
expect(output).toContain('Gain focus');
317415
expect(output).toContain('Coverage: 1 / 1 live calls (100%)');
318-
expect(output).toContain('Saved: 4.9k saved');
416+
expect(output).toContain('Net: 4.9k saved');
319417
expect(output).toContain('Top saving: Search result shape 4.9k saved across 1 call');
320-
expect(output).toContain('USD saved: $0.008208 saved');
418+
expect(output).toContain('Net USD: $0.008208 saved');
321419
expect(output).toContain('Colony spent: $0.000125');
322420
expect(output).toContain('Standard est: $0.008333');
323421
expect(output).toContain('Live matched total');
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-14
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# agent-claude-fix-gain-output-double-word-and-add-hot-2026-05-14-13-40 (minimal / T1)
2+
3+
Branch: `agent/claude/fix-gain-output-double-word-and-add-hot-2026-05-14-13-40`
4+
5+
Tighten `colony gain` so dominance jumps off the page and the labels stop reading like
6+
duplicated verbs.
7+
8+
- Top spend line now shows `(N% of total)` so an operation owning 98% of token spend
9+
is impossible to miss when scanning.
10+
- New `Hot loop:` callout fires when a single op holds ≥70% of token spend across
11+
≥100 calls, with a one-line nudge (narrow filters / compact mode / cache result).
12+
- `Saved:` / `USD saved:` prefix labels renamed to `Net:` / `Net USD:` to drop the
13+
"Saved: X saved" double-word; `formatTokenDelta` / `formatUsdDelta` still emit the
14+
"X saved" / "X over" phrase verbatim so the `Top saving:` sentence reads naturally.
15+
- Live sessions header trims its trailing `, -` when cost isn't configured.
16+
17+
## Handoff
18+
19+
- Handoff: change=`agent-claude-fix-gain-output-double-word-and-add-hot-2026-05-14-13-40`; branch=`agent/<your-name>/<branch-slug>`; scope=`TODO`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`.
20+
- Copy prompt: Continue `agent-claude-fix-gain-output-double-word-and-add-hot-2026-05-14-13-40` on branch `agent/<your-name>/<branch-slug>`. Work inside the existing sandbox, review `openspec/changes/agent-claude-fix-gain-output-double-word-and-add-hot-2026-05-14-13-40/notes.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/<your-name>/<branch-slug> --base dev --via-pr --wait-for-merge --cleanup`.
21+
22+
## Cleanup
23+
24+
- [ ] Run: `gx branch finish --branch agent/<your-name>/<branch-slug> --base dev --via-pr --wait-for-merge --cleanup`
25+
- [ ] Record PR URL + `MERGED` state in the completion handoff.
26+
- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`).

0 commit comments

Comments
 (0)