| name | research-pipeline |
| description | Rebuild the Claude Code agent-usage research dataset (sessions, loops, tool calls, gold-selection events, correlations) from ~/.claude/projects/*.jsonl logs into /tmp/claude_analysis/*.parquet. Use when the user wants to bootstrap analytics on a new machine, refresh after new sessions, validate reproducibility, or onboard the research pipeline after cloning this repo. |
| allowed-tools | Bash, Read |
research-pipeline
Reproducible pipeline for the Paper 1 (TrimTree) and related research. Parses
Claude Code session logs (~/.claude/projects/**/*.jsonl) into a family of
Parquet tables under /tmp/claude_analysis/, then optionally runs LLM
classification and correlation analyses.
All scripts are anonymizing-by-construction: no raw paths, session UUIDs, or
project slugs leave the pipeline. API keys resolve through environment
variables only.
Preconditions
uv installed โ curl -LsSf https://astral.sh/uv/install.sh | sh
- Repo cloned โ scripts live at
docs/research/scripts/*.py
- JSONL logs present โ
~/.claude/projects/ is populated automatically
by Claude Code; nothing to do if the user has used Claude Code before
- (Optional) z.ai key for LLM classification:
mkdir -p /tmp/claude_analysis
cat > /tmp/claude_analysis/.env.zai <<EOF
ZAI_API_KEY=<key>
ZAI_BASE_URL=https://api.z.ai/api/anthropic
ZAI_MODEL=glm-4.6
EOF
Pipeline DAG
~/.claude/projects/*.jsonl
โ
โผ
1. analyze_sessions.py โ core ETL + anonymizer
โ emits: sessions, turns, tool_calls, bash_commands,
โ human_turns, compactions, meta_events (parquet)
โผ
2. detect_workflow_patterns.py โ workflow labels
3. extract_loops.py โ 1 row per agent loop
4. enrich_loops.py โ cost, cache, trigger, success_proxy
5. compute_session_features.py โ 1 row per session, 225 features
โ
โผ
6. extract_loop_list_events.py โ MCP gold-selection events
7. extract_bash_list_events.py โ Bash file-search gold events
โ
โผ
8. llm_classify_bash_events.py โ (optional, needs z.ai key)
โ
โผ
9. mine_correlations.py โ pairwise feature correlations
10. partial_correlations.py โ controlling for session size
11. within_bucket_correlations.py โ per-bucket analyses
12. analyze_features.py โ human-readable summary tables
Steps 2-5 depend on step 1. Steps 6-7 depend on 1 and 3. Step 8 depends on 7.
Steps 9-12 depend on 5. Within each group you can parallelize if memory allows.
Run โ canonical sequence
Run from the repo root. Default outputs land in /tmp/claude_analysis/.
mkdir -p /tmp/claude_analysis
uv run docs/research/scripts/analyze_sessions.py
uv run docs/research/scripts/detect_workflow_patterns.py
uv run docs/research/scripts/extract_loops.py
uv run docs/research/scripts/enrich_loops.py
uv run docs/research/scripts/compute_session_features.py
uv run docs/research/scripts/extract_loop_list_events.py
uv run docs/research/scripts/extract_bash_list_events.py
uv run docs/research/scripts/llm_classify_bash_events.py \
--concurrency 10 --checkpoint-every 25
uv run docs/research/scripts/mine_correlations.py
uv run docs/research/scripts/partial_correlations.py
uv run docs/research/scripts/within_bucket_correlations.py
uv run docs/research/scripts/analyze_features.py
Total wall-clock: 10-20 minutes without LLM step; +~20 min with it.
Verify the pipeline succeeded
ls -la /tmp/claude_analysis/*.parquet
for f in /tmp/claude_analysis/*.parquet; do
echo -n "$(basename $f): "
uv run --with 'duckdb>=1.0' python3 -c "
import duckdb; print(duckdb.sql(\"SELECT COUNT(*) FROM '$f'\").fetchone()[0])"
done
Expected tables after a full run (names, typical row magnitude):
| File | ~rows | Contents |
|---|
| sessions_enriched.parquet | ~3k | per-session, 225 columns |
| loops_enriched.parquet | ~15k | per-agent-loop with cost/cache/outcome |
| turns.parquet | ~700k | every turn (human/agent/tool-result) |
| tool_calls.parquet | ~250k | every tool invocation |
| bash_commands.parquet | ~100k | every Bash command |
| human_turns.parquet | ~20k | user messages with intent labels |
| meta_events.parquet | ~370k | lifecycle events (start/resume/compact) |
| bash_list_events.parquet | ~4k | Bash gold-selection events |
| bash_events_classified.parquet | ~4k | (if step 8 ran) LLM-categorized |
| compactions.parquet | ~900 | context compactions |
| workflow_patterns.parquet | ~25k | phase labels |
Quick analytics (after pipeline)
A few one-liners to confirm data is real:
uv run --with 'duckdb>=1.0' python3 -c "
import duckdb; D='/tmp/claude_analysis'
print(duckdb.sql(f\"\"\"
SELECT COUNT(*) n_sessions,
SUM(agent_loops) n_loops,
ROUND(SUM(in_tokens_total+out_tokens_total+cache_read_total+cache_create_total)/1e9, 2) tokens_b
FROM '{D}/sessions_enriched.parquet'
\"\"\").fetchdf())"
uv run --with 'duckdb>=1.0' python3 -c "
import duckdb; D='/tmp/claude_analysis'
print(duckdb.sql(f\"\"\"
SELECT top_model, COUNT(*) n FROM '{D}/sessions_enriched.parquet'
WHERE top_model IS NOT NULL GROUP BY 1 ORDER BY n DESC LIMIT 10
\"\"\").fetchdf())"
Known issues (document as you find them)
-
classify_file misclassifies tests as code โ analyze_sessions.py:505.
.test.ts, _test.py, *.spec.tsx, .feature files end up in writes_code
instead of writes_test. Fix: reorder conditions in classify_file so the
test/spec pattern check runs before the code-extension check. After fix,
rerun from step 1.
-
Model pricing in enrich_loops.py is a snapshot โ if Anthropic changes
rates, cost_usd becomes approximate. Fix by editing the MODEL_PRICING
dict in enrich_loops.py and rerunning step 4.
-
/tmp may be cleared on reboot (macOS) โ if you want durable storage,
pass --out-dir ~/claude_analysis (where supported) or rsync the parquet
tree to a permanent location after each run.
-
Sessions with ts_ms outside their own [ts_start_ms, ts_end_ms] โ
23 sessions (~6M turns) have this artifact, probably from resume/replay
semantics. For concurrency analyses, clamp active_ms BETWEEN session_start AND session_end to avoid inflated counts.
Portability โ running on a new machine
Three scenarios:
A. Fresh dataset on another machine (different Claude Code account)
git clone <repo-url> && cd devboy-tools
curl -LsSf https://astral.sh/uv/install.sh | sh
B. Move this machine's corpus to another
rsync -avz --exclude='*.tmp' ~/.claude/projects/ target:~/.claude/projects/
C. Copy pre-computed parquets (no recompute)
tar czf /tmp/claude_analysis.tgz -C /tmp claude_analysis/
scp /tmp/claude_analysis.tgz target:/tmp/
tar xzf /tmp/claude_analysis.tgz -C /tmp/
Anonymization guarantees (never emitted)
- Full user-turn text (only character counts + intent labels)
- Session UUIDs (aggregates or stable hash prefixes only)
- Raw file paths from codebases (only extensions, depths, token counts)
- MCP project slugs (hashed via
anonymize_tool_name in analyze_sessions.py)
- API keys, host IPs, user names, emails
Before copying any derived CSV/parquet into docs/research/data/ (the
committed public directory), run the pre-publish checklist from
docs/research/benchmarks/paper1/anonymization_rules.md.
When to rerun
- Step 1 only โ if you just want updated numbers with no feature changes
(new sessions since last run). Steps 2-11 are fast; running everything
from 1 takes ~15 min and guarantees consistency.
- All steps โ after editing a script (e.g. fixing the
classify_file
bug); upstream outputs propagate.
- Step 8 selectively โ re-classify only new bash events by using the
--resume flag so prior classifications are kept.
Related docs
docs/research/scripts/README.md โ DAG + anonymization rules
docs/research/paper-1-trimtree.md โ research context and claims
docs/research/benchmarks/paper1/TASK.md (local-only, gitignored) โ
public benchmark continuation plan