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
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.usageandclaude_code.cost.usageboth carrysession.id, andclaude_code.active_time.totalgives the time.Per-step breakdown cannot come from the metrics.
skill.nameon both counters reads the literal stringthird-partyfor every AIDD skill, because the docs replace third-party plugin skill names, andOTEL_LOG_TOOL_DETAILS=1does not lift it on metrics. The real name appears only on theskill_activatedlog 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:
skill_activatedskill.name, withsession.id,prompt.id,event.sequenceapi_requestinput_tokens,output_tokens,cache_*,cost_usd,model,query_source, withsession.id,prompt.id,event.sequenceThe rule: within a session, order by
event.sequenceand carry the lastskill_activatedforward onto theapi_requestrecords that follow, until the next one.api_requesthas its ownskill.name, and it is redacted tothird-partyexactly 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_activatedequivalent, so there the step boundaries must be emitted by the framework itself.Two measured limits the output must respect:
skill.nameis sticky. Once activated it rides the following datapoints, including subagents launched afterwards. Correct for sequential steps, wrong for interleaved ones.active_time.totalcarries 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
Relations