Repository navigation
Releases: woksin/workstats
Release list
Release v1.11.0
Summary
workstats can now produce consultant timesheets: suggested hours per day and engagement, rounded the way a timesheet is, with the evidence behind each entry, a ledger for manual hours and locked periods, and CSV exports for Toggl, Harvest and Clockify. Work is attributed to branches, issues and features, and new commands cover the effort behind a branch or pull request, insights, a weekly digest, a prompt-line status, goals, a calendar heatmap and merging several machines.
Added
workstats timesheet: suggested hours per day and engagement with evidence, rounding (nearest, up, down, balanced), minimum entry, drop threshold and daily cap; every hour attributed once, reconciled with the report's human time, in table, JSON, CSV, Markdown and HTML. See Timesheet.- Engagements in the config: client work matched by project, remote, path, branch pattern or issue prefix, with rate, currency and billable flag; also usable as
--group-by engagement. - A timesheet ledger:
timesheet add,rm,set,unset,entriesfor manual hours and overrides, andlock,unlock,locksto freeze a submitted period and report drift instead of silently changing it. timesheet --export toggl|harvest|clockify|genericfor importing into time trackers.- Branch, issue and feature attribution from the branch fields Claude, Codex and Copilot record and from local Git refs, with
--group-by branch|issue|featureand configurable issue-key patterns. See Branches and pull requests. workstats branchandworkstats pr: the human time, agent time, tokens, list value and commits behind one branch or pull request, with PR-ready Markdown.workstats insights(focus, weekday-by-hour patterns, leverage, models) andworkstats digest(this week against the last, with top repositories and features).workstats now: a fast one-line status for a shell prompt or status bar, with templates and background refresh.- Goals: weekly and daily hour targets and list-value caps per subscription, shown in reports, the digest and
now. - A calendar heatmap of human time per day:
workstats calendar, the HTML report, Markdown with--daily, and the explorer (c). workstats exportandworkstats merge(or--import) to combine machines without double counting; bundles carry no prompts, commit subjects, titles or absolute paths. See Merging machines.- Opt-in descriptions:
--describe commits(subject lines of your own commits),--describe sessions(tool-generated titles, never prompts) andtimesheet --summarize-with CMDwith--digestto preview what is sent. --dailyper-day figures in JSON, andworkstats record --branch NAME.
Changed
- The transcript cache moves to parser version 5 and is rebuilt on first use, now holding branch names and pull-request numbers.
- New config blocks
branches,issues,engagements,timesheet,goals,insightsandnoware validated when used, naming any bad key.
Fixed
- Report rows that tie on every figure now come out in the same order on every run.
Release v1.10.0
Summary
Git counting now sees work it used to miss, and reports gain weekly periods, period comparison, and Markdown and HTML output. Everyday flags can live in the config file, and the documentation moved into focused guides under docs/.
Added
- Weekly periods:
--period week,--group-by weekand--week YYYY-Www|current|lastgroup and filter a report by ISO 8601 week. --compare previous(or a namedYYYY-MM,YYYY-WwworYYYYwindow) shows the headline figures beside the previous period's, with changes labelled as estimates. Each side is exactly the report that window prints alone.--format markdownand--format htmlfor reports andworkstats allocate. The HTML is a single self-contained file with no scripts or external references. Both formats escape every value, stop GitHub mentions and issue links, and replace your home directory with~.- A
defaultsblock in the config file for everyday flags (dir,depth,format,providers,group_by,gap_cap,human_idle,review_credit). A flag always wins, and the output names the defaults that applied. --authorcan be given more than once, and the config file acceptsauthors, so several Git identities count as you.model_ratesin the config file overrides or adds per-model list rates forworkstats allocate, andallocatewarns when the built-in rate table is more than 90 days old.- A warning when a tool's history files are read but yield no usable activity, which usually means the tool changed its history format.
Changed
- The documentation moved from the README into guides under
docs/: install, usage, configuration, how it works, allocate, privacy and development. - The transcript index is also keyed on the workstats version, so an upgrade never answers from entries an older parser wrote.
- A config file with an invalid
authors,defaultsormodel_ratesvalue is refused with an error naming the key, instead of being ignored as a whole.
Fixed
- Commits on every local branch are counted, not only those reachable from the checked-out branch.
- The report window is decided by author date, so rebased and amended commits are no longer dropped from the period they were written in. Reports for past periods also no longer diff every later commit.
workstats allocatenames every window flag it accepts when no window is given.
Release v1.9.0
Summary
--project was required, so the only question allocate could answer was what one project is owed. The prior question — where did the money actually go — had no answer, and working it out meant running the command once per project and adding up the results by hand.
Naming a project still asks for a claim. Omitting it now asks for a breakdown:
$ workstats allocate --sub claude=2@1866 --sub codex=3@1992 \
--currency NOK --vat 25 --month 2026-08 --top 6
ALLOCATION every project
2026-08 · 5 subscriptions · 12,135 kr billed incl. 25% tax · basis: output tokens
PROJECTS
project claude openai TOTAL % out tokens
──────────────────────────────────────────────────────────────────────────────────────────────────
Ada 2,072 kr 3,048 kr 5,121 kr 42.2% 111.3M 38.28B
Chronicle.Wolverine [f81e3bee] 102 kr 1,020 kr 1,121 kr 9.2% 18.5M 7.56B
AI 295 kr 433 kr 728 kr 6.0% 15.8M 7.41B
cratis 385 kr 261 kr 646 kr 5.3% 16.1M 5.89B
Screenplay 83 kr 532 kr 616 kr 5.1% 10.6M 5.07B
Strategy 90 kr 491 kr 581 kr 4.8% 10.2M 5.55B
(59 smaller projects) 3,322 kr 27.4%One column per plan held, so a project leaning on a single vendor is visible at a glance — Screenplay draws six times as much from OpenAI as from Claude, cratis the other way.
Added
workstats allocateno longer requires--project. Omitting it reports every project's slice of the spend, ranked by amount, with one column per plan held.--topbounds the list and states the remainder rather than dropping it;--top 0shows every project.
Notes for review
- The rows reconcile exactly to what was billed, and a test pins that — a breakdown that does not add up is not a breakdown of anything.
- Ranking is by money, not tokens. A test covers the case where a project with less total output outranks one with more because it drew on the pool backed by more plans; that is the correct answer and an easy one to get wrong.
- A project absent from a pool contributes zero to it rather than silently borrowing another project's share. Also tested.
- Stating the remainder follows the rule the rest of the command already uses for pruned history and unmatched project names: never a silent zero.
- 288 tests pass (2 new). Clippy, rustfmt, MSRV 1.88 and rustdoc clean locally.
Release v1.8.0
Summary
allocate assumed one price, untaxed, in dollars. None of that holds for a buyer outside the US, and each gap pushed arithmetic back onto the person the command exists to spare.
$ workstats allocate -p Ada --sub claude=2@1866 --sub codex=3@1992 \
--currency NOK --vat 25 --month 2026-08
2026-08 · 5 subscriptions · 12,135 kr billed incl. 25% tax · basis: output tokens
month family subs plan/mo project pool share owed
─────────────────────────────────────────────────────────────────────────────────
2026-08 claude 2 2,333 kr 65.5M 147.5M 44.4% 2,072 kr
2026-08 openai 3 2,490 kr 45.8M 112.1M 40.8% 3,048 kr
─────────────────────────────────────────────────────────────────────────────────
ATTRIBUTABLE 42.2% 5,121 krTax is money that left the account. Vendors advertise ex-tax prices, so a Norwegian buyer paying 25% MVA was under-reporting the pool by a fifth.
Two vendors rarely cost the same. Anthropic prices in dollars and lets a card convert them; OpenAI sets a local price. Previously the only way to express that was to run the two pools separately and add them by hand — which defeats the point of the command.
No exchange rate is applied. --currency labels and formats; it converts nothing. workstats makes no network calls outside workstats update, and a rate compiled into a binary goes stale without saying so — the worst failure mode for a number about to be invoiced. State what you were charged and the arithmetic stays yours.
Added
--vat PERCENTadds the consumption tax applied at checkout, and the tax-inclusive unit price now appears on each row so a claim can be checked line by line against a bank statement.--currency CODEstates which currency--priceis in and formats amounts to match — symbols where unambiguous, ISO code otherwise. No conversion is performed.--sub PLAN=N@PRICEprices one vendor separately from the rest, for the common case of two vendors billing the same buyer different amounts.
Notes for review
- A share is a usage ratio and cannot move when only a price does. A test pins that, so a pricing change can never quietly restate what the work was.
Plan { count, price }replaces the bare count inAllocationOptions; a vendor given no@PRICEfalls back to the run's--price, which is also tested.- Two display bugs found while running real data: a literal
$in the cross-check that rendered$ 5,121 kr, and the ATTRIBUTABLE row sitting one column out once the plan-price column was added. Both fixed. - 286 tests pass (4 new). Clippy, rustfmt, MSRV 1.88 and rustdoc all clean locally.
Release v1.7.0
Summary
Flat-rate plans are bought per vendor and spent across every project. workstats allocate answers what share of that spend one project accounts for — the question a reimbursement claim turns on.
It splits each vendor's pool separately and weighs it by how many plans that vendor holds, rather than pooling every token together. With 2 Claude and 4 Codex plans, 44% of the Claude pool and 41% of the OpenAI pool are worth different amounts; pooling the tokens would have reported a single misleading 43%.
$ workstats allocate -p Ada --sub claude=2 --sub codex=4 --month 2026-08
ALLOCATION Ada
2026-08 · 6 subscriptions @ $200/mo · $1,200 billed · basis: output tokens
month family subs project pool share owed
────────────────────────────────────────────────────────────────────
2026-08 claude 2 65.5M 147.5M 44.4% $178
2026-08 openai 4 45.8M 112.1M 40.8% $326
────────────────────────────────────────────────────────────────────
ATTRIBUTABLE 42.0% $504Three failure modes share one shape — a silent zero that reads as "nothing to claim" when it means "nothing was seen" — and none of them are reported quietly:
- Pruned history. A month whose transcripts are gone is not a month of no work.
--gap-policychooses explicitly between excluding it, claiming nothing for it, or imputing that vendor's mean share. - Clients billing on their own seat. A Copilot seat is not a Claude or ChatGPT plan even when it runs their models. Copilot forms its own pool instead of diluting one it was never billed to — which also stops a single stray Copilot session from making a month of missing vendor history look covered.
- A misspelled project. Named, with the nearest real repository offered.
Every run prints all five bases side by side, so a share that depends on the metric is visible rather than chosen quietly. Human time is not the default and is marked estimated: it is inferred from prompt counts and session edges, so orchestration-heavy work books more apparent attention per real hour than one long session does.
Added
workstats allocateapportions flat-rate subscription spend to one project, splitting each vendor pool separately and weighting it by plans held.- Per-model breakdown behind every share, with models weighed by published list rate so a million Opus tokens and a million Haiku tokens are not equal claims on a plan.
--basisselects the measured quantity —output(default),value,wall,tokensorhuman— and every run cross-checks all five against each other.--gap-policymakes the handling of pruned history an explicit choice rather than a silent zero.- Table, JSON and CSV output, composing with the existing
--month,--sinceand--untilwindow flags.
Notes for review
src/pricing.rscarries the published list rates, stamped withRATES_AS_OFso a stale table is visible in output rather than silent. They are used as relative weights and as a stated ceiling, never as an amount owed.- Allocation takes
&[ReportRow]rather than a wholeReport; it is a pure function of the grouped rows and is unit tested directly. ReportRowgains#[derive(Default)]so tests can build rows without restating every field.- 282 tests pass (17 new). Two of them are regression tests for bugs found during development: the cross-check row for the chosen basis contradicting the headline it corroborates, and Copilot usage being counted into the vendor pool.
Release v1.6.1
Summary
Apply the formatting emitted by the Rust 1.98 stable toolchain used by GitHub Actions.
Fixed
- Make the
rustfmtCI job pass under Rust 1.98 without changing behavior.
Release v1.6.0
Summary
Treat linked worktrees, clones of one remote, and explicitly aliased repositories as logical projects while preserving natural-repository identity where deduplication requires it.
Added
- Add configurable
project_aliasesfor combining distinct repositories into one product. - Add
--explain-repository-attribution/--explain-reposwith privacy-safe table and JSON evidence. - Persist repository identity history so deleted foreground and delegated Pi worktrees remain attributable.
Changed
- Group repository rows by normalized local Git identity instead of checkout folder name.
- Keep commit and file deduplication scoped to natural repositories, including TUI navigation and search.
- Make
--repo-exactprefer final display labels, then fall back to checkout folder names when no label matches.
Fixed
- Recover linked and orphaned worktree identities without contacting remotes.
- Reject overlapping project aliases and ambiguous history instead of choosing silently.
- Invalidate Pi cache entries when their parent-session attribution context changes.
- Restrict attribution evidence and counters to checkouts that contribute inside the report window.
- Normalize HTTPS, SSH, SCP-style, file, and Windows-path remote spellings consistently.
Release v1.5.0
Summary
Make the human-time estimate auditable without turning reports into an attendance claim. The calculation now has one versioned source of truth, an opt-in structural ledger for people and API consumers, and explicit safeguards around block overlap, precision, and sensitive metadata.
Release notes are generated from this description:
Added
- Add
--explain-human-timefor a readable table ledger and structured JSON details covering scoped/effective signals, timestamp deduplication, block boundaries, credit, clipping, and the reconciled total. - Add stable
signal-blocks-v1algorithm, timezone, and boundary-basis metadata to JSON reports. - Add privacy and reconciliation regression coverage, including exact half-millisecond ties and equal-priority timestamp collisions.
Changed
- Calculate human-time intervals, block explanations, and the summary total through one shared path.
- Round the final total from exact integer microseconds with ties-to-even, exposing the unrounded block subtotal and rounding adjustment for auditability.
- Reject
--explain-human-timefor CSV and the TUI, whose output shapes cannot represent the one-to-many ledger without ambiguity.
Fixed
- Reject
--review-creditvalues greater than--human-idle, preventing separately clustered work blocks from overlapping and counting time twice.
Privacy
- Explanation output includes structural timestamps, signal kinds, providers, and repository labels only; it excludes prompt/response text, raw session IDs, cwd/root paths, model names, and commit hashes.
Verification
cargo test— 232 unit tests and 10 integration tests passed.cargo clippy --all-targets -- -D warnings— passed.cargo fmt --check— passed.git diff --check— passed.- Two structured validation reviews completed; all findings were addressed before opening this PR.
Release v1.4.2
Fixed
- Unfiltered reports now scan retained AI sessions\u2019 locally available Git checkouts outside
--dir, so a repository filter no longer changes whether authored commits are found.
Release v1.4.1
Follow-up to #18, which added the pi adapter. Completes the source support.
The gap
Pi persists !command — a shell command typed at its own prompt — with the
role bashExecution. The adapter skipped that role, so a session spent driving
a build or a test run by hand reported no human involvement at all beyond
its session edges.
The model cannot produce one of these. It reaches the shell through the bash
tool, which is persisted as a tool result and is already treated as agent
activity. So bashExecution is unambiguous keyboard evidence — more direct
than the session edges that were carrying these sessions.
It is counted in a foreground session only: inside a subagent the command
was issued by the agent driving it, not typed by anyone. !!command, which
keeps output out of the model's context, counts the same — someone still typed
it.
Measured
A session holding one typed !npm run lint:
| Binary | prompts | human work |
|---|---|---|
| v1.4.0 (shipped) | 0 | 1800 s |
| this PR | 1 | 1810 s |
Real local history is unchanged (203 prompts either way), because the bash
tool — not ! — is what an agent-driven session uses. This affects people who
drive Pi from the keyboard.
Privacy
The command and its output are still never read. They reach the deserializer as
ignored fields, exactly like a response body, so nothing about them can appear
in a report or the cache. A test asserts that a token placed in a command and
in its output is absent from the serialized result.
Verification
cargo fmt --all -- --check,cargo clippy --all-targets --locked -D warnings,cargo test --locked— all clean- 228 unit + 9 integration tests pass, including 2 new tests covering the
foreground and delegated cases