---
title: "Analytics"
description: "Queries local analytics across OrchestKit projects for agent usage, skill frequency, hook timing, team activity, session replay, cost estimation, and model delegation trends. Privacy-safe with hashed project IDs. Supports time-range filtering and comparative analysis. Use when reviewing performance, estimating costs, or understanding usage patterns."
---

<span className="badge badge-gray">Reference</span> <span className="badge badge-green">low</span>

> **Auto-activated** — this skill loads automatically when Claude detects matching context.

<ContextualSkillSidebar slug="analytics" />


# Cross-Project Analytics

Query local analytics data from `~/.claude/analytics/`. All data is local-only, privacy-safe (hashed project IDs, no PII).

## Subcommands

Parse the user's argument to determine which report to show. If no argument provided, use AskUserQuestion to let them pick.

| Subcommand | Description | Data Source | Reference |
|------------|-------------|-------------|-----------|
| `agents` | Top agents by frequency, duration, model breakdown | `agent-usage.jsonl` | `$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md` |
| `models` | Model delegation breakdown (opus/sonnet/haiku) | `agent-usage.jsonl` | `$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md` |
| `skills` | Top skills by invocation count | `skill-usage.jsonl` | `$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md` |
| `hooks` | Slowest hooks and failure rates | `hook-timing.jsonl` | `$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md` |
| `teams` | Team spawn counts, idle time, task completions | `team-activity.jsonl` | `$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md` |
| `session` | Replay a session timeline with tools, tokens, timing | CC session JSONL | `$\{CLAUDE_SKILL_DIR\}/references/session-replay.md` |
| `cost` | Token cost estimation with cache savings | `stats-cache.json` | `$\{CLAUDE_SKILL_DIR\}/references/cost-estimation.md` |
| `trends` | Daily activity, model delegation, peak hours | `stats-cache.json` | `$\{CLAUDE_SKILL_DIR\}/references/trends-analysis.md` |
| `summary` | Unified view of all categories | All files | `$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md` |
| `otel` | CC 2.1.117 + 2.1.122 + 2.1.126 OTEL enrichments: top slash commands (user vs model), per-effort cost, effort-vs-success correlation, skill activation by trigger type, most-mentioned `@` targets | `~/.claude/otel/*.jsonl` | `$\{CLAUDE_SKILL_DIR\}/references/otel-fields.md` |

### Quick Start Example

```bash
# Top agents with model breakdown
jq -s 'group_by(.agent) | map({agent: .[0].agent, count: length}) | sort_by(-.count)' ~/.claude/analytics/agent-usage.jsonl

# All-time token costs
jq '.modelUsage | to_entries | map({model: .key, input: .value.inputTokens, output: .value.outputTokens})' ~/.claude/stats-cache.json
```

### Quick Subcommand Guide

**`agents`, `models`, `skills`, `hooks`, `teams`, `summary`** — Run the jq query from `Read("$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md")` for the matching subcommand. Present results as a markdown table.

**`session`** — Follow the 4-step process in `Read("$\{CLAUDE_SKILL_DIR\}/references/session-replay.md")`: locate session file, resolve reference (latest/partial/full ID), parse JSONL, present timeline.

**`cost`** — Apply model-specific pricing from `Read("$\{CLAUDE_SKILL_DIR\}/references/cost-estimation.md")` to CC's stats-cache.json. Show per-model breakdown, totals, and cache savings.

**`trends`** — Follow the 4-step process in `Read("$\{CLAUDE_SKILL_DIR\}/references/trends-analysis.md")`: daily activity, model delegation, peak hours, all-time stats.

**`summary`** — Run all subcommands and present a unified view: total sessions, top 5 agents, top 5 skills, team activity, unique projects. If `~/.claude/otel/*.jsonl` exists with non-empty content, append the three OTEL panels from `otel-fields.md`; otherwise omit them (do not render empty panels).

**`otel`** — Render the OTEL panels: 3 from CC 2.1.117 (top slash commands user-vs-model, per-effort cost, effort-vs-success correlation), 3 from CC 2.1.119 (oversized inputs, pre/post latency, see `otel-fields.md`), 1 from CC 2.1.122 (most-mentioned `@` targets), and 1 from CC 2.1.126 (skill activation by trigger type). See `Read("$\{CLAUDE_SKILL_DIR\}/references/otel-fields.md")` for queries, graceful-fallback rules, and panel semantics. Each panel falls back cleanly to "no OTEL data available (upgrade to CC ≥ X)" when its specific file is absent or empty — render only the panels with data.

## Data Files

Load `Read("$\{CLAUDE_SKILL_DIR\}/references/data-locations.md")` for complete data source documentation.

| File | Contents |
|------|----------|
| `agent-usage.jsonl` | Agent spawn events with model, duration, success |
| `skill-usage.jsonl` | Skill invocations |
| `hook-timing.jsonl` | Hook execution timing and failure rates |
| `session-summary.jsonl` | Session end summaries |
| `task-usage.jsonl` | Task completions |
| `team-activity.jsonl` | Team spawns and idle events |

## Rules

Each category has individual rule files in `rules/` loaded on-demand:

| Category | Rule | Impact | Key Pattern |
|----------|------|--------|-------------|
| Data Integrity | `$\{CLAUDE_SKILL_DIR\}/rules/data-privacy.md` | CRITICAL | Hash project IDs, never log PII, local-only |
| Cost & Tokens | `$\{CLAUDE_SKILL_DIR\}/rules/cost-calculation.md` | HIGH | Separate pricing per token type, cache savings |
| Performance | `$\{CLAUDE_SKILL_DIR\}/rules/large-file-streaming.md` | HIGH | Streaming jq for >50MB, rotation-aware queries |
| Visualization | `$\{CLAUDE_SKILL_DIR\}/rules/visualization-recharts.md` | HIGH | Recharts charts, ResponsiveContainer, tooltips |
| Visualization | `$\{CLAUDE_SKILL_DIR\}/rules/visualization-dashboards.md` | HIGH | Dashboard grids, stat cards, widget registry |

**Total: 5 rules across 4 categories**

## References

| Reference | Contents |
|-----------|----------|
| `$\{CLAUDE_SKILL_DIR\}/references/jq-queries.md` | Ready-to-run jq queries for all JSONL subcommands |
| `$\{CLAUDE_SKILL_DIR\}/references/session-replay.md` | Session JSONL parsing, timeline extraction, presentation |
| `$\{CLAUDE_SKILL_DIR\}/references/cost-estimation.md` | Pricing table, cost formula, daily cost queries |
| `$\{CLAUDE_SKILL_DIR\}/references/trends-analysis.md` | Daily activity, model delegation, peak hours queries |
| `$\{CLAUDE_SKILL_DIR\}/references/data-locations.md` | All data sources, file formats, CC session structure |
| `$\{CLAUDE_SKILL_DIR\}/references/otel-fields.md` | CC 2.1.117 OTEL fields (command_name, command_source, effort), queries, and dashboard panels |

## Important Notes

- All files are JSONL (newline-delimited JSON) format
- For large files (>50MB), use streaming `jq` without `-s` — load `Read("$\{CLAUDE_SKILL_DIR\}/rules/large-file-streaming.md")`
- Rotated files: `&lt;name&gt;.&lt;YYYY-MM&gt;.jsonl` — include for historical queries
- `team` field only present during team/swarm sessions
- `pid` is a 12-char SHA256 hash — irreversible, for grouping only

## Output Format

Present results as clean markdown tables. Include counts, percentages, and averages. If a file doesn't exist, note that no data has been collected yet for that category.

## Related Skills

- `ork:explore` - Codebase exploration and analysis
- `ork:feedback` - Capture user feedback
- `ork:remember` - Store project knowledge
- `ork:doctor` - Health check diagnostics


---

## Rules (5)

### Calculate token costs accurately by separating cache reads from regular input pricing — HIGH


## Token Cost Calculation

Calculate accurate token costs using model-specific pricing with cache-aware formulas.

**Incorrect — treating all tokens equally:**
```typescript
// WRONG: ignores cache pricing difference (10x cheaper for reads)
const cost = totalTokens / 1_000_000 * 5.00;
```

**Correct — separate pricing per token type:**
```typescript
const mtok = 1_000_000;
const pricing = { input: 5.00, output: 25.00, cache_read: 0.50, cache_write: 6.25 };

const cost =
  (tokens.input / mtok) * pricing.input +
  (tokens.output / mtok) * pricing.output +
  (tokens.cache_read / mtok) * pricing.cache_read +
  (tokens.cache_write / mtok) * pricing.cache_write;

// Cache savings: what it would cost if cache reads were full-price input
const withoutCache =
  ((tokens.input + tokens.cache_read) / mtok) * pricing.input +
  (tokens.output / mtok) * pricing.output;

const savings = withoutCache - cost;
```

**Key rules:**
- Always calculate 4 token types separately: input, output, cache_read, cache_write
- Cache reads are 10x cheaper than regular input — this is the biggest cost factor
- Show cache savings prominently — users want to know caching is working
- When daily data only has total tokens (no split), estimate 70% input / 30% output
- Use `formatCost()` from `cost-estimator.ts` for consistent formatting
- Pricing is user-overridable via `~/.claude/orchestkit-pricing.json`


### Protect analytics data privacy by hashing identifiers and stripping sensitive fields — CRITICAL


## Analytics Data Privacy

All analytics data must be local-only and privacy-safe. Never log PII or reversible identifiers.

**Incorrect — logging raw paths and usernames:**
```typescript
// WRONG: raw project path is PII
appendAnalytics('agent-usage.jsonl', {
  project: process.env.CLAUDE_PROJECT_DIR,  // /Users/john/secret-project
  user: os.userInfo().username,              // john
  file: input.file_path,                     // /Users/john/secret-project/auth.ts
});
```

**Correct — hashed identifiers, no PII:**
```typescript
// RIGHT: irreversible 12-char hash, no PII
appendAnalytics('agent-usage.jsonl', {
  ts: new Date().toISOString(),
  pid: hashProject(process.env.CLAUDE_PROJECT_DIR || ''),  // "a3f8b2c1d4e5"
  agent: agentType,       // "code-quality-reviewer" (not PII)
  model: modelName,       // "claude-opus-4-7" (not PII)
  duration_ms: durationMs,
  success: true,
});
```

**Key rules:**
- Use `hashProject()` (12-char SHA256 truncation) for project identifiers — irreversible
- Never log file paths, usernames, environment variables, or file contents
- Agent names, skill names, and hook names are safe to log (not PII)
- All data stays in `~/.claude/analytics/` — never transmitted externally
- The `team` field uses team names (user-chosen), not paths


### Stream large analytics files with jq instead of slurping to prevent OOM crashes — HIGH


## Large File Streaming

Handle large JSONL files (>50MB) with streaming queries and rotation-aware patterns.

**Incorrect — slurping large files into memory:**
```bash
# WRONG: -s loads entire file into memory — OOM on 500MB file
jq -s 'group_by(.agent) | map({agent: .[0].agent, count: length})' ~/.claude/analytics/agent-usage.jsonl
```

**Correct — streaming without slurp:**
```bash
# RIGHT: stream-process line by line, then aggregate
jq -r '.agent' ~/.claude/analytics/agent-usage.jsonl | sort | uniq -c | sort -rn

# RIGHT: for complex aggregations, use reduce
jq -n '[inputs | .agent] | group_by(.) | map({agent: .[0], count: length}) | sort_by(-.count)' ~/.claude/analytics/agent-usage.jsonl
```

**Including rotated files for historical queries:**
```bash
# Rotated files follow pattern: <name>.<YYYY-MM>.jsonl
# Include all months for full history
jq -r '.agent' ~/.claude/analytics/agent-usage.*.jsonl ~/.claude/analytics/agent-usage.jsonl 2>/dev/null | sort | uniq -c | sort -rn
```

**Key rules:**
- Check file size before querying: `ls -lh` the target file
- Files >50MB: use streaming `jq` without `-s` (slurp) flag
- Files &lt;50MB: `-s` is fine for `group_by` operations
- Include rotated files (`*.YYYY-MM.jsonl`) when user asks for historical data
- For date-range queries, filter by `ts` field before aggregating


### Design dashboard layouts with shared query keys and grid widgets for performance — HIGH


## Dashboard Layout & Widgets

Build responsive dashboard grids with stat cards, widget composition, and real-time data patterns.

**Incorrect — each widget fetches independently:**
```tsx
// WRONG: 5 widgets = 5 duplicate API calls
function Dashboard() {
  return (
    <div>
      <RevenueWidget /> {/* fetches /api/metrics */}
      <UsersWidget />   {/* fetches /api/metrics AGAIN */}
      <OrdersWidget />  {/* fetches /api/metrics AGAIN */}
    </div>
  );
}
```

**Correct — shared query with responsive grid layout:**
```tsx
// Dashboard grid with responsive breakpoints
function DashboardGrid() {
  return (
    <div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
      <StatCard title="Revenue" value="$45,231" change="+12%" trend="up" />
      <StatCard title="Users" value="2,350" change="+5.2%" trend="up" />
      <StatCard title="Orders" value="1,234" change="-2.1%" trend="down" />
      <StatCard title="Conversion" value="3.2%" change="+0.4%" trend="up" />

      {/* Full-width chart spanning all columns */}
      <div className="col-span-full">
        <RevenueChart />
      </div>

      {/* Two-column layout for secondary charts */}
      <div className="col-span-1 lg:col-span-2">
        <TrafficChart />
      </div>
      <div className="col-span-1 lg:col-span-2">
        <TopProductsTable />
      </div>
    </div>
  );
}

// Stat card component
function StatCard({
  title, value, change, trend,
}: {
  title: string; value: string; change: string; trend: 'up' | 'down';
}) {
  return (
    <div className="rounded-lg border bg-card p-6">
      <p className="text-sm text-muted-foreground">{title}</p>
      <p className="text-2xl font-bold">{value}</p>
      <p className={trend === 'up' ? 'text-green-600' : 'text-red-600'}>
        {change}
      </p>
    </div>
  );
}
```

**Widget registry pattern for dynamic dashboards:**
```tsx
const widgetRegistry: Record<string, React.ComponentType<WidgetProps>> = {
  'stat-card': StatCard,
  'line-chart': LineChartWidget,
  'bar-chart': BarChartWidget,
  'data-table': DataTableWidget,
};

function DynamicDashboard({ config }: { config: DashboardConfig }) {
  return (
    <div className="grid gap-4 grid-cols-12">
      {config.widgets.map((widget) => {
        const Widget = widgetRegistry[widget.type];
        return (
          <div key={widget.id} className={`col-span-${widget.colSpan}`}>
            <Suspense fallback={<WidgetSkeleton />}>
              <Widget {...widget.props} />
            </Suspense>
          </div>
        );
      })}
    </div>
  );
}
```

**Real-time updates with SSE + TanStack Query:**
```tsx
function useRealtimeMetrics() {
  const queryClient = useQueryClient();

  useEffect(() => {
    const source = new EventSource('/api/metrics/stream');
    source.onmessage = (event) => {
      const metric = JSON.parse(event.data);
      // Update specific query, not entire dashboard
      queryClient.setQueryData(['metrics', metric.key], metric.value);
    };
    return () => source.close();
  }, [queryClient]);
}
```

**Key rules:**
- Use CSS Grid with responsive breakpoints (`grid-cols-1 sm:grid-cols-2 lg:grid-cols-4`)
- Share data via TanStack Query with granular query keys (not per-widget fetch)
- Use `col-span-full` for full-width charts, `col-span-2` for half-width
- Skeleton loading for content areas during initial load
- SSE for server-to-client real-time, WebSocket for bidirectional
- Update specific query keys on real-time events, not entire cache


### Configure Recharts with ResponsiveContainer and animation control for stable rendering — HIGH


## Recharts Chart Components

Build Recharts 3.x chart components with responsive containers, custom tooltips, and accessibility.

**Incorrect — chart without responsive container:**
```tsx
// WRONG: Fixed width, no container, animations on real-time data
function BrokenChart({ data }: { data: ChartData[] }) {
  return (
    <LineChart width={800} height={400} data={data}>
      {/* Fixed width overflows on mobile */}
      {/* Animation on every data update = jank */}
      <Line type="monotone" dataKey="value" />
    </LineChart>
  );
}
```

**Correct — responsive chart with proper setup:**
```tsx
import {
  LineChart, Line, BarChart, Bar, PieChart, Pie, Cell,
  CartesianGrid, XAxis, YAxis, Tooltip, Legend,
  ResponsiveContainer, AreaChart, Area,
} from 'recharts';

// Line chart (trends over time)
function RevenueChart({ data }: { data: ChartData[] }) {
  return (
    <div className="h-[400px]"> {/* Parent MUST have height */}
      <ResponsiveContainer width="100%" height="100%">
        <LineChart data={data}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="date" />
          <YAxis />
          <Tooltip content={<CustomTooltip />} />
          <Legend />
          <Line
            type="monotone"
            dataKey="revenue"
            stroke="#8884d8"
            strokeWidth={2}
            dot={{ r: 4 }}
          />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

// Custom tooltip for branded UX
function CustomTooltip({ active, payload, label }: any) {
  if (!active || !payload?.length) return null;
  return (
    <div className="rounded-lg border bg-background p-3 shadow-md">
      <p className="font-medium">{label}</p>
      {payload.map((entry: any, i: number) => (
        <p key={i} style={{ color: entry.color }}>
          {entry.name}: {entry.value.toLocaleString()}
        </p>
      ))}
    </div>
  );
}

// Real-time chart: disable animations
function LiveMetricChart({ data }: { data: MetricData[] }) {
  return (
    <ResponsiveContainer width="100%" height={300}>
      <AreaChart data={data}>
        <Area
          type="monotone"
          dataKey="value"
          isAnimationActive={false}  // No animation on real-time data
          dot={false}                // No dots for performance
        />
      </AreaChart>
    </ResponsiveContainer>
  );
}

// Accessible chart with figure role
function AccessibleChart({ data, title }: { data: ChartData[]; title: string }) {
  return (
    <figure role="figure" aria-label={title}>
      <figcaption className="sr-only">{title}</figcaption>
      <ResponsiveContainer width="100%" height={400}>
        <BarChart data={data}>
          <Bar dataKey="value" fill="#8884d8" />
        </BarChart>
      </ResponsiveContainer>
    </figure>
  );
}
```

**Chart type selection guide:**

| Chart | Component | Best For |
|-------|-----------|----------|
| Line | `LineChart` | Trends over time |
| Bar | `BarChart` | Comparisons between categories |
| Pie/Donut | `PieChart` with `innerRadius` | Proportions/percentages |
| Area | `AreaChart` with gradient | Volume over time |

**Key rules:**
- Always wrap charts in `ResponsiveContainer` with a parent that has explicit height
- Disable animations on real-time/frequently-updating charts (`isAnimationActive=\{false\}`)
- Use custom tooltips for branded UX instead of default
- Add `figure` role and `aria-label` for accessibility
- Limit data points to prevent rendering performance issues
- Memoize data calculations outside the render function



---

## References (6)

### Cost Estimation

# Cost Estimation

Estimate token costs from CC's `~/.claude/stats-cache.json` using model-specific pricing.

## Pricing Table (Feb 2026)

| Model | Input/MTok | Output/MTok | Cache Read/MTok | Cache Write/MTok |
|-------|-----------|------------|----------------|-----------------|
| claude-opus-4-6 | $5.00 | $25.00 | $0.50 | $6.25 |
| claude-sonnet-4-6 | $3.00 | $15.00 | $0.30 | $3.75 |
| claude-haiku-4-5 | $1.00 | $5.00 | $0.10 | $1.25 |

## Cost Formula

```
cost = (input_tokens / 1M * input_price)
     + (output_tokens / 1M * output_price)
     + (cache_read_tokens / 1M * cache_read_price)
     + (cache_write_tokens / 1M * cache_write_price)
```

**Cache savings** = cost if all cache reads were full-price input minus actual cost.

## All-Time Model Usage Query

```bash
jq '.modelUsage | to_entries | map({
  model: .key,
  input: .value.inputTokens,
  output: .value.outputTokens,
  cache_read: .value.cacheReadInputTokens,
  cache_write: .value.cacheCreationInputTokens
})' ~/.claude/stats-cache.json
```

## Daily Costs (Last 7 Days)

```bash
jq '.dailyModelTokens[-7:] | .[] | {date: .date, tokens: .tokensByModel}' ~/.claude/stats-cache.json
```

Note: `dailyModelTokens` only has total tokens per model, not split by type. Estimate with 70% input / 30% output ratio as a rough average for CC usage.

## Presentation Format

```markdown
## Token Cost Estimate

| Model | Input Tokens | Output Tokens | Cache Read | Cache Write | Est. Cost |
|-------|-------------|--------------|------------|-------------|-----------|
| claude-opus-4-6 | 5.2M | 1.4M | 42.0M | 2.1M | $16.20 |
| claude-sonnet-4-6 | 200K | 50K | -- | -- | $1.85 |
| **Total** | | | | | **$18.50** |

**Cache savings:** $8.20 (what it would cost without prompt caching)

### Daily Costs (Last 7 Days)
| Date | Est. Cost |
|------|-----------|
| Feb 12 | $2.10 |
| Feb 13 | $1.85 |
| **Total** | **$18.50** |
```

## User-Overridable Config

Users can override pricing by creating `~/.claude/orchestkit-pricing.json` — see `src/hooks/src/lib/cost-estimator.ts` for the schema.


### Data Locations

# Data Sources & File Locations

All analytics data sources used by the analytics skill.

## OrchestKit Analytics Files

Location: `~/.claude/analytics/`

| File | Contents | Key Fields |
|------|----------|-----------|
| `agent-usage.jsonl` | Agent spawn events | `ts, pid, agent, model, duration_ms, success, output_len, team?` |
| `skill-usage.jsonl` | Skill invocations | `ts, pid, skill, team?` |
| `hook-timing.jsonl` | Hook execution timing | `ts, hook, duration_ms, ok, pid, team?` |
| `session-summary.jsonl` | Session end summaries | `ts, pid, total_tools, team?` |
| `task-usage.jsonl` | Task completions | `ts, pid, task_status, duration_ms, team?` |
| `team-activity.jsonl` | Team spawns and idle | `ts, pid, event, agent, member?, idle_ms?, model?, team` |

## CC Native Data Sources

| Source | Path | Contents |
|--------|------|----------|
| CC session logs | `~/.claude/projects/\{encoded-path\}/*.jsonl` | Full conversation with per-turn token usage |
| CC stats cache | `~/.claude/stats-cache.json` | Pre-aggregated daily model tokens, session counts |
| CC history | `~/.claude/history.jsonl` | Command history across all projects |

## JSONL Format Notes

- All OrchestKit files use newline-delimited JSON (JSONL)
- Each line is a self-contained JSON object
- Rotated files follow pattern `&lt;name&gt;.&lt;YYYY-MM&gt;.jsonl` — include them in queries for historical data
- The `team` field is only present for entries recorded during team/swarm sessions
- `pid` is a 12-char SHA256 hash of the project path — irreversible, used for grouping

## CC Session JSONL Structure

Each line in a CC session JSONL file is a JSON object. Key entry types:

| Entry Pattern | How to Identify | Key Fields |
|---------------|-----------------|------------|
| Session metadata | Has `sessionId`, `gitBranch`, `version` | First entries in file |
| Assistant message | `.message.role == "assistant"` | `.message.content[]`, `.message.usage` |
| User message | `.message.role == "user"` | `.message.content` |
| Tool use | `.message.content[].type == "tool_use"` | `.name`, `.input` |
| Hook progress | `.type == "progress"` + `.data.type == "hook_progress"` | `.data.hookName` |

## Encoded Project Path

CC encodes project paths by replacing `/` with `-`:
- `/Users/foo/coding/bar` becomes `-Users-foo-coding-bar`
- The encoded path is the directory name under `~/.claude/projects/`


### Jq Queries

# Analytics jq Queries

Ready-to-run jq queries for each analytics subcommand. All queries target `~/.claude/analytics/*.jsonl`.

## agents — Top agents by frequency and duration

```bash
jq -s 'group_by(.agent) | map({
  agent: .[0].agent,
  count: length,
  avg_ms: (map(.duration_ms // 0) | add / length | floor),
  success_rate: (map(select(.success)) | length) / length * 100 | floor,
  models: (group_by(.model) | map({model: .[0].model, count: length}) | sort_by(-.count))
}) | sort_by(-.count)' ~/.claude/analytics/agent-usage.jsonl
```

## models — Model delegation breakdown

```bash
jq -s 'group_by(.model) | map({
  model: .[0].model,
  count: length,
  avg_ms: (map(.duration_ms // 0) | add / length | floor),
  agents: ([.[].agent] | unique)
}) | sort_by(-.count)' ~/.claude/analytics/agent-usage.jsonl
```

## skills — Top skills by invocation count

```bash
jq -s 'group_by(.skill) | map({skill: .[0].skill, count: length}) | sort_by(-.count)' ~/.claude/analytics/skill-usage.jsonl
```

## hooks — Slowest hooks and failure rates

```bash
jq -s 'group_by(.hook) | map({
  hook: .[0].hook,
  count: length,
  avg_ms: (map(.duration_ms) | add / length | floor),
  fail_rate: (map(select(.ok == false)) | length) / length * 100 | floor
}) | sort_by(-.avg_ms) | .[0:15]' ~/.claude/analytics/hook-timing.jsonl
```

## teams — Team spawn counts, idle time, task completions

```bash
# Team activity (spawns + idle)
jq -s 'group_by(.team) | map({
  team: .[0].team,
  spawns: [.[] | select(.event == "spawn")] | length,
  idles: [.[] | select(.event == "idle")] | length,
  agents: [.[].agent] | unique
}) | sort_by(-.spawns)' ~/.claude/analytics/team-activity.jsonl

# Task completions by team
jq -s '[.[] | select(.team != null)] | group_by(.team) | map({
  team: .[0].team,
  tasks: length,
  avg_ms: (map(.duration_ms // 0) | add / length | floor)
})' ~/.claude/analytics/task-usage.jsonl
```

## summary — Quick counts

```bash
# Total sessions (excluding zero-tool sessions)
jq -s '[.[] | select(.total_tools > 0)] | length' ~/.claude/analytics/session-summary.jsonl

# Line counts per file
wc -l ~/.claude/analytics/*.jsonl 2>/dev/null

# Unique projects
jq -r .pid ~/.claude/analytics/agent-usage.jsonl 2>/dev/null | sort -u | wc -l
```

## Presentation Format

Present all results as clean markdown tables with counts, percentages, and averages. If a file doesn't exist, note that no data has been collected yet for that category.

Example output:

```markdown
| Agent | Count | Avg Duration | Success Rate | Top Model |
|-------|-------|-------------|-------------|-----------|
| code-quality-reviewer | 45 | 8.2s | 98% | opus |
| test-generator | 32 | 12.1s | 94% | sonnet |
```


### Otel Fields

# CC 2.1.117 + 2.1.119 + 2.1.122 + 2.1.126 OTEL Enrichments

OpenTelemetry attributes shipped in CC 2.1.117 (3 fields), CC 2.1.119 (3 more), CC 2.1.122 (1 new event + numeric-attr fix), and CC 2.1.126 (1 new attribute on existing event) that enable cross-cutting analytics this skill did not previously surface. All are **optional** — data from older CC versions lacks them and every query here falls back cleanly.

> **CC 2.1.122 numeric-attribute fix (#1584):** numeric attributes on `api_request` / `api_error` log events are now emitted as numbers, not strings. Queries that compared numeric strings lexicographically (e.g. `select(.input_tokens > "1000")`) silently broke after upgrading. Prefer JSON-numeric comparisons (`select(.input_tokens > 1000)`) — jq compares numbers and numeric-strings differently. The queries below all use numeric comparisons and are correct on CC 2.1.122+.

## The fields

### From 2.1.117 (M117 adoption)

| Field | Event | Values | Purpose |
|---|---|---|---|
| `command_name` | `user_prompt` | `/ork:implement`, `/commit`, `/effort`, … (string, may be null) | Which slash command triggered this prompt. Null for free-text prompts. |
| `command_source` | `user_prompt` | `user` \| `model` | `user` = user typed the slash; `model` = model invoked it via `SlashCommand` tool. |
| `effort` | `cost.usage`, `token.usage`, `api_request`, `api_error` | `low` \| `medium` \| `high` \| `xhigh` | Effort tier set via `/effort` or `xhigh` (CC 2.1.111+). |

### From 2.1.119 (M122 adoption)

| Field | Event | Values | Purpose |
|---|---|---|---|
| `duration_ms` | `tool_result`, `tool_decision` (PostToolUse + PostToolUseFailure inputs) | non-negative integer (ms) | Server-measured per-tool latency. Accurate for streaming/async tools where local timing misses dispatch/queue overhead. |
| `tool_use_id` | `tool_result`, `tool_decision` | string (UUID-like) | Correlates a PreToolUse span with its PostToolUse span — enables pre/post pair queries in tracing backends. |
| `tool_input_size_bytes` | `tool_result` | non-negative integer | Byte size of the serialized tool input. Surfaces oversized inputs that bloat context (e.g., 100K-byte file_path lists from broken tool callers). |

### From 2.1.122 (M128 adoption)

| Field | Event | Values | Purpose |
|---|---|---|---|
| `target` | `claude_code.at_mention` | string (file path, directory, or URL) | What was resolved by an `@`-mention in the prompt. New event in 2.1.122 — captures every `@file.ts`, `@docs/`, `@https://...` reference. Lets us see which files/dirs users repeatedly pull into context. |

### From 2.1.126 (M128 adoption)

| Field | Event | Values | Purpose |
|---|---|---|---|
| `invocation_trigger` | `claude_code.skill_activated` | `user-slash` \| `claude-proactive` \| `nested-skill` | Disambiguates how a skill was activated: user typed `/ork:foo`, model auto-invoked it via Skill tool, or another skill chained into it. Critical for measuring "is the model finding our skills" vs "are users using them". The event now also fires for user-typed slash commands (it previously fired only for proactive activations). |

Exports land in `~/.claude/otel/` (when OTEL export is enabled in `settings.json`) and in the same JSONL streams this skill already reads if OTEL-to-JSONL bridging is on.

## Data location

```
~/.claude/otel/user-prompts.jsonl       # command_name, command_source         (2.1.117)
~/.claude/otel/usage.jsonl              # effort                                (2.1.117)
~/.claude/otel/api-requests.jsonl       # effort, numeric attrs as numbers     (2.1.117 + 2.1.122 fix)
~/.claude/otel/tool-results.jsonl       # duration_ms, tool_use_id, tool_input_size_bytes  (2.1.119)
~/.claude/otel/tool-decisions.jsonl     # duration_ms, tool_use_id              (2.1.119)
~/.claude/otel/at-mentions.jsonl        # target                                (2.1.122)
~/.claude/otel/skill-activated.jsonl    # invocation_trigger                    (2.1.126)
```

When OTEL export is disabled the files simply do not exist — queries below handle this with `2>/dev/null` and empty-result fallbacks.

## Dashboard panels

Three new panels compose a CC-2.1.117-aware extension of the existing `summary` subcommand.

### Panel 1 — Top invoked slash commands (user-typed vs model-delegated)

```bash
jq -s 'map(select(.command_name != null))
  | group_by(.command_name)
  | map({
      command: .[0].command_name,
      total: length,
      user: (map(select(.command_source == "user")) | length),
      model: (map(select(.command_source == "model")) | length)
    })
  | sort_by(-.total)' ~/.claude/otel/user-prompts.jsonl 2>/dev/null
```

Output shape: `[\{command: "/ork:implement", total: 47, user: 42, model: 5\}, …]`.

Falls back to `[]` if the file is absent — render as "no OTEL data available (upgrade to CC ≥ 2.1.117)".

### Panel 2 — Per-effort-level cost breakdown

```bash
jq -s 'map(select(.effort != null))
  | group_by(.effort)
  | map({
      effort: .[0].effort,
      runs: length,
      input_tokens: (map(.input_tokens // 0) | add),
      output_tokens: (map(.output_tokens // 0) | add),
      est_cost_usd: (map(.cost_usd // 0) | add | . * 100 | floor / 100)
    })
  | sort_by(.effort)' ~/.claude/otel/usage.jsonl 2>/dev/null
```

Expected order when rendered: `low → medium → high → xhigh`. If all rows share one effort tier, present as a single-row table rather than an empty-panel error.

### Panel 3 — Effort-vs-success rate correlation

```bash
jq -s 'map(select(.effort != null))
  | group_by(.effort)
  | map({
      effort: .[0].effort,
      runs: length,
      success_rate: ((map(select(.success == true)) | length) / length * 100 | floor),
      error_rate: ((map(select(.error != null)) | length) / length * 100 | floor),
      avg_duration_ms: (map(.duration_ms // 0) | add / length | floor)
    })
  | sort_by(.effort)' ~/.claude/otel/api-requests.jsonl 2>/dev/null
```

Interpretation: a monotonic success-rate increase from `low → xhigh` is the expected signal. A dip in the middle (e.g., medium worse than low) often indicates context-budget thrash at that tier.

### Panel 4 — Per-tool latency p50/p95 (CC 2.1.119)

```bash
jq -s 'map(select(.duration_ms != null))
  | group_by(.tool_name)
  | map({
      tool: .[0].tool_name,
      runs: length,
      p50: ((map(.duration_ms) | sort)[length / 2 | floor]),
      p95: ((map(.duration_ms) | sort)[(length * 0.95) | floor]),
      max: (map(.duration_ms) | max)
    })
  | sort_by(-.p95)' ~/.claude/otel/tool-results.jsonl 2>/dev/null
```

Use this to spot tools whose tail latency dominates session time. p95 ≫ p50 typically indicates either (a) variable input size or (b) backend-throttling on a particular MCP server.

### Panel 5 — Oversized tool inputs (CC 2.1.119)

```bash
jq -s 'map(select(.tool_input_size_bytes != null and .tool_input_size_bytes > 10000))
  | sort_by(-.tool_input_size_bytes)
  | .[0:10]
  | map({
      tool: .tool_name,
      bytes: .tool_input_size_bytes,
      kb: (.tool_input_size_bytes / 1024 | floor),
      tool_use_id: .tool_use_id
    })' ~/.claude/otel/tool-results.jsonl 2>/dev/null
```

Top 10 tool inputs over 10 KB. Repeated offenders are usually broken tool callers or unexpected large file_path lists — investigate the matching `tool_use_id` in your traces.

### Panel 6 — Pre/post latency correlation (CC 2.1.119)

```bash
jq -s 'map(select(.tool_use_id != null and .duration_ms != null))
  | group_by(.tool_use_id)
  | map(select(length == 2))    # only pairs (PreToolUse + PostToolUse)
  | map({
      tool_use_id: .[0].tool_use_id,
      tool: .[0].tool_name,
      pre_ms: ((map(select(.event == "tool_decision")) | .[0]?.duration_ms) // 0),
      post_ms: ((map(select(.event == "tool_result")) | .[0]?.duration_ms) // 0)
    })' ~/.claude/otel/tool-decisions.jsonl ~/.claude/otel/tool-results.jsonl 2>/dev/null
```

Reveals hooks that add significant pre-tool latency. If `pre_ms` rivals `post_ms`, the hook chain is the bottleneck — candidate for `type: "mcp_tool"` direct dispatch (see `src/skills/chain-patterns/references/mcp-tool-hooks.md`).

### Panel 7 — Skill activation by trigger type (CC 2.1.126, #1581)

```bash
jq -s 'map(select(.invocation_trigger != null))
  | group_by(.skill_name)
  | map({
      skill: .[0].skill_name,
      total: length,
      user_slash: (map(select(.invocation_trigger == "user-slash")) | length),
      claude_proactive: (map(select(.invocation_trigger == "claude-proactive")) | length),
      nested_skill: (map(select(.invocation_trigger == "nested-skill")) | length),
      proactive_ratio: ((map(select(.invocation_trigger == "claude-proactive")) | length) / length * 100 | floor)
    })
  | sort_by(-.total)' ~/.claude/otel/skill-activated.jsonl 2>/dev/null
```

Output shape: `[\{skill: "frontend-design", total: 23, user_slash: 4, claude_proactive: 17, nested_skill: 2, proactive_ratio: 73\}, …]`.

**Reading the data:**
- High `proactive_ratio` (>50%) → description is doing its job; model is finding the skill from intent.
- Low `proactive_ratio` (&lt;10%) and low `total` → description may be too narrow, or skill not user-invocable. Candidate for sharpening or for `user-invocable: true`.
- High `nested_skill` → skill is composed by other skills (e.g., `/ork:design-import` → `component-search`). Verify the chain is intentional.

Falls back to `[]` when the file is absent — render as "no OTEL data available (upgrade to CC ≥ 2.1.126)".

### Panel 8 — Most-mentioned `@` targets (CC 2.1.122, #1584)

```bash
jq -s 'map(select(.target != null))
  | group_by(.target)
  | map({target: .[0].target, count: length})
  | sort_by(-.count)
  | .[0:20]' ~/.claude/otel/at-mentions.jsonl 2>/dev/null
```

Top 20 `@`-referenced files, dirs, or URLs across the time range. Repeatedly-mentioned targets are candidates for inclusion in CLAUDE.md or for a custom slash-command shortcut. Falls back to `[]` when the file is absent — render as "no OTEL data available (upgrade to CC ≥ 2.1.122)".

## Graceful fallback

All three queries:

1. Use `2>/dev/null` on the file read — missing file → empty stream.
2. Filter `select(.command_name != null)` / `select(.effort != null)` — events from older CC versions lack the attributes and are skipped, not rendered as "unknown" bars.
3. Return `[]` on empty input so the dashboard renders a placeholder instead of crashing.

## When to use

Include these panels in `summary` when **any** of the three OTEL files are present with non-empty content for the time range. Otherwise fall back to the legacy `summary` output — do not render empty OTEL panels just because the fields exist in recent events.

## Related

- `src/skills/analytics/references/jq-queries.md` — base queries for non-OTEL JSONL sources.
- `src/skills/analytics/references/cost-estimation.md` — per-model pricing; combines with Panel 2 for dollar-denominated breakdowns.
- `src/hooks/src/lib/cc-version-matrix.ts` — `otel_command_attrs` entry gates these fields behind `MIN_CC_VERSION = 2.1.117`.


### Session Replay

# Session Replay

Parse and visualize CC session JSONL files to understand what happened in a session.

## Usage

- `/ork:analytics session latest` — most recent session
- `/ork:analytics session &lt;partial-id&gt;` — match by prefix (e.g., `08ed1436`)
- `/ork:analytics session &lt;full-uuid&gt;` — exact match

## Step 1: Locate the Session File

CC session logs live at `~/.claude/projects/\{encoded-project-path\}/`.

The encoded path replaces `/` with `-` in the project directory path.
Example: `/Users/foo/coding/bar` becomes `-Users-foo-coding-bar`

```bash
# Find project session dir
PROJECT_DIR=$(echo "$CLAUDE_PROJECT_DIR" | sed 's|/|-|g')
SESSION_DIR="$HOME/.claude/projects/$PROJECT_DIR"

# List recent sessions (newest first)
ls -t "$SESSION_DIR"/*.jsonl 2>/dev/null | head -5

# For "latest": use the first result
LATEST=$(ls -t "$SESSION_DIR"/*.jsonl 2>/dev/null | head -1)
```

## Step 2: Resolve the Session Reference

- `latest` — find the most recently modified `.jsonl` file in the project directory
- Partial ID (e.g., `08ed1436`) — find file starting with that prefix
- Full UUID — exact match

## Step 3: Parse JSONL and Extract Timeline

Each line is a JSON object. Key extraction patterns:

```bash
# Count messages by role
jq -r '.message.role // empty' "$SESSION_FILE" | sort | uniq -c | sort -rn

# Extract tool calls with timestamps
jq -r 'select(.message.role == "assistant") | .message.content[]? | select(.type == "tool_use") | .name' "$SESSION_FILE" | sort | uniq -c | sort -rn

# Sum token usage
jq -s '[.[].message.usage // empty | {
  i: .input_tokens, o: .output_tokens,
  cr: .cache_read_input_tokens, cw: .cache_creation_input_tokens
}] | {
  input: (map(.i) | add), output: (map(.o) | add),
  cache_read: (map(.cr) | add), cache_write: (map(.cw) | add)
}' "$SESSION_FILE"

# Get session metadata
jq -r 'select(.gitBranch) | .gitBranch' "$SESSION_FILE" | head -1
jq -r 'select(.version) | .version' "$SESSION_FILE" | head -1

# Get start/end timestamps
jq -r '.timestamp' "$SESSION_FILE" | head -1   # start
jq -r '.timestamp' "$SESSION_FILE" | tail -1   # end

# Count agent spawns by type
jq -r '.message.content[]? | select(.type == "tool_use" and .name == "Task") | .input.subagent_type' "$SESSION_FILE" | sort | uniq -c | sort -rn
```

## Step 4: Present as Timeline

```markdown
## Session: 08ed1436 — 2026-02-18 10:50 -> 11:35 (45min)
**Branch:** bugfix/windows-spawn | **CC Version:** 2.1.45
**Tokens:** 152K in, 38K out | **Cache hit rate:** 89%

### Timeline
| Time | Event | Details |
|------|-------|---------|
| 10:50:00 | SESSION START | branch: bugfix/windows-spawn |
| 10:50:01 | HOOK | SessionStart:startup |
| 10:50:05 | Read | src/hooks/bin/spawn-worker.mjs |
| 10:50:08 | Grep | "spawn" in src/ |
| 10:50:15 | Task (agent) | code-quality-reviewer |
| 10:51:00 | Edit | src/hooks/bin/spawn-worker.mjs |
| 10:52:30 | Bash | npm test -> 8.3s |
| 11:35:00 | SESSION END | 23 tool calls, 3 agents |

### Tool Usage
| Tool | Count |
|------|-------|
| Read | 12 |
| Edit | 5 |
| Bash | 4 |
| Task | 2 |

### Token Breakdown
| Metric | Value |
|--------|-------|
| Input tokens | 152,340 |
| Output tokens | 38,210 |
| Cache read | 1,245,000 |
| Cache write | 18,500 |
| Cache hit rate | 89% |
```


### Trends Analysis

# Trends Analysis

Show daily activity, model delegation trends, and cost patterns over time.

## Usage

- `/ork:analytics trends` — default 7 days
- `/ork:analytics trends 30` — last 30 days

## Step 1: Daily Activity (sessions, messages, tool calls)

```bash
jq '.dailyActivity[-7:]' ~/.claude/stats-cache.json
```

## Step 2: Daily Model Token Breakdown

```bash
jq '.dailyModelTokens[-7:] | .[] | {
  date: .date,
  models: (.tokensByModel | to_entries | map({model: .key, tokens: .value}) | sort_by(-.tokens))
}' ~/.claude/stats-cache.json
```

## Step 3: Peak Productivity Hours

```bash
jq '.hourCounts | to_entries | sort_by(-.value) | .[0:5] | map({
  hour: (.key + ":00"),
  sessions: .value
})' ~/.claude/stats-cache.json
```

## Step 4: All-Time Stats

```bash
jq '{
  totalSessions: .totalSessions,
  totalMessages: .totalMessages,
  longestSession: {
    id: .longestSession.sessionId,
    duration_min: (.longestSession.duration / 60000 | floor),
    messages: .longestSession.messageCount
  }
}' ~/.claude/stats-cache.json
```

## Presentation Format

```markdown
## Trends -- Last 7 Days

### Daily Activity
| Date | Sessions | Messages | Tools | Est. Cost |
|------|----------|----------|-------|-----------|
| Feb 12 | 6 | 1,200 | 450 | $2.10 |
| Feb 13 | 5 | 980 | 380 | $1.85 |
| ... | ... | ... | ... | ... |
| **Total** | **42** | **8,380** | **3,390** | **$18.50** |

### Model Delegation Trend
| Date | opus | sonnet | haiku |
|------|------|--------|-------|
| Feb 12 | 452K | 31K | -- |
| Feb 13 | 380K | 25K | 12K |
| ... | ... | ... | ... |

### Peak Productivity Hours
| Hour | Sessions |
|------|----------|
| 10:00 | 78 |
| 9:00 | 71 |
| 14:00 | 65 |

### All-Time Stats
- **Total sessions:** [N]
- **Total messages:** [N]
- **Longest session:** [id] -- [N] min, [N] messages
```

## Cost Per Day

Apply pricing from `references/cost-estimation.md` to daily token counts:
- Split daily tokens by model
- Apply per-model pricing (70/30 input/output estimate for daily totals)
- Show daily cost in the activity table

