skill-usage-audit
Use when measuring whether a skill-concierge gate-threshold change helped real skill usage or adoption — "did the new gate values help", "skill usage impact after the change", "assess skill helpfulnes
SKILL.mdskills/skill-usage-audit/SKILL.md
GitHubSkill Usage Audit
Overview
Whether agents use the right skill is NOT the enforcer's offer→take. The invocation-ledger measures gate compliance and is INVALID for usage analysis (operator-flagged). The real signal lives in the transcript store — and most of it is the SKILL-FIRST declaration trail, which no invocation counter records.
Violating the letter here is violating the spirit: "I'll just use the ledger, it has invocations" is the exact failure this skill exists to stop.
Pick the right source
| Source | Path | Measures | Use for |
|---|---|---|---|
| invocation-ledger | ~/.claude/skill-concierge/logs/skill-invocation-ledger.log |
gate compliance (offer→take; auto/manual/search) |
gate firing only — NOT usage |
| skill-usage-tracker | transcripts → ~/.claude/audits/skill-usage-stats/ |
usage frequency (Skill tool + /slash) |
how often each skill actually ran |
| SKILL-FIRST trail | assistant text in ~/.claude/projects/**/*.jsonl |
agent KNEW + chose a skill (USING/SEARCH/NO SKILL declarations; SKIPPING before 0.52.0 — both read) |
the operator's metric |
Inline SKILL-FIRST use (declare USING: <skill> → read its SKILL.md → execute) fires no Skill
tool, so the ledger AND the usage-tracker both miss it. The declaration trail is the proxy that
catches it; subagent/Task skill use is missed by all three.
Run it
python3 scripts/audit_skill_usage.py --since "<ship/commit time, e.g. 2026-06-29 01:06:35>"
Outputs the scoped post-change counts (Skill-tool, /slash, and the USING/SEARCH/skip-ruling
trail, skips split into NO SKILL: and the old SKIPPING), self/meta sessions flagged, plus a false-SKIPPING rate — per turn, a skip ruling (NO SKILL:, or the older SKIPPING)
declared with none of the doctrine's three lawful sources (rule 4): a same-turn search_skills call
before the ruling, the enforcer's SKILL-CHECK: line, or a whole-shelf ranking whose top row the
ruling names (below). A turn carrying the
enforcer's SKILL-CHECK: marker (AUTHORIZED_SKIP_MARKER, injected on the enforcer's five
authorized-skip legs — getaway, intent_skip, selfref, the harness-message lane (ADR-0054) and the
router's no-fit leg (ADR-0061) — see hooks/scripts/enforcer.py) is a lawful, hook-pre-authorized
skip: it is excluded from the false-skip count and tallied separately as authorized_skip, reported
alongside the false-skip figure so "false-SKIPPING" stays honestly defined. The third lawful source
(ADR-0082) is a whole-shelf ranking: a NO SKILL: whole-shelf — <top row>: … ruling counts as
shelf_skip only when the enforcer's own output that turn carried the "Whole-shelf ranking" head and
the ruling names that ranking's first row. Since 0.52.0 the marker counts only from the enforcer's own output (_enforcer_output: a
UserPromptSubmit hook_additional_context attachment whose text starts with SKILL-FIRST,
SKILL-CHECK: or CONSULT-ROUTE) — never from the agent's own text, a tool result, a file echo, a
memory or instructions attachment, another hook or the session-start standing order, so a copied line
cannot authorize a skip (ADR-0062). The report adds an enforcer-run turns only line — the
same verdict over turns where the enforcer's offer, consult route or SKILL-CHECK: line reached the agent
before it ruled (Stop-hook feedback, subagent prompts, and short or slash prompts get no offer, though the
doctrine still binds the last two). Since 0.52.1 a line that arrives after the ruling — a queued
notification — neither authorizes the skip nor marks the turn enforcer-run (ADR-0063). Since 0.52.2
the same holds for the search: a skip is search-backed only by a skill-search search_skills call made
before the ruling (rule 4).
The report also counts continuations (rule 3; ADR-0064, ADR-0065; epoch-watch W28): every
USING: <name> (continuing …) — (continued …), (continuation …) and several names included — per
turn: whether the turn loads the skill (the Skill tool or skill-search's get_skill, before or after the
line), whether the session used it before this turn (a load, a USING: line or the user's slash command,
before the --since window too), and how many work turns ago it was last used (more than 5 is
flagged as likelier new work). A work turn is a prompt that hands the agent work: typed text, a relayed
chat message, pasted content, a slash command with arguments, or a typed prompt stored as a list of text
and image blocks. Stop-hook feedback, notifications, scheduled tasks, cross-session messages,
local-command output, session builtins (/compact, /plugin, /model …) and compaction summaries do
not count (since 0.52.4, by prompt shape since 0.52.5). Since 0.52.6 the harness's own record
fields decide first: an origin.kind other than human, a system prompt, or a promptSource: sdk
prompt from a program entrypoint (sdk-…) is not work — including the few a person typed through an
SDK app, a recorded trade-off (ADR-0071) — and neither is a team runner's inbox relay or scaffolding
(## New Messages, ## Team Governance, ## Turn Context, Team: "…") or a bot scheduler's message
(Meanwhile, Heartbeat check and three siblings). A "turn" for the verdicts is every string-content user
record plus a list-form prompt that hands over work: a notification's or a message's content is a task
under the standing order (rule 4), so its turn is scored. A list-form harness record, and a program's
prompt in either form (a system or program-SDK prompt whose text would be work from a person), open a
turn whose skip rulings are never scored (since 0.52.8; string-form programs since ADR-0073); a
continuation in such a turn still counts. The report adds the verdicts on turns a work prompt opened, as
its own line: the headline is mostly harness-message turns. A record line the store
wrote twice is read once, and a skip ruling a resumed session copied into its file counts once (the
session's own file wins; ADR-0073). Files are read in sorted order; a resumed session usually rewrites the copied
records' session id, so that order decides which copy of a continuation counts (when a copy keeps the
original id, the session's own file wins). A note that negates the continuation word itself ("(not
continuing x)", "(not actually continuing)", "(instead of continuing x)") or names a new task is a fresh
ruling; a justification elsewhere in the note ("same task, no new search — continuing", "(no need for a
new task)", "(won't stop continuing)") still reads; the bare USING <name> (continuing) reads in
capitals. A later name after a comma or + reads only as one word ("then study"), a skill this session
already used, or a hyphenated installed skill ("+ ak-git for the commit"): "then run the tests" is prose
even where a skill is named run. The installed-skill catalogue skips test fixtures and marketplace
copies (ADR-0073). Name forms of one skill
match loosely (plugin:name and name, or one name ending in -<the other>), which can join two
different skills whose names nest (ssh-doctor, tk-servers-ssh-doctor); a slash command counts as
earlier use only from the user's own prompt, not from a tool result that quotes one. The listing shows
local time, sorted, with self/meta sessions marked. Counts are shown for all sessions and for organic ones (self/meta excluded);
--continuations lists each one (session id prefix, time, skill) for hand review. A queued user prompt
splits a turn, so a re-read after it is missed. The SEARCH declaration count is display-only and
includes bare title-case prose lines ("Search …"); the raw-line pre-filter is case-sensitive, so a
lower- or title-case ruling that carries none of its tokens is not read. The verdicts come from tool
calls. Since 0.49.0
(ADR-0059 §5), a USING:
retracted by a same-reply re-rule (the doctrine's USING:/SEARCH: line ending
(re-rule: <old>), fired after skill_exclusions.py echoes a loaded skill's own "not for" lines
back) is moved out of the uptake counts into a separate re-rules tally, so a switched-away-from
skill no longer inflates uptake. Run --help for flags; --selftest pins the false-SKIPPING
verdict logic, including the authorized-skip case.
Scope to reduce noise (post-change)
- Window to events at/after the ship/commit time (not calendar "since I deployed").
- Drop fallback-band offers (embed/Qdrant down — the gate never ran; not its decision).
- Drop self/meta sessions — work on the audited project itself (dogfooding/verification) is the agent testing the gate, not organic usage.
- Canonicalize names before any offered-vs-invoked join (
ck:journal≡journal); raw set-intersection silently undercounts namespaced-vs-bare. - Gate the verdict on VOLUME, not calendar time — need ~50–100+ organic offered turns before a few-point change clears noise.
Right metric, not the trap
- Report absolute coverage (right skill surfaced-and-used ÷ applicable turns), NOT conditional offer→take: raising a floor fires fewer offers and mechanically inflates conditional conversion even if real routing drops.
- Suppression cost is unobservable post-change (a suppressed offer is never shown → never taken). Estimate it ONLY via a backtest over history: which taken offers would the new floor have blocked? (On the live ledger: taken offers score LOWER than dodged — cosine is anti-correlated with adoption.)
Tuning thresholds (optimal values)
- Sweep candidate floors through the real enforcer fns (
_embed/_retrieve/_is_imperative/_intent_conversational) over a labeled corpus — never reimplement the gate. - Held-out only:
prompt_intentis built from the same corpus, so_intent_conversationalclassifies in-sample (~73% noise-catch vs ~53% held-out). Build a temp collection on a train split (SKILL_PROMPT_INTENT_COLLECTION=...heldout), evaluate on the test split, delete it. - Frame as volume vs adoption: a higher floor suppresses more (volume) but cuts the better-converting low-cosine offers (adoption). Re-confirm on the live ledger before recommending.
- Worked example:
skill-concierge/plans/reports/impact-analysis-260629-1020-*.md.
v0.10.0 caveat (multi-vector, ADR-0012). The "cosine anti-correlated with adoption / taken offers score LOWER than dodged" findings above were measured on the SINGLE-vector index. Multi-vector MAX-pool roughly doubled positive↔negative separation, so the cosine↔adoption relationship must be re-measured on post-v0.10.0 traffic before reuse — do not carry the old anti-correlation forward as fact. The methodology (held-out sweeps, absolute coverage, drop self/meta, gate on volume) is unchanged.
Common mistakes (from baseline failures)
| Rationalization | Reality |
|---|---|
| "The ledger records invocations, so it's the source." | Ledger = gate compliance; operator flagged it INVALID for usage. Use transcripts. |
| "offer→take = how often the right skill is used." | Conditional on the gate firing; misses inline SKILL-FIRST use entirely. |
| "Post-change window is enough to judge." | Usually thin + dogfood-contaminated. Gate on organic volume; drop self/meta. |
| "Sweep the corpus through the intent gate as-is." | The intent corpus self-scores in-sample. Held-out split only. |
| "Compare pre vs post conversion." | Denominator shifts (the floor changes who is offered). Use within-population replay / absolute coverage. |
"Offered ck:journal ≠ invoked journal → not used." |
Canonicalize names before joining. |
Red flags — STOP
- Reaching for
skill-invocation-ledger.logto answer a usage question. - Leading with offer→take / "accept rate".
- Reporting a post-change number without dropping self/meta sessions or checking volume.
- Sweeping the live
prompt_intentcollection in-sample.