Skip to content

Latest commit

 

History

History
97 lines (59 loc) · 20.3 KB

session-cost.md

File metadata and controls

97 lines (59 loc) · 20.3 KB

Session cost

Unless you skip it (below), every PRD-lifecycle command tags its own run with a dollar figure and appends it to a per-PRD, per-session cost file, so the plugin maintainer can later add up what a Product Requirements Document cost across every engineer and team who touched it. Claude Code stores no dollar figure of its own anywhere in the transcript — cost is always computed from token usage against a price table, never read off some authoritative source, which makes it an estimate that drifts from Claude Code's own accounting by exactly the accuracy of that price table. This is an accepted trade: cost accuracy is explicitly secondary to code and doc quality, and the "Session cost" phase always runs and, short of --skip-costs, always writes something, even when the something is only a line in the run's own printed output; under --skip-costs it still runs, but only advances the checkpoint and prints its Session cost: skipped line, plus one line per pending deferred record it drops.

Skipping cost (--skip-costs)

Under --skip-costs (or WORKFLOWS_SKIP_COSTS), a command's cost phase never calls emit-cost and never loads workflows-core:cost-emission — it runs workflows-core:run-flags's skip-cost entry point in its place. The checkpoint still advances exactly as a full run would, so the next command's window is measured correctly; nothing else happens — no cost entry, no pending file, no reconciliation offer. Any deferred record from a prior ceded /prompt-brainstorm or /prompt-grill-me run is dropped rather than replayed: the skipping run names each dropped record in its output and deletes the deferred file, since the checkpoint has already moved past what it could have matched.

The run reports Session cost: skipped (--skip-costs) or Session cost: skipped (WORKFLOWS_SKIP_COSTS) in place of the usual persisted-path line.

Where cost files land

Cost writes to a cost/ subdirectory with one file per session, not one file per PRD the way feedback and follow-ups do — <PRD-dir>/dev-workflows/cost/<sid8>.md, named after the first eight characters of the session id. That per-session split is deliberate merge-safety: the largest PRD to date was worked by 23 teams, and giving every session its own file means no two engineers' commands can ever collide in the same file. A single session that runs several dev-workflows commands in a row — /idea then /create-prd then /specify, say — appends one entry per command invocation to that same session file, never deduplicated, because each invocation is a distinct dollar measurement; /design Epic1 followed by /design Epic2 in one session produces two separate entries. No entry carries a user name anywhere — the git commit author, once the file is committed and pushed, is the only identity layer this feature relies on.

The write target is resolved by a specs-first ladder, walked top-down, stopping at the first tier that applies:

  1. $SPECS_PATH is writable and the PRD directory can be matched → <PRD-dir>/dev-workflows/cost/<sid8>.md. This is the primary case and the one the whole feature exists for.
  2. $SPECS_PATH is writable but no PRD directory matches — a keyless run, such as /implement in direct mode, or /feedback invoked outside any PRD — → the entry goes to a pending file instead, at $SPECS_PATH/dev-workflows-cost/pending-<date>-<sid8>.md. The next time any command in the same or a later session resolves a real PRD key, it lists the pending files it finds and offers to relocate their entries into the now-known PRD's cost directory — same-session files are pre-selected as the likely match. A confirmed relocation moves the entries and deletes the pending file so it never resurfaces; a declined one is simply left in place and may be offered again later.
  3. The run's source is a folder in the specs tree and $SPECS_PATH is unavailable → the file lands beside that directory.
  4. Nothing resolvable → report-only: the entry stays only in the run's printed output. The plugin never writes into your current working directory, where it is not the specs repository, since it may be a code repository.

None of this touches git — the cost entry is committed and pushed later, once, by the run's terminal commit-artifacts step, the same as every other session artifact this plugin writes into $SPECS_PATH.

How cost is computed

The session-cost.py script the companion workflows-core plugin ships does the actual arithmetic: it reads the assistant-turn usage and model fields already present in the session's main transcript, plus every subagent transcript dispatched during the window being measured, sums token counts per model, and multiplies by the price table in effect for that run (below). An unknown model — one absent from the price table entirely — is still recorded with its token counts, priced cost_usd: null, and tagged note: unpriced-model; the run never fails on this, but if that unpriced model turns out to hold the largest token share of any model in the entry, the command prints a visible warning naming it, so a cost figure that is silently a lower bound doesn't go unnoticed.

The window a command's cost figure covers is chained, not fixed. A small local checkpoint file, keyed by session id and never committed, records where the last dev-workflows cost phase left off — a transcript line offset and a timestamp. The next cost phase in the same session picks up exactly there, so a session running three dev-workflows commands back to back produces three entries whose windows are contiguous: activity between one command's end and the next command's start rolls into the next command's bucket, and only the time before the first command and after the last is left unattributed. The checkpoint advances after every computation, including in the pending and report-only tiers above, so the next command's window is always correct regardless of where this one's entry ended up.

The price table is the companion plugin's references/cost-prices.yaml, which ships beside the script — a data file rather than a loadable reference, which is why it is named as a path here and not in the workflows-core:<name> form the loader resolves — keyed by model id, giving USD-per-million-token rates for input, output, cache_read, cache_write_5m, and cache_write_1h. Rates are the standard, permanent first-party API prices — never a promotional or introductory rate — so cost stays comparable across PRDs over time rather than making identical work look artificially cheap today and dearer once a promotion ends. Cache tokens follow Anthropic's standard multipliers (a cache read prices at roughly a tenth of the input rate; a 5-minute cache write at 1.25x; a 1-hour write at 2x), and model lookup falls back from an exact model-id match to the longest matching undated prefix, so a dated transcript model id like claude-sonnet-5-20250930 still prices correctly against an undated claude-sonnet-5 table entry.

An optional cross-check ("Option B") is available once the plugin's status line is installed: the status line snapshots Claude Code's own total_cost_usd on every render, and the cost phase reads the delta between the current snapshot and the one recorded at the previous checkpoint, surfacing it as cost_statusline_usd alongside the computed figure. It is emitted only once both a current snapshot and a prior baseline exist — the very first cost phase in a session always omits it, even with the status line installed, since there is no baseline yet to diff against.

How a run is attributed

Every cost-emitting command passes a phase and a role label to the cost phase at the point it calls the shared entry point — this is what lets the maintainer later group spend by where in the lifecycle it was spent and who was doing the spending. Roles and phases is the place this plugin's own phase/role vocabulary lives — the three cost-attribution phases this plugin's commands emit, all role dev — so this page does not restate it; the family's other lifecycle phases are workflows-core:cost-emission §7's to list — those with roles pm/pa/pe are documented on the companion product-workflows plugin's own Roles and phases page, and docs-workflows' role-dev phases (documenting, docs-scaffold, docs-audit) on that plugin's own session-cost page. What belongs here is the mechanics of the assignment itself, not what the labels mean once assigned.

Three commands emit a cost entry here, unless --skip-costs (above): /design, /implement, and /ready. Every one of them passes a fixed phase/role pair baked into the command itself; none infers. Twelve more commands that used to make this list — /idea, /create-prd, /update-prd, /create-ard, /specify, /epics, and the BRD-to-PRD route's /brd-intake, /brd-split, /prd-ground, /brd-interview, /brd-package and /brd-reconcile — ship in the companion product-workflows plugin now; five more ship in docs-workflows and charge to phases no command of this plugin emits: /document with a fixed documenting/dev pair and /release-notes by inference, so a keyed run's spend still lands against the same PRD, and /docs-init, a standalone /docs-brand and /docs-audit under docs-scaffold/dev and docs-audit/dev, filed for the documentation repository rather than a PRD. Each companion plugin's own session-cost page describes its own commands.

What does not emit one, and why. /vuln and /upgrade emit no cost entry. The number measures AI investment in a product increment, and neither advances a PRD- or BRD-scoped artifact — a CVE bump or a library upgrade is noise against a PRD, and a metric that averages the two answers a question nobody asked. Session feedback is a different question: /vuln and /upgrade do emit that, because it records a gap in the plugin itself rather than the cost of the work.

Five more emitters ship in workflows-core, and the total spend they represent is real. /feedback, /frames and /prompt write their own entry; /prompt-brainstorm and /prompt-grill-me cede the session and defer theirs to a later run, as Spend a command cannot measure itself below describes. /frames infers from the resolved folder's own kind — a frame set in a folder asserting kind: brd, a BRD container or a BRD-route slice, is brd-to-prd/pm spend, and one in a folder asserting prd or epic is prd-creation/pm, an Epic folder under a slice included; neither is one of this plugin's own three phases. The other four resolve the target command whose output they are correcting or remarking on and inherit that command's own fixed labels — a correction to a /design, /implement, or /ready output lands on one of this plugin's own phases, but a correction to any other command in the family lands elsewhere just as easily. They are described here because a run of any of the five may land on this plugin's phases; their own page is workflows-core's.

The four correction commands in workflows-core infer differently: each resolves the target command whose output it is correcting or remarking on, and inherits that command's own fixed labels. A /prompt correcting a /specify output is priced as specification/pe, one correcting a /design output as planning/dev — the cost of fixing a phase's output belongs to that phase, so "what did specifying cost" includes the cost of making the spec right. Where no target resolves, where the target has no attribution row of its own, or where the target is itself a feedback command, the entry becomes plugin-feedback/n/a. n/a is the absence of a role recorded rather than guessed; aggregation should treat it as unattributed rather than folding it into dev.

Why the other two do not. /vuln and /upgrade have no Product Requirements Document to attribute spend to and sit outside the pipeline entirely. (Commands that have never emitted one — as opposed to deferring one, above — ship elsewhere too, and each companion plugin's own session-cost page names the ones it holds; this page does not restate them.)

Spend a command cannot measure itself

/prompt-brainstorm and /prompt-grill-me hand control to something else at their Phase 3 — a brainstorming skill, or a long interactive grill — and never get it back. Neither can run a cost phase after its own expensive work, because there is no "after" that it controls, and one placed before the hand-off would price the logging prologue alone. So the run that cedes records the labels it would have claimed into a local, transient, never-committed file beside the checkpoint, and the next cost-emitting run in the session writes the entry on its behalf (workflows-core:cost-emission §13). --skip-costs stops that entry from either side: a ceding run under the flag marks its record skip, so the replaying run still carves the segment out of its own window and then writes nothing for it, and a replaying run under the flag drops every pending record unreplayed, naming each in its output.

The boundary between the two runs is not guessed. A transcript already records every slash-command invocation, so the replaying run splits its own measurement window: the ceded run gets the segment from its own invocation to the next command of any kind, and everything else stays with the replaying run. The slices are disjoint and sum to what that run would otherwise have claimed whole.

Two details in that sentence are doing real work. The ceded run is found by name, not by counting invocations — a window often contains commands that emit no cost entry at all (/vuln, /upgrade, /workflows-core:statusline, /docs-workflows:docs-profile), and an interrupted run leaves a mark too, so counting positions would shift every attribution by one and file a security run's spend under a product phase. And a command invocation is recognised by its whole envelope rather than by one tag, because Claude Code writes built-in and plugin commands in two different tag orders; matching only the built-in shape finds no plugin command at all. If a recorded intent matches no invocation, nothing is written for it — its spend stays with the replaying run, and the run says so rather than attaching it to a neighbour.

A claim only resolves onto a command of this family. The deferred claim is matched against a manifest of the family's own <plugin>:<command> names, so a command from another marketplace ends the open window — its spend is its own — without ever being claimable. A claim it interrupts is reported as unmatched and dropped, never quietly attached to it. workflows-core:cost-emission §13 owns the rule.

This matters because the error it removes was large, not small. A grill is expensive, and before the deferral existed its spend was not lost but misattributed — it rolled into the next command's bucket, so a long grill followed by /implement was priced as implementation/dev. What still goes unmeasured is a session where no cost-emitting command ever follows the grill; there the spend stays in the ordinary post-last-command tail, unattributed, exactly as it would for any other command.

/vuln and /upgrade emit no cost attribution at all. Neither has a Product Requirements Document to attribute spend to, and neither calls the cost entry point; workflows-core:cost-emission gives neither an attribution row; it names them only to say why they emit none, among the commands a correction has nothing to inherit from, and among the boundaries a deferred claim's window may hold. Both commands do pass a field also named phase to their own fixer/executor subagents (full, verify-resume, regression-resume), but that is a completely different vocabulary belonging to the model-routing resume protocol, saying how much of a single command's own work must be re-executed after a review or a failed test — not where a run sits in the product lifecycle. The two vocabularies share nothing but the field name; see the closing note on Roles and phases for the fuller distinction.

Every attributed entry also carries the run's resolved keys: prd is always present, and epic is present only when an Epic key was in scope for that run.

The dollar figure attached to a phase depends on which price table priced it — override the bundled default with $DEV_WORKFLOWS_COST_PRICES, a path to your own models:-keyed price file. That variable is read by the workflows-core plugin, which ships the price table and the emitter, and its environment page carries the exact resolution order and file shape. Setting it is never required — the shipped table covers every model the routing policy can reach.

Reading a cost file

Here is one persisted entry, copied verbatim from the shared reference this page is derived from:

## 2026-07-09T14:22:33Z — /implement — implementation

```yaml
id: PRODUCT-1234-98760-implement-2026-07-09T14:22:33Z   # timestamp => unique
date: 2026-07-09T14:22:33Z
command: /implement
phase: implementation
role: dev
prd: PRODUCT-1234
epic: EPIC-98760            # present only when an Epic key is in scope
plugin_version: 2.10.0
duration_s: 1284
cost_computed_usd: 3.4821
cost_statusline_usd: 3.5102   # present only when the plugin statusline is installed
models:
  - {model: claude-opus-5, cost_usd: 2.9114, input_tokens: 12043, output_tokens: 88210, cache_read_tokens: 2109887, cache_write_tokens: 145002}
  - {model: claude-sonnet-5, cost_usd: 0.5707, input_tokens: 45120, output_tokens: 210334, cache_read_tokens: 880122, cache_write_tokens: 42011}
```

Walking it field by field: id is stable and timestamp-suffixed, so a re-run never double-logs against it. date is when this entry's window ended — except on a deferred entry, which is dated when its run ceded the session, a moment near the start of the window it measures. And command/phase/role/prd/epic are the attribution fields from the section above. plugin_version pins which release of the plugin produced this entry. duration_s is wall-clock seconds, not token-processing time — it is the gap between when this command's window started (the previous dev-workflows command's checkpoint in this session, or the session's own start if this was the first) and when it ended; a large duration_s on a small cost_computed_usd usually just means a long-idle session, not an expensive one.

cost_computed_usd and cost_statusline_usd measure the same run through two different lenses and are expected to differ by a few cents, not agree exactly. cost_computed_usd is built by re-deriving cost from the transcript's own token counts against the price table, so it stays complete right up to the very last turn but is only as accurate as that price table. cost_statusline_usd is a delta against Claude Code's own status-line cost figure, so it is authoritative on price, but the status line renders after the final turn completes, which means it can lag slightly at the tail of a run. Neither one is simply "the one to trust" over the other — a small, stable gap between them is the plugin's own calibration signal, and a gap that grows noticeably is the cue to refresh the price table, not evidence that one figure is broken.

models holds one row per model actually used, not one row per agent that used it. The window being measured spans the main session transcript plus every subagent transcript dispatched during it, and token usage from all of them is summed together by model id before pricing runs — so if both the orchestrating session and three different subagents all happened to run on claude-sonnet-5, that is still exactly one row, because the price table only cares which model billed the tokens, never which agent asked for them.

Finally, cache_read_tokens running far larger than input_tokens — as it does in both rows above — is a healthy sign, not a red flag: it means most of what this run "read" was already-cached context (prior conversation, tool definitions, repeated system prompt) rather than freshly billed input, and a cache read prices at roughly a tenth of the input rate. A run with a huge cache_read_tokens figure and a comparatively small cost_usd is exactly what efficient reuse of context looks like; the raw token count is not the number to be alarmed by, cost_usd is.