| name | analyze-usage |
| description | Analyze higher-level patterns in Claude Code usage. Outputs (a) graphic monthly/weekly digest with metaphors — aquarium of biomes (🐋🦈🐬🐟🦐🦠), archetypes (⚙️🔬🌐🛠📝🔍🏗💬), rhythm, stack palette, DORA radar (CFR + lead time + pushes), friction (compacts/pivots/subagents); (b) per-session parquet bundles for further analysis (biome, archetype, rhythm, growth, milestones, idle gaps, subagent pyramids, compact patterns, parallelism, burst classes, topics). Use when the user asks about weekly/monthly reports, session biomes, productivity profiles, "what kind of work was done", when a session became a whale, DORA metrics, or wants drill-down view of a specific session. |
| allowed-tools | Bash, Read, Write, Edit, Glob, Grep |
analyze-usage
Higher-level usage analytics on top of research-pipeline. Extracts patterns
that the existing pipeline does not surface directly: biome class, archetype,
growth curves, milestones, gaps, subagent pyramids, compact patterns, and
burst-level RW classes. Output is a pair of parquet bundles plus an
agent-written markdown report.
When to use
Trigger on any of:
- "biome", "whale", "shark", "creature", "плактон", "кит", "акула"
- "archetype", "constructor", "operator", "researcher"
- "growth", "milestone", "когда сессия стала", "rate of growth"
- "subagent pyramid", "subagent stats", "сабагенты"
- "compact pattern", "context window over time", "автокомпакты"
- "idle gaps", "should I split this session", "split point"
- "drill into session", "view session timeline" + a session id
- "weekly digest", "monthly report", "отчёт за неделю/месяц/квартал", "графический отчёт", "DORA", "CFR", "lead time" → use
scripts/period_report.py --from --to --period monthly|weekly|both
- "rhythm", "Mono / Phased / Mixed / Chaos", "ритм сессии", "stack palette", "self-feedback", "TDD ratio", "motivated read" — these come from
lib/classifiers.py and the corresponding extractors
If the user only wants the base research dataset (sessions, turns, tool_calls,
loops, correlations), use research-pipeline instead. This skill extends
that dataset.
Three-tier output
outputs/
raw/ Tier 1 stat parquet, original UUIDs/paths/branches — owner only
anon/ Tier 1 anonymized — shareable bundle (audit with `--audit-anon`)
llm/ Tier 2 LLM-augmented features (session names, narratives) — owner only
reports/ markdown reports written by the agent (Tier 2)
raw/ keeps original session UUIDs, project paths, file paths, branch
names, and prompt token frequencies. Owner-only; never share without
re-running the anonymizer.
anon/ follows the anonymization contract below. Hashed session ids
(s0001 / a0001), hashed project slugs (p<6hex>), file ext only,
no raw paths or branch names, prompt tokens replaced by 6-char hashes
(reverse dictionary stays in raw/_token_dict.json). Auditable via
pattern_detector.py --audit-anon.
llm/ holds Tier 2 outputs that depend on the agent (Claude). Tier 1
extractors stay deterministic; LLM enrichment is layered on top via
extract_llm_*.py extractors that build a queue (_queue.jsonl) the
agent consumes and writes back as parquet. Owner-only by default.
reports/ is markdown the agent generates from the parquet tables.
Reports may include real session names and quotes if the user owns the
data.
Tier model
- Tier 0 — tests:
tests/run_all.sh runs 28 unit + smoke tests
(classifiers, stats, full extractor pipeline on a synthetic fixture,
anonymization leak audit). Run before shipping a new extractor.
- Tier 1 — always-on (no API key): pure Python extractors using regex
and statistics. Lives in
scripts/extract_*.py. Reuses lib/ helpers.
- Tier 2 — agent-side LLM: when the skill is active, the agent itself
reads parquet + raw jsonl and adds the narrative layer (session naming,
phase detection, pattern interpretation, drill-down quotes). No external
LLM API call required.
If you (the agent) need a metric that doesn't exist yet, write a new
extract_<name>.py, wire it into pipeline.py, add a test in
tests/test_extractors_smoke.py, run it. The pipeline is designed to
extend without rewrite.
Run
uv run .claude/skills/analyze-usage/scripts/period_report.py \
--from 2026-02-01 --to 2026-04-30 --period both
uv run .claude/skills/analyze-usage/scripts/period_report.py \
--from 2026-04-01 --to 2026-04-30 --period monthly
uv run .claude/skills/analyze-usage/scripts/period_report.py \
--from 2026-04-23 --to 2026-04-30 --period weekly --out /tmp/last_week.txt
uv run .claude/skills/analyze-usage/scripts/pipeline.py \
--since 2026-04-01 \
--outputs .claude/skills/analyze-usage/outputs/
uv run .claude/skills/analyze-usage/scripts/extract_biome.py
uv run .claude/skills/analyze-usage/scripts/extract_growth.py
uv run .claude/skills/analyze-usage/scripts/view_session.py 2c052d83
The pipeline expects ~/.claude/projects/*.jsonl to be present. Override
the location by setting CLAUDE_PROJECTS_ROOT=/some/dir/projects (used by
the smoke tests; useful for sandboxed runs on someone else's logs).
The current extractors parse the jsonl logs directly — they do not
reuse /tmp/claude_analysis/sessions.parquet from research-pipeline.
A future extractor can short-circuit by reading that parquet if it
exists, but until that lands, every extractor walks ~/.claude/projects/
on each run.
Available extractors
| Script | Output (raw + anon) | What it computes |
|---|
extract_biome.py | biome.parquet | Whale / Shark / Dolphin / Fish / Shrimp / Plankton per session (main + subagents) |
extract_archetype.py | archetype.parquet | 8-class archetype (Variant B 45/25): Constructor / Operator / Researcher / Builder / Scholar / Inspector / Polymath / Discusser, plus rhythm class (Mono / Phased / Mixed / Chaos) |
extract_growth.py | growth.parquet, milestones.parquet | Per-session: n_bursts, daily R/LOC, CV, Gini, acceleration_R; milestone burst-index for thresholds 10/30/100/500 |
extract_outputs.py | outputs.parquet | Commits by type (feat/fix/refactor/chore/docs/test/ci) with review-fix vs prod-fix split, pushes, PR/MR/issue creation, comments, files edited/written, lines added/removed, unique branches/issues |
extract_subagents.py | subagent_pyramid.parquet | Per-parent distribution of subagent sizes (whale/shark/.../plankton bins) |
extract_compacts.py | compact_pattern.parquet | Per-session compact frequency, real_prompts before first compact, max context size, compact_per_asst |
extract_gaps.py | idle_gaps.parquet | Gaps >8h with suggested split points (long-form: one row per gap) |
extract_topics.py | topic_signatures.parquet | BoW per session (top-20 tokens). raw keeps tokens, anon hashes them; reverse dictionary in raw/_token_dict.json |
extract_burst_classes.py | burst_classes.parquet | Per-session counts: pure_qa / read_only / bash_only / write_only / write_heavy / chain_run / mixed |
extract_parallelism.py | parallelism.parquet | Hour-bin concurrent session counts (date+hour → cnt) |
| Other tool | Purpose |
|---|
period_report.py | Graphic monthly/weekly digest with metaphors (aquarium, palette, DORA radar, friction). Reads jsonl directly, no parquet needed. Args: --from --to --period monthly|weekly|both --format text|markdown|html|all [--out FILE | --out-dir DIR] [--open]. With --open auto-opens HTML in browser. |
extract_llm_session_names.py | Tier 2 stub: enqueues Whale/Shark/Dolphin sessions for narrative summarization by the agent. Writes outputs/llm/_queue.jsonl (and an empty session_names.parquet stub). Agent consumes the queue and appends rows. |
bin/analyze-usage | Standalone CLI wrapper. Subcommands: period, pipeline, session SID, audit, patterns, report, test. Add to PATH for non-agent usage. |
pipeline.py | Orchestrator — runs all 10 extractors sequentially (~4–5 min on a full corpus). Args: --since --outputs --only <names> |
view_session.py SID | Drill-down view of one session by sid prefix (raw jsonl required). Prints biome/archetype/rhythm + first 5 prompts + bash subcategory mix |
pattern_detector.py | Default: heuristic flags from raw/ parquets: needs_split (≥2 idle gaps), pure_constructor (Constructor+Phased), feature_with_debug (Inspector+compacts≥2), pipeline_rescue (compacts≥3 + LOC<500), ghost_session (Plankton+0 tools). With --audit-anon: walks anon/ parquets and asserts no raw UUIDs / abs paths / bash commands / branches / mcp slugs leak — exits 1 on any leak. |
report_generator.py | Markdown report skeleton from parquet tables → outputs/reports/<date>.md |
Library helpers
lib/ holds the shared building blocks. Extractors should import from there
rather than duplicate logic.
| Module | Provides |
|---|
lib/parsers.py | find_session_files(root=None), find_subagents(jsonl), parse_session(jsonl, since, until) → Iterator[Event], build_bursts(events, sid, project) → list[Burst], load_session(jsonl) → ParsedSession, split_chain(cmd). Honors CLAUDE_PROJECTS_ROOT env var. |
lib/classifiers.py | Pure-function classifiers: biome_of(real_prompts), archetype_of(edit, bash, read) (Variant B 45/25), rhythm_of(event_seq) (Mono / Phased / Mixed / Chaos / "—"), stack_of(file_path) (frontend / backend / infra / docs / config / other), bash_sub_of(cmd) (git / test / build / run / device / deploy / format / net / playwright_cli / shell / other), commit_type_of(first_line), is_review_fix(body). Plus emoji dicts: BIOME_EMOJI, ARCHETYPE_EMOJI, RHYTHM_EMOJI. |
lib/stats.py | cv(xs), gini(xs), shannon_entropy(counter), milestone_at(cum, threshold), acceleration(daily_xs), pearson(xs, ys), percentile(xs, p), median(xs), power_law_fit(xs, ys) (returns slope/intercept/r/n). |
lib/anonymize.py | hash_path(s) → p<6hex>, hash_token(w), hash_branch(name), hash_mcp_slug(name), SidProjector(prefix) (sequential s0001 / a0001 mapping), file_ext(path), k_anon_filter(rows, group_key) (drops groups with N<5), write_token_sidecar(dict, raw_dir). |
lib/io.py | outputs_dirs(base=None) → (raw, anon), outputs_dirs_v2(base=None) → (raw, anon, llm) (auto-mkdir), write_parquet(rows, path) (snappy, returns row count, empty marker if no rows). |
lib/render.py | Period-report renderers in three formats: render_period_terminal/markdown/html_section(label, agg), render_html_doc(title, sections), render_weekly_table_*, render_trends_*. Pure data → string. Used by period_report.py. |
Tests
tests/run_all.sh runs four test suites (~15 seconds total, 28 tests):
| Suite | Coverage | Count |
|---|
tests/test_classifiers.py | biome thresholds, archetype Variant B (pure / hybrid / too-few-tools), rhythm Mono / Chaos / too-short, stack classification (all extensions), bash sub regex coverage, conventional commit type, review-fix detection | 11 |
tests/test_stats.py | cv (empty + known), gini (extremes), Shannon entropy, milestone_at, pearson (perfect / zero), percentile / median, power_law_fit (y=2x² recovers slope=2), acceleration (const / accel / decel) | 10 |
tests/test_extractors_smoke.py | Builds a synthetic ~/.claude/projects/ fixture with 3 controlled sessions (Operator-Fish, Constructor with feat+fix+review-fix+push, ghost). Sets CLAUDE_PROJECTS_ROOT, runs all 10 extractors, asserts schemas + specific values (e.g. session2 must have feat=1, fix=2, review_fix=1, prod_fix=1, pushes=1, archetype=Constructor). | 1 (full pipeline) |
tests/test_audit_anon.py | Builds anon parquets with synthetic content; asserts pattern_detector.py --audit-anon returns 0 on clean fixture and 1 on each kind of leak (raw UUID, abs path, raw bash, raw branch in nested list, bad sid format). | 6 |
bash .claude/skills/analyze-usage/tests/run_all.sh
When you add an extractor, add a corresponding assertion to
test_extractors_smoke.py so future refactors stay honest.
Anonymization contract
When emitting to outputs/anon/:
session_id → sequential s0001 (main) or a0001 (subagent)
project_path → p<6-hex> SHA-1 truncation
file_path → drop, keep file_ext and file_type only
bash_command → drop, keep category + structural flags only
prompt_text → drop, keep chars, intent_flags, and token_hash BoW
branch_name → drop, keep branch_hash only
tool_use_id → drop
mcp_server_slug → hash, keep verb intact
- Any aggregate with N<5 sessions in the bucket → suppress (K=5 threshold)
The outputs/anon/ directory is auditable via pattern_detector.py --audit-anon: it walks every parquet, recursively scans every string-
typed column (including nested lists/dicts), and exits 1 if it finds
raw UUIDs, absolute paths, bash commands, branch names, or un-hashed
MCP slugs. Run after every pipeline regeneration before sharing.
Extending the pipeline
To add a new metric foo:
- Create
scripts/extract_foo.py using lib/parsers.py to load sessions
and lib/classifiers.py / lib/stats.py / lib/anonymize.py /
lib/io.py for the rest. Follow the shape of existing extractors
(CLI: --since YYYY-MM-DD, --outputs DIR).
- Emit two parquet files:
outputs/raw/foo.parquet (full data) and
outputs/anon/foo.parquet (anonymized via SidProjector + hash_path
- Append
extract_foo to the EXTRACTORS list in scripts/pipeline.py.
- Add a row to the
Available extractors table above.
- Add an assertion in
tests/test_extractors_smoke.py checking the new
schema + at least one value on the synthetic fixture.
- Run
bash tests/run_all.sh — must stay 28+/28+ green.
Do not assume only humans extend this — the agent itself adds extractors
when the user asks "what about " and the existing tables don't
cover it.