{
  "schema_version": 1,
  "type": "app",
  "slug": "skill-pilot",
  "title": "skill-pilot",
  "brief": "Lints SKILL.md files for description quality (1,536-char cap, front-loaded triggers, valid frontmatter), and ships a UserPromptSubmit hook that injects a system reminder naming the skills that match e",
  "version": "0.1.2",
  "tags": [],
  "license": "MIT",
  "discovery_triggers": [
    "skill-pilot",
    "lint skills",
    "audit skills",
    "audit my skills",
    "check skill descriptions",
    "skill description quality",
    "skill front-load",
    "skill triggers",
    "why isn't claude using my skill",
    "skill not triggering",
    "claude doesn't use my skills",
    "claude misses skills",
    "skill triggering",
    "skill router",
    "skill matcher",
    "prompt routing",
    "route to skill",
    "enable skill hook",
    "disable skill hook",
    "skill-pilot on",
    "skill-pilot off",
    "skill-pilot status",
    "skill-pilot lint",
    "skill-pilot match",
    "1536 cap",
    "skill cap",
    "system reminder skills",
    "claude code hook skills",
    "are my skills formatted right"
  ],
  "discovery_pitch": "Stops Claude Code from missing skills. Lints every SKILL.md for description quality (front-loaded triggers, the 1,536-char cap, valid frontmatter), and a UserPromptSubmit hook injects a system reminder listing matching skills on every prompt so Claude is nudged by name instead of fuzzy-matching against 100+ descriptions.",
  "install": {
    "binary_name": "skill-pilot",
    "install_dir": "",
    "install_hint": "",
    "version_cmd": ""
  },
  "readme": "# skill-pilot\n\nStops Claude Code from missing skills. Two halves working together: a **lint** that audits every `SKILL.md` for description quality, and a **router hook** that nudges Claude on every prompt by name-matching the prompt against your skill index.\n\n## The problem this solves\n\nClaude Code surfaces every installed skill to the model via its `description` frontmatter — capped at **1,536 characters per skill**, with a global ~8,000-char budget across all skills. With 100+ skills, two things go wrong:\n\n1. **Buried trigger phrases.** A description that puts keywords past the first 200 characters effectively hides them. Claude never sees \"use this for Mouser searches\" if the prose only mentions Mouser at the end.\n2. **Soft signal.** Even when Claude *does* see a relevant description, picking it from 100+ candidates is fuzzy. A skill that should fire ~always (like `tool-publisher` for any \"publish\" prompt) often gets passed over.\n\n`skill-pilot` fixes both.\n\n## How the router works\n\n`skill-pilot on` registers a `UserPromptSubmit` hook in `~/.claude/settings.json`. Every time you hit Enter, the hook runs `skill-pilot match` with your prompt as stdin. The matcher:\n\n1. Loads (or builds + caches for 5 min) an in-memory keyword index from every `SKILL.md` in `~/.claude/skills/` and `~/project/gallia/skills/`. Triggers come from `Trigger words:` blocks, comma-separated noun phrases in the description, and proper-noun-ish words like `Mouser` / `KiCad` / `JLCPCB`.\n2. Word-boundary matches the prompt against the index. Single-word triggers require word boundaries (so `mouser` matches `mouser`, never `mouserate`); multi-word phrases use substring match.\n3. Emits one short `<system-reminder>` listing the top-5 matching skill names, e.g.:\n\n   > `<system-reminder>skill-pilot: matching skills for this prompt — adom-mouser, adom-parts-search, electrical-engineering. Invoke if relevant; ignore if not.</system-reminder>`\n\n4. Logs to `~/.adom/skill-pilot/matches.log` (timestamp, skills, prompt prefix) so you can audit precision/recall.\n\nClaude sees the reminder *by name* — much louder signal than ambient description matching against 100+ skills.\n\n## How the lint works\n\n`skill-pilot lint` scans every `SKILL.md` and scores 0–100 with deductions for:\n\n| Issue | Penalty |\n|---|---|\n| Missing `name` / `description` | −30 / −50 |\n| `description + when_to_use` over 1,536 chars (gets truncated) | −20 |\n| No detectable trigger phrases | −30 |\n| Triggers exist but none in first 200 chars | −25 |\n| Description shorter than 60 chars | −15 |\n\nOutput is human-readable by default, `--json` for machine consumption. Use it before `skill-pilot on` to find skills the matcher won't be able to see, then front-load their trigger words.\n\n## Install (Tier A — auto-installed)\n\nskill-pilot is **Tier A**: every Adom container gets it automatically when `gallia/install.mjs` runs (which happens on every gallia auto-update, ~30 min cadence). The router hook is **enabled by default** during install. New containers will see a one-time notice in the install output explaining what was just turned on.\n\nIf you need to (re)install manually:\n\n```bash\ncurl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/skill-pilot/skill-pilot \\\n  -o /tmp/skill-pilot && chmod +x /tmp/skill-pilot \\\n  && sudo install -m 0755 /tmp/skill-pilot /usr/local/bin/skill-pilot \\\n  && skill-pilot install \\\n  && skill-pilot on\n```\n\nGallia's 30-min wiki-refresh hook keeps the installed binary in sync with the wiki's `pub_version`.\n\n### Don't want it?\n\n```bash\nskill-pilot off                   # remove hook from settings.json\nexport ADOM_SKILL_PILOT=0         # or pause per-shell, no settings change\n```\n\nBoth off-switches survive future gallia updates — `install.mjs` runs `skill-pilot on` only on the *initial* install (`on` is idempotent and won't re-enable a hook you've explicitly removed).\n\n## CLI\n\n```bash\n# Lint (do this BEFORE turning the router on)\nskill-pilot lint                    # human-readable report\nskill-pilot lint --json             # machine output\nskill-pilot lint --dir <path>       # custom skill dirs\n\n# Router hook\nskill-pilot on                      # register UserPromptSubmit hook\nskill-pilot off                     # remove hook\nskill-pilot status                  # wired? env-var state? last 10 matches\n\n# Dry-run / debug\nskill-pilot match --prompt \"find a 10k resistor on mouser\"\nskill-pilot match --prompt \"...\" --no-log\n\n# Historical analysis\nskill-pilot analyze                 # top-firing skills, false-positive\n                                    #   candidates, co-occurrence, zero-matches\nskill-pilot analyze --json          # machine output\nskill-pilot analyze --log <path>    # custom log path\n```\n\n## Auditing the router with `skill-pilot analyze`\n\nEvery prompt that flows through the hook is appended to\n`~/.adom/skill-pilot/matches.log` — including prompts that matched\nzero skills (so you can see what the router is *missing*, not just\nwhat it found). Format is one line per prompt:\n\n```\n<unix_timestamp>\\t<comma_separated_skills>\\t<first_120_chars_of_prompt>\n```\n\n`skill-pilot analyze` reads this log and surfaces:\n\n| Section | What it tells you |\n|---|---|\n| **Activity** | Total prompts logged, time buckets (1h / 24h / 7d), zero-match %, avg matches per matched prompt |\n| **Top firing skills** | Top-25 skills by count + percent of all prompts. `⚠` flags skills firing on ≥30% (false-positive candidates). `·` flags skills firing on <2% (rare or niche) |\n| **False-positive candidates** | Skills that fire on ≥30% of prompts — almost certainly matching too aggressively. Narrow their triggers |\n| **Top co-occurring pairs** | Skills that always fire together. Often a sign that one matcher is dragging others along (shared trigger or noise like `<task-notification>` blobs) |\n| **Recent zero-match prompts** | Recent prompts the router didn't route at all — gaps where you might want to widen a skill's triggers, or accept that no skill should fire there |\n\n**How to read the output:**\n\n- **Avg matches per matched prompt > 3.5** — too liberal. Either narrow triggers, or lower the per-prompt cap (currently 5).\n- **Any skill firing on >30% of prompts** — almost always a false positive on a common word. Edit its description's trigger phrases to be more specific.\n- **A pair of skills with co-occurrence equal to each individual count** — they're firing as a block (either correctly because they're always relevant together, or incorrectly because they share a common trigger). Investigate the prompts that fired both.\n- **Zero-match prompts** that look obviously routable — an opportunity to add a missing trigger phrase to a skill, or write a new skill.\n\n**Privacy:** the log only stores the first 120 characters of each prompt. Tabs and newlines are flattened. The log lives at `~/.adom/skill-pilot/matches.log` — local to your container, never uploaded.\n\n**Recommended cadence:** run `skill-pilot analyze` after a week of real use. The first few hours of a fresh log are dominated by whatever conversational topic happens to be active and won't reflect steady-state behavior.\n\n## Recommended rollout\n\n1. **Lint first.** `skill-pilot lint` on a fresh box almost always surfaces 5–15 skills with buried triggers. Front-load them. (Score-100 skills tend to have a `Trigger words: a, b, c, ...` line in the first 200 chars of the description.)\n2. **Then enable.** Run `skill-pilot on`. **Active from the very next prompt — no Claude Code restart needed.** Every session on the same container picks it up on its next message.\n3. **Verify in the log, not the UI.** The Claude Code VS Code extension renders hook output inconsistently — sometimes as a collapsible `▶ UserPromptSubmit hook success` disclosure above your message, sometimes not visible at all. The hook itself fires reliably regardless. Trust these instead:\n   - `skill-pilot status` — last 10 matches inline\n   - `tail -f ~/.adom/skill-pilot/matches.log` — streams every match in real time\n4. **Audit weekly.** Watch the log for skills firing on >30% of prompts — those are false-positive candidates whose triggers need narrowing.\n\n## Three off-switches\n\nThe router is opt-in *and* easy to kill. In order of scope:\n\n| Layer | Command | Effect |\n|---|---|---|\n| Per-shell | `export ADOM_SKILL_PILOT=0` | matcher exits silently for that shell |\n| Per-machine | `skill-pilot off` | removes hook entry from `~/.claude/settings.json` (backup at `.skill-pilot.bak`) |\n| Manual | edit `~/.claude/settings.json` | restore from `.skill-pilot.bak` if anything went sideways |\n\nPlus the hook **fails closed**: if the binary errors or panics, it emits nothing and exits 0. Claude Code is never blocked by skill-pilot.\n\n## Performance\n\n- Hook overhead: ~5–15 ms per prompt (read cached index, word-boundary scan, write log line).\n- Injection: capped at 5 skills, ~200 chars total — invisible against the prompt itself.\n- Index cache: `~/.adom/skill-pilot/index.json`, rebuilt every 5 minutes.\n\n## Source\n\nSource lives in the gallia monorepo at `gallia/skill-pilot/` ([adom-inc/gallia](https://github.com/adom-inc/gallia), private). Build with `cargo build --release` from that directory. The compiled binary is published to this wiki page as a `docker_binary` asset; users install via the `install_hint` above (Tier B — on-demand fetch from the wiki, no GitHub credentials needed).\n\n## Related\n\n- `skill-creator` — Anthropic's official skill-authoring skill; use it to write new skills, then run `skill-pilot lint` to verify discoverability before shipping.\n- `tool-publisher` — for publishing CLIs and skills to the wiki (this very page).\n- `adom-wiki` — wiki search / publish CLI.\n- `skill-pilot-build` — sibling skill for cutting new releases.\n",
  "author": {
    "name": "Kyle Bergstedt",
    "email": "kyle@adom.inc"
  },
  "visibility": {
    "public": true
  },
  "hero": null,
  "sample_prompts": [],
  "metadata": {},
  "created_at": "2026-05-28T05:29:19.325Z",
  "updated_at": "2026-05-28T05:29:19.325Z",
  "skills": []
}