Skip to content

feat: skill usage analytics (invocation tracking, leaderboard, TUI+Web parity) - #56

Merged
FlorianBruniaux merged 14 commits into
mainfrom
feat/skill-usage-analytics
Aug 6, 2026
Merged

feat: skill usage analytics (invocation tracking, leaderboard, TUI+Web parity)#56
FlorianBruniaux merged 14 commits into
mainfrom
feat/skill-usage-analytics

Conversation

@FlorianBruniaux

Copy link
Copy Markdown
Owner

Summary

Real Skill invocation tracking for ccboard, built off the design doc consolidated from a multi-agent brainstorming session (3 architects, adversarial critique, second pass on blocking points).

  • Phase 0 — incremental invocation-stats refresh: the file watcher now updates a single changed session's stats without a full rescan (invocation_by_path map + refresh_invocations_for()), instead of only recomputing at startup. Includes a fix for a lost-update race between the incremental refresh and the background full scan (epoch-tagged entries, mutation-tested).
  • Phase 1 — extraction, persistence, CLI: real Skill invocations parsed from session transcripts (never textual mentions), proportional token/cost attribution matching session_index.rs's existing formula, two new additive SQLite tables (CACHE_VERSION unchanged, purely additive schema), and ccboard skills [--json] [--since] [--project] [--include-subagents] [<skill>].
  • Phase 2 — TUI tab + Web page, at parity: a new Skills leaderboard surface on both frontends, a [retired] badge for skills with invocation history but no on-disk definition (correctly resolving plugin-installed skills, not just locally-installed ones), and a fix for a live-refresh gap that predated this feature (the TUI's needs_refresh flag was set but never consumed).

Verification

  • session_index.rs: zero-line diff (measured project success criterion).
  • CACHE_VERSION stays at 9, schema additions are purely additive.
  • Real-data validation gate (non-negotiable per the design doc): ccboard skills --json --include-subagents cross-checked against cc-skill-usage --no-cache --include-subagents for flow-lean, exact match on invocation count and project count.
  • Cold-start regression: measured feat vs main on release builds with cache cleared, statistically identical (~3.32-3.34s both), well within the 10% budget.
  • Every task went through an isolated implementer + task-scoped review + fix loop where findings surfaced (two rounds caught and fixed a real race condition, a badge that was wrong on 81%/24% of real leaderboard rows on TUI/Web respectively, and a provably inert regex-classification fix).
  • Final whole-branch review passed after one fix wave (the Web-side retired-badge fix was ported from the TUI's, plus moved the plugin catalogue scan off the async executor thread).

cargo fmt --all && cargo clippy --all-targets && cargo test --all is clean except for one pre-existing, sandbox-specific test failure unrelated to this diff (live_monitor::tests::test_detect_live_sessions_no_panic shells out to ps, a setuid binary this review environment's sandbox categorically refuses; confirmed via a dedicated sandbox-diagnostic pass, not present in real environments).

Test plan

  • cargo fmt --all && cargo clippy --all-targets && cargo test --all
  • Manual TUI verification: isolated --claude-home test directory, live skill invocation appended to a watched session file, counter confirmed updating without restart
  • Real-data cross-check against cc-skill-usage for flow-lean
  • Cold-start timing comparison against main
  • Manual click-through of the new Web Skills page (not yet done by a human)

🤖 Generated with Claude Code

FlorianBruniaux and others added 14 commits July 26, 2026 13:20
Consolidated design from multi-agent brainstorming session (3 architects,
adversarial critique, second pass to resolve blocking points). Covers
schema, cost attribution formula, phased file list, and reference
signatures for the skill invocation analytics feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfnXvrvdLuhGXmyEPt4DsF
Add invocation_by_path map on DataStore so a single session update
(update_session, the file-watcher entry point) can rescan only the
touched file and re-fold the invocation_stats aggregate, instead of
requiring a full compute_invocations() rescan of every session.

Write locks on the map and the aggregate are scoped to the swap
itself, never held across an await, per ADR-001.
compute_invocations() (a slow, sequential full rescan) blindly replaced
invocation_by_path with its own snapshot, so a concurrent
refresh_invocations_for() write from the file watcher landing while the
scan was mid-flight got silently discarded once the scan finished. Both
run concurrently at startup (main.rs spawns compute_invocations() as a
background task, then starts the watcher immediately after).

Tag each invocation_by_path entry with a monotonic epoch. The full scan
snapshots the epoch before it starts and merges its results instead of
replacing the map, skipping any path whose entry is newer than that
snapshot. Also drops the redundant sessions_analyzed reset in
fold_invocations() flagged as a minor cosmetic note.
Extracts real Skill tool_use invocations from session transcripts (never
textual mentions), persists them in two new additive SQLite tables
(skill_scan_inventory, skill_events, CACHE_VERSION unchanged at 9), and
exposes an exact leaderboard via `ccboard skills [--json] [--since]
[--project] [--include-subagents] [<skill>]`.

Token attribution mirrors session_index.rs's proportional base/remainder
split exactly (zero changes to that file), verified by
test_parsers_agree_on_token_totals. Subagent transcripts under a
subagents/ directory are deduplicated by session path, never by the
JSON sessionId they share with their parent.

Cross-checked against the real ~/.claude/projects dataset with
cc-skill-usage: flow-lean invocation count matches exactly and stably
across repeated runs (9 invocations, 6 projects), both with and without
--include-subagents.
Adds a dedicated Skills tab (crates/ccboard-tui/src/tabs/skills.rs) showing
the invocation leaderboard from DataStore::skill_leaderboard(): invocation
count, project count, total tokens, estimated cost, first/last seen, with a
[retired] badge for skills that have invocation history but are no longer
present in the on-disk skill catalogue.

Also fixes the pre-existing gap where App::needs_refresh was set by
poll_events() on every DataEvent but never consumed anywhere: the tick loop
in lib.rs now re-fetches invocation_stats() and refreshes both the Agents
invocation badges and the new Skills leaderboard whenever a refresh is
flagged, instead of only once at startup.

Two related fixes bundled in, both scoped by the design doc: classify_plugin()
in plugin_usage.rs now tries an exact match on the full namespaced name before
falling back to substring matching, preventing false positives across
namespaces (e.g. local "brainstorming" vs "superpowers:brainstorming"). The
Agents tab's Skills sub-tab now shows an explicit [unused] label gated on
invocation_count == 0, matching the same source of truth as the new
leaderboard tab.
Expose Phase 1's DataStore::skill_leaderboard() through a new Axum route
(GET /api/skills/leaderboard) and a matching Leptos page at /skills, table
parity only per the design doc scope. Each entry gets a retired flag computed
fresh per-request against the on-disk ~/.claude/skills scan, matching the
same source the TUI's SkillsTab uses for its own [retired] badge (confirmed
via direct coordination with the TUI implementer) so both surfaces agree on
which skills count as defined, including the shared blind spot around
namespaced plugin skills.
…ssify_plugin no-op

Fix round 1 response to code review: two Important findings, both confirmed.

The [retired] badge on the new Skills leaderboard tab false-fired on any
plugin-namespaced invocation (superpowers:*, vercel:*, ...) because the
on-disk catalogue only ever held bare local .claude/skills/* names, never
resolving installed plugin skills. Added scan_known_skill_names_blocking()
in tabs/agents.rs, which walks both the plugins/marketplaces/**/skills/ and
plugins/cache/**/skills/ trees collecting lowercased leaf skill names, and
changed SkillsTab::is_retired() to match on the invocation's leaf name
against that combined catalogue instead of the raw namespaced string. Also
fixes the case-sensitivity bug (TDD vs tdd) as the same lowercase pass, and
the staleness issue: the catalogue now rescans in the background (via a
tokio mpsc channel, spawn_blocking, non-blocking poll each tick) whenever
needs_refresh fires, instead of being scanned once at startup.

classify_plugin()'s round-1 fix was a mathematically provable no-op (exact
match implies substring match, so the added OR clause changed nothing).
Restructured to check exact matches across both skills and commands before
falling back to substring matching in either category, which actually
changes behavior: an exact command match can no longer lose to an unrelated
skill's substring collision. Replaced the non-discriminating test with one
that fails against the pre-fix function and passes against the current one.
Three subagent task reports from .superpowers/sdd/ landed in earlier
commits via a broad git add. They're internal planning artifacts, not
shipped code; .gitignore now excludes the whole scratch workspace.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
on_disk_skill_names() only scanned ~/.claude/skills/, so any
plugin-installed skill (superpowers:brainstorming, vercel:vercel-cli, ...)
was falsely flagged [retired] on the web leaderboard even when actively
used. Measured 23/94 rows wrong, including the highest-cost skill on the
board (25 invocations, $8.02).

Mirrors the TUI fix from c49b84b: walk plugins/marketplaces and
plugins/cache recursively for skills/<name>/SKILL.md, build a lowercased
leaf-name catalogue, and match invocations by their leaf (text after the
last ':') instead of an exact full-name comparison. Corrects the doc
comment above on_disk_skill_names() that claimed the TUI and web sides
shared this blind spot, no longer true since c49b84b.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The plugin marketplace/cache walk touches hundreds of files on a
populated ~/.claude; running it inline in the request handler blocks
the tokio executor per this project's async conventions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pre-existing lint (clippy::useless_borrows_in_formatting), unrelated
to this branch's feature work but flagged by CI's newer stable
toolchain and blocking merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same pre-existing lint as the previous commit, flagged by CI's clippy
after fixing config.rs (clippy stops at the first error per crate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same pre-existing lint as the previous two commits, this time in the
ccboard-types crate's mirrored copy of the same masked_api_key logic.
Swept the whole workspace for remaining instances of this pattern,
none found.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FlorianBruniaux
FlorianBruniaux merged commit f5694c4 into main Aug 6, 2026
1 of 5 checks passed
FlorianBruniaux added a commit that referenced this pull request Aug 6, 2026
Skill usage analytics (PR #56): incremental invocation tracking, the
ccboard skills CLI, and TUI/Web leaderboard parity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant