Summary
burn by-tool is structurally a summary mode — same query path, same filters, different aggregator and output shape. Move it under summary as --by-tool, matching the existing --by-provider, --by-subagent-type, and --subagent-tree mode flags.
Background
burn summary already has three "mode flags" that swap the grouping axis and the output shape:
--by-provider — table grouped by provider instead of model (commands/summary.ts:50,63-65)
--by-subagent-type — aggregated subagent stats, different columns
--subagent-tree <session-id> — tree layout, not a table at all
burn by-tool does the same thing in spirit:
- Same prelude:
ingestAll → loadPricing → queryAll with the same filters (commands/by-tool.ts:21-23 is literally the prefix of commands/summary.ts:52-54).
- Different aggregator:
attributeCostToTools (by-tool.ts:59-112) splits turn N's input cost across turn N-1's tool_use blocks.
- Different output shape:
tool | calls | attributedCost instead of model | turns | input | output | reasoning | cacheRead | cacheCreate | cost.
Filter parity gap: by-tool accepts --since, --project, --session, --provider but is missing --workflow and --agent (by-tool.ts:11-15). Folding into summary closes that gap by inheritance.
Proposed shape
burn summary --by-tool [--since 7d] [--project <path>] [--session <id>]
[--workflow <id>] [--agent <id>] [--provider <p>] [--json]
Output columns stay as today (tool | calls | attributedCost), and the existing attribution-method footer carries over verbatim:
attributedCost = (turn N input cost) split evenly across tool_use blocks in turn N-1, grouped by tool name.
unattributed cost (no prior tool call, e.g. first turn): $X.XX
This is exactly the same precedent --subagent-tree set: a mode flag that produces an output shape unrelated to the default model/provider table. Don't try to reshape by-tool's columns into the default schema — that's a worse design than the current standalone command.
Implementation Steps
No users yet — this lands as a clean break with no deprecation alias path.
Single PR.
- Move the aggregator:
- Lift
attributeCostToTools (commands/by-tool.ts:59-112) into commands/summary.ts (or a sibling helper file commands/summary-by-tool.ts if summary.ts is getting large — it's already 441 lines).
- Wire up the mode in
runSummary:
- Read
args.flags['by-tool'] alongside the existing byProvider, subagentTreeFlag, subagentTypeFlag checks (summary.ts:48-61).
- Add a
renderByToolMode(args, turns, pricing) branch parallel to renderSubagentTreeMode / renderSubagentTypeMode. Same dispatch shape, returns Promise<number>.
- Mode precedence: explicit subagent flags first (existing), then
--by-tool, then default --by-provider | by-model. Document the precedence in the help block.
- Mode exclusivity:
- If
--by-tool is combined with --by-provider / --by-subagent-type / --subagent-tree, exit non-zero with a clear error: "--by-tool cannot be combined with --by-provider/--by-subagent-type/--subagent-tree". Mirrors how the existing modes implicitly assume one-axis-at-a-time.
- JSON contract:
summary --by-tool --json emits { ingest, turns, byTool: [{ tool, calls, attributedCost }], unattributed, fidelity }. Keep the fidelity block parallel to the model/provider JSON shapes (summary.ts:69-101) so programmatic consumers see one schema family.
- CLI dispatch & help:
- Update the help block in
cli.ts:22-78 — drop the burn by-tool line, add --by-tool to the summary flag list with a one-line note about the different output shape and metric.
- Drop the
case 'by-tool' branch from cli.ts:95-96.
- Tests:
- Move
commands/by-tool.test.ts content into summary.test.ts (or keep as summary-by-tool.test.ts) — same fixtures, point them at runSummary with --by-tool.
- Add a mode-exclusivity test (
--by-tool --by-provider should fail with the expected message).
- Docs: README, AGENTS.md, landscape.md, CHANGELOG
[Unreleased] on @relayburn/cli. Most user-facing examples are burn by-tool --since 7d — update each.
Files touched (scope)
packages/cli/src/commands/by-tool.ts — removed
packages/cli/src/commands/summary.ts — new --by-tool branch + aggregator helper
packages/cli/src/commands/by-tool.test.ts — merged into summary.test.ts or renamed
packages/cli/src/cli.ts — dispatch + help block
- README.md, AGENTS.md, landscape.md,
packages/cli/CHANGELOG.md
No external API change — attributeCostToTools is currently un-exported from @relayburn/analyze (it lives in the CLI). Stays internal.
Risks / questions
- Metric mixing.
attributedCost (the by-tool number) and cost (the by-model/by-provider number) are not the same number. The existing footer in by-tool.ts:44-46 explains why; that explanation must ride along into the new mode's output, not be lost in a denser combined help block. Worth reviewing the JSON schema for the same reason — programmatic consumers should be able to tell which metric they're looking at.
- Help-block density.
summary already lists eight flags; adding --by-tool plus its precedence note pushes it close to the readability cliff. Consider promoting the mode flags to a small "Modes:" subsection in the help text rather than mixing them with filters.
Out of scope
Summary
burn by-toolis structurally asummarymode — same query path, same filters, different aggregator and output shape. Move it undersummaryas--by-tool, matching the existing--by-provider,--by-subagent-type, and--subagent-treemode flags.Background
burn summaryalready has three "mode flags" that swap the grouping axis and the output shape:--by-provider— table grouped by provider instead of model (commands/summary.ts:50,63-65)--by-subagent-type— aggregated subagent stats, different columns--subagent-tree <session-id>— tree layout, not a table at allburn by-tooldoes the same thing in spirit:ingestAll→loadPricing→queryAllwith the same filters (commands/by-tool.ts:21-23is literally the prefix ofcommands/summary.ts:52-54).attributeCostToTools(by-tool.ts:59-112) splits turn N's input cost across turn N-1's tool_use blocks.tool | calls | attributedCostinstead ofmodel | turns | input | output | reasoning | cacheRead | cacheCreate | cost.Filter parity gap:
by-toolaccepts--since,--project,--session,--providerbut is missing--workflowand--agent(by-tool.ts:11-15). Folding intosummarycloses that gap by inheritance.Proposed shape
Output columns stay as today (
tool | calls | attributedCost), and the existing attribution-method footer carries over verbatim:This is exactly the same precedent
--subagent-treeset: a mode flag that produces an output shape unrelated to the default model/provider table. Don't try to reshapeby-tool's columns into the default schema — that's a worse design than the current standalone command.Implementation Steps
No users yet — this lands as a clean break with no deprecation alias path.
Single PR.
attributeCostToTools(commands/by-tool.ts:59-112) intocommands/summary.ts(or a sibling helper filecommands/summary-by-tool.tsifsummary.tsis getting large — it's already 441 lines).runSummary:args.flags['by-tool']alongside the existingbyProvider,subagentTreeFlag,subagentTypeFlagchecks (summary.ts:48-61).renderByToolMode(args, turns, pricing)branch parallel torenderSubagentTreeMode/renderSubagentTypeMode. Same dispatch shape, returnsPromise<number>.--by-tool, then default--by-provider | by-model. Document the precedence in the help block.--by-toolis combined with--by-provider/--by-subagent-type/--subagent-tree, exit non-zero with a clear error: "--by-tool cannot be combined with --by-provider/--by-subagent-type/--subagent-tree". Mirrors how the existing modes implicitly assume one-axis-at-a-time.summary --by-tool --jsonemits{ ingest, turns, byTool: [{ tool, calls, attributedCost }], unattributed, fidelity }. Keep thefidelityblock parallel to the model/provider JSON shapes (summary.ts:69-101) so programmatic consumers see one schema family.cli.ts:22-78— drop theburn by-toolline, add--by-toolto thesummaryflag list with a one-line note about the different output shape and metric.case 'by-tool'branch fromcli.ts:95-96.commands/by-tool.test.tscontent intosummary.test.ts(or keep assummary-by-tool.test.ts) — same fixtures, point them atrunSummarywith--by-tool.--by-tool --by-providershould fail with the expected message).[Unreleased]on@relayburn/cli. Most user-facing examples areburn by-tool --since 7d— update each.Files touched (scope)
packages/cli/src/commands/by-tool.ts— removedpackages/cli/src/commands/summary.ts— new--by-toolbranch + aggregator helperpackages/cli/src/commands/by-tool.test.ts— merged intosummary.test.tsor renamedpackages/cli/src/cli.ts— dispatch + help blockpackages/cli/CHANGELOG.mdNo external API change —
attributeCostToToolsis currently un-exported from@relayburn/analyze(it lives in the CLI). Stays internal.Risks / questions
attributedCost(the by-tool number) andcost(the by-model/by-provider number) are not the same number. The existing footer inby-tool.ts:44-46explains why; that explanation must ride along into the new mode's output, not be lost in a denser combined help block. Worth reviewing the JSON schema for the same reason — programmatic consumers should be able to tell which metric they're looking at.summaryalready lists eight flags; adding--by-toolplus its precedence note pushes it close to the readability cliff. Consider promoting the mode flags to a small "Modes:" subsection in the help text rather than mixing them with filters.Out of scope
attributeCostToToolsalgorithm itself — even-split across prior-turn tool_use blocks. Improving attribution accuracy (e.g. weighting by tool_result bytes) is separate work, not blocked by this rename.burn rebuild-index(already an alias forburn rebuild --index) #151 (rebuild-indexremoval), cli + analyze: coordinated rename —waste/diagnose→hotspots,context→overhead#152 (waste→diagnose), cli: unifyburn rebuildandburn archiveunder one "rebuild derived state" verb #153 (rebuild+archiveunification), cli: collapseburn claude/burn codex/burn opencodeintoburn run <harness>#154 (burn run <harness>).