As a developer or a tech lead
I want to see what a task cost, broken down by step and by model
So that I can decide where to spend optimisation effort instead of guessing
Acceptance
- Steps sum to the task total, with a residual bucket so the sum reconciles exactly.
- A task with no session prints zeros and exits 0, never an error.
- A run whose identifier joins nothing is counted as unattributed and named, never dropped.
- Telemetry whose session id matches no run file is also named — the mirror case, and the one the epic exists to catch.
- A session that emitted a run file but no datapoints is distinguished from a session that was never journaled.
- The unattached share over the period is printed, and the period is a named flag with a documented default.
- When two skills interleave, the output says step attribution is approximate rather than presenting it as exact.
- No prompt, code or diff content appears anywhere in the output.
Expected output
task 2026_08_14_telemetry-v1
sessions 6
active time 47 min (per session; not attributable to steps)
tokens 310,400 34% cache
cost $4.20
by step
aidd-dev:02-implement 61% $2.56
aidd-dev:05-review 19% $0.80
aidd-dev:01-plan 12% $0.50
residual 8% $0.34
by model
claude-opus-5 78% of cost for 31% of calls
unattached 39% of the period
How the join actually works
Measured, and not what the first design assumed.
Per-session totals come from the metrics: claude_code.token.usage and claude_code.cost.usage both carry session.id, and claude_code.active_time.total gives the time.
Per-step breakdown cannot come from the metrics. skill.name on both counters reads the literal string third-party for every AIDD skill, because the docs replace third-party plugin skill names, and OTEL_LOG_TOOL_DETAILS=1 does not lift it on metrics. The real name appears only on the skill_activated log event.
So the breakdown is an exact event correlation, not a time window. Measured on a real session, both events carry the same correlation keys:
| Event |
Carries |
| skill_activated |
the real skill.name, with session.id, prompt.id, event.sequence |
| api_request |
input_tokens, output_tokens, cache_*, cost_usd, model, query_source, with session.id, prompt.id, event.sequence |
The rule: within a session, order by event.sequence and carry the last skill_activated forward onto the api_request records that follow, until the next one.
api_request has its own skill.name, and it is redacted to third-party exactly like the metrics — with the flag on as well. It is not usable, and the carry-forward is what replaces it. That is not a workaround: it mirrors the provider's sticky attribution instead of fighting it.
The same mechanism serves the other four tools later, with one difference: none of them emits a skill_activated equivalent, so there the step boundaries must be emitted by the framework itself.
Two measured limits the output must respect:
- skill.name is sticky. Once activated it rides the following datapoints, including subagents launched afterwards. Correct for sequential steps, wrong for interleaved ones.
- active_time.total carries no skill attribute. Time is per session only. Any percentage in the by-step block is cost, never time.
Why a skill and not a CLI command
The question is asked from inside a session, about work in progress. The plugin already carries the diagnostic; the figure belongs beside it. This also satisfies #297's requirement that at least one skill consume the data and produce something a user acts on, without sending anything anywhere.
Out of scope
- The four other tools, and the price table the two that export no amount will need.
- Any aggregation by person, team or epic.
- Currency conversion. Costs print in USD, as exported.
Relations
As a developer or a tech lead
I want to see what a task cost, broken down by step and by model
So that I can decide where to spend optimisation effort instead of guessing
Acceptance
Expected output
How the join actually works
Measured, and not what the first design assumed.
Per-session totals come from the metrics: claude_code.token.usage and claude_code.cost.usage both carry session.id, and claude_code.active_time.total gives the time.
Per-step breakdown cannot come from the metrics. skill.name on both counters reads the literal string third-party for every AIDD skill, because the docs replace third-party plugin skill names, and OTEL_LOG_TOOL_DETAILS=1 does not lift it on metrics. The real name appears only on the skill_activated log event.
So the breakdown is an exact event correlation, not a time window. Measured on a real session, both events carry the same correlation keys:
The rule: within a session, order by event.sequence and carry the last skill_activated forward onto the api_request records that follow, until the next one.
api_request has its own skill.name, and it is redacted to third-party exactly like the metrics — with the flag on as well. It is not usable, and the carry-forward is what replaces it. That is not a workaround: it mirrors the provider's sticky attribution instead of fighting it.
The same mechanism serves the other four tools later, with one difference: none of them emits a skill_activated equivalent, so there the step boundaries must be emitted by the framework itself.
Two measured limits the output must respect:
Why a skill and not a CLI command
The question is asked from inside a session, about work in progress. The plugin already carries the diagnostic; the figure belongs beside it. This also satisfies #297's requirement that at least one skill consume the data and produce something a user acts on, without sending anything anywhere.
Out of scope
Relations