Bucket session clock time from ~/.claude/logs/{timings,invocations}.jsonl into:
- Local tools — Bash/Read/Edit/Write/Grep/Glob and other main-process tools
- Agent / subagent spawns — Task/Agent calls (sync + background)
- Skill —
tool=Skillwall durations - AskUserQuestion idle — human-wait, separate column, excluded from compute total
- Main-loop reasoning (residual) — session wall minus buckets above
...and bucket session tokens/cost from Claude Code transcripts (~/.claude/projects/**, main-loop + subagent files) into:
- Sessions ranked by cost (window mode) — main $ vs subagent $ vs total $ per session, plus a per-command rollup; or one session's deep-dive (
--session-id) — cost by main/sidechain × model tier, agent roster, top cache-rebuild calls, cold-start share
Outputs a markdown report at .reports/profile/<UTC-timestamp>/report.md plus a .temp/output-profile-...md copy: per-session clock table, per-skill clock rollup, top-N longest single calls, plus a ## Tokens & cost section scoped to the same window / --session-id.
NOT for: per-line Python perf (use foundry:perf-optimizer); known failure diagnosis (use /foundry:investigate); a real billing statement (prices are public list rates, not effective plan rates).
--since DURATION(default24h) — window:NNs|NNm|NNh|NNd--session-id ID— optional; restrict to one session--top-n N(default5) — slowest single calls to list
If $ARGUMENTS empty, default window is 24h.
Task tracking: TaskCreate two tasks up front — 1 "Run analyzers + render report" (Steps 1–3), 2 "Step 4b: Print report header" (Step 4). Mark each in_progress before its first tool call; 1 completed once report.md exists, 2 completed right after the header and path are printed, before the executive summary.
Step 1: Parse args + create run dir
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# timeout: 5000
SINCE="24h"
SESSION_ID=""
TOP_N="5"
for tok in $ARGUMENTS; do
case "$tok" in
--since=*) SINCE="${tok#--since=}" ;;
--since) next_is_since=1 ;;
--session-id=*) SESSION_ID="${tok#--session-id=}" ;;
--top-n=*) TOP_N="${tok#--top-n=}" ;;
*)
if [ "${next_is_since:-0}" = "1" ]; then SINCE="$tok"; next_is_since=0; fi
;;
esac
done
STAMP="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
REPORT_DIR=".reports/profile/$STAMP"
mkdir -p "$REPORT_DIR"
{
echo "REPORT_DIR=$REPORT_DIR"
echo "SINCE=$SINCE"
echo "SESSION_ID=$SESSION_ID"
echo "TOP_N=$TOP_N"
} | tee "${TMPDIR:-/tmp}/foundry-profile-state-${CSID}"
Values persisted to ${TMPDIR:-/tmp}/foundry-profile-state-${CSID}; Steps 2–3 re-source it (bash state does not persist across Bash calls, and REPORT_DIR carries a per-shell timestamp that cannot be re-derived).
Step 2: Run analyzers — clock time, then tokens/cost
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# timeout: 60000
. "${TMPDIR:-/tmp}/foundry-profile-state-${CSID}" 2>/dev/null # reload REPORT_DIR/SINCE/SESSION_ID/TOP_N (fresh shell)
OPT_SID=""
[ -n "$SESSION_ID" ] && OPT_SID="--session-id $SESSION_ID"
python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_foundry}/bin/timing_analyzer.py" \
--since "$SINCE" \
--top-n "$TOP_N" \
--output "$REPORT_DIR/report.md" \
$OPT_SID 2>"$REPORT_DIR/warnings.log"
echo "time_exit=$?"
OPT_SID left unquoted so empty expands to nothing (no flag). Exit code 1 → no sessions in window — surface that and stop; nothing in Step 2b can rescue a missing clock report, since enforce-profile-header.js keys on report.md specifically.
Step 2b: Run analyzer — tokens/cost (best-effort, appended)
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# timeout: 60000
. "${TMPDIR:-/tmp}/foundry-profile-state-${CSID}" 2>/dev/null # reload REPORT_DIR/SINCE/SESSION_ID/TOP_N (fresh shell)
OPT_SID=""
[ -n "$SESSION_ID" ] && OPT_SID="--session-id $SESSION_ID"
python "${CLAUDE_PLUGIN_ROOT:-plugins/cc_foundry}/bin/cost_analyzer.py" \
--since "$SINCE" \
--top-n "$TOP_N" \
--output "$REPORT_DIR/cost.md" \
$OPT_SID 2>>"$REPORT_DIR/warnings.log"
COST_EXIT=$?
if [ "$COST_EXIT" -eq 0 ] && [ -s "$REPORT_DIR/report.md" ]; then
printf '\n' >> "$REPORT_DIR/report.md"
cat "$REPORT_DIR/cost.md" >> "$REPORT_DIR/report.md"
fi
echo "cost_exit=$COST_EXIT"
Separate data source (transcripts under ~/.claude/projects/**, not timings.jsonl), separate script, same --since/--session-id/--top-n scoping. cost_exit=1 means no transcripts fell in the window (or no match for --session-id) — that is not fatal: the clock report still ships without a ## Tokens & cost section. The two analyzers never share a report path write; only report.md is append-target and only after cost_exit=0, so a cost-side failure cannot corrupt or block the clock report.
Step 3: Mark run complete
export CSID="${CLAUDE_CODE_SESSION_ID:-$PPID}"
# timeout: 5000
. "${TMPDIR:-/tmp}/foundry-profile-state-${CSID}" 2>/dev/null # reload REPORT_DIR/SINCE/SESSION_ID/TOP_N (fresh shell)
echo '{"status":"complete","since":"'"$SINCE"'","session_id":"'"$SESSION_ID"'","top_n":'"$TOP_N"'}' > "$REPORT_DIR/result.jsonl"
Step 4: Emit terminal output
Read YAML header from $REPORT_DIR/report.md (first block between --- lines) and render as a two-column Markdown table (Field | Value, one row per key, file order) per quality-gates.md §Report File Format's Universal terminal-print rule — never print the raw ----delimited block. Then print → $REPORT_DIR/report.md. Then read Headline split block plus top 3 sessions from per-session table, and — when a ## Tokens & cost section is present — the window total or single-session total cost line, and surface both as executive summary (per quality-gates.md output routing).
Also Write the long-output dump per quality-gates rule:
Write(file_path=".temp/output-profile-<branch>-<YYYY-MM-DD>.md", content=<full report contents>)
Where <branch> = $(git branch --show-current 2>/dev/null | tr '/' '-' || echo 'main').
Backed structurally by hooks/enforce-profile-header.js: while the Step 1 state file is live and $REPORT_DIR/report.md is absent, Step 5's AskUserQuestion is denied, so the gate can never be reached from an ad-hoc in-context summary.
Step 5: Follow-up gate
Invoke AskUserQuestion (denied by enforce-profile-header.js until Step 2 has written report.md — if the analyzer found no sessions, report that and stop instead of asking):
- (a) Drill into slowest session — re-run with
--session-id <id> - (b) Re-run with different window (
--since 7d,--since 30d) - (c) Skip — done
- Scope vs
/foundry:investigate: investigate diagnoses failures; profile measures wall time when things ran fine but slow. - Scope vs
foundry:perf-optimizer: perf-optimizer profiles Python/ML code (CPU/GPU/IO); profile measures Claude Code session wall time. - No model split on the clock side: timings.jsonl
modelfield is 100% null in the current task-log.js payload — the clock report does not break down by model tier. Documented in report Confidence Gaps. The## Tokens & costsection does break down by tier, but from a different data source (transcripts, not timings.jsonl) — the two never merge into one table. - Subagent internals invisible to the clock side:
timings.jsonl's hook fires only in the main Claude Code process; subagent internal tool calls do NOT hit it, so the reasoning bucket underestimates when subagent spawns dominate. The cost side does not have this gap — it reads each session'ssubagents/agent-*.jsonltranscripts directly, so subagent token spend is included, not inferred. - Background agent join: rows with
duration_ms < 1s(likelyrun_in_background=true) are matched against invocations.jsonlstarted→completedpairs by(agent, desc)substring within ±60s window. Concurrent same-type spawns may mispair. - Bash clip: any
duration_ms > 1hfortool=Bashis clipped at 3,600,000 ms to avoid runaway-shell pollution; clip count surfaced in legend. - Cost window filtering:
cost_analyzer.pyuses each transcript's own rowtimestampfield for the--sincecutoff (file mtime is only a cheap pre-filter to skip old files before parsing them), so the window is as accurate as the clock side's. - Prices are public list rates:
## Tokens & costdollar figures are proportional truth (useful for ranking and comparing), not a billing statement — effective plan rates may differ. Per-commandsession $also attributes a session's whole cost to every command it ran, so that column ranks, never sums. - Cost section is best-effort: if
cost_analyzer.pyfinds no transcripts in the window (or no match for--session-id), it exits 1 and the## Tokens & costsection is simply omitted — the clock report still ships. - Read-only: skill never edits source files. No commits, no pushes.