SLOPSHOPPER

skill-router

Adds one line to a prompt that clearly matches a skill (review, plan-first, writing, ahpcc, assess-plugin, agents-md-scaffold), so the skill is used when the…

newcommandprompt
★ 1v0.1.0MITupdated 2026-10-21samuelbfernandes/moddeck/mods/skill-router
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · skill-router
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /skill-router ⎿ skill-router: skill-router · on · hinted this session: none ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

skill-router

Install: /plugin install skill-router --marketplace samuelbfernandes/moddeck

Makes sure a skill is used when the task calls for it. A prompt that clearly asks for one of the toolkit's skills gets one added line naming it; the model then loads the skill with the Skill tool.

What it does

Local keyword rules, first match wins, no model call:

SkillA prompt that mentions
ahpccahpcc, pinnacle, slurm, sbatch, squeue, "on the cluster"
assess-pluginassess / vet / "safe to install" with plugin, mod, mcp or skill
agents-md-scaffoldAGENTS.md, or starting a new repo with rules
reviewreview, second opinion, "check this", "before I commit/push"
plan-firsta long request to build an app, portal, pipeline or package; "plan/scope/align first"
writingemail, abstract, cover letter, rewrite, humanize, "sounds like AI", "draft a..."

Each skill is hinted once per session. Slash commands, > prompts, replies under five words, and prompts that already say "the <skill> skill" get nothing. About 30 tokens on a matching prompt.

Use

/skill-router status: skills hinted this session /skill-router on | off

Limits

Keywords are a guess at intent: a hint is advice ("unless the request clearly is not that"), not a command. The skills' own descriptions still let the model choose them without a hint.

State for the moddeck side bar

skill-router → isOff, hinted (see types/index.d.ts).

Source 3 files
hooks/register.tsx 58 lines
1// skill-router: a prompt that clearly matches one of the toolkit's skills gets one added line telling the model which
2// skill to load. Local keyword rules (no model call); each skill is hinted once per session; slash commands, short
3// replies and prompts that already name the skill get nothing. /skill-router on|off|status.
4// FAIL OPEN: any error sends the prompt unchanged.
5import { atom, read, update } from 'claude-code'
6import type { EngineInterface, Register } from 'claude-code'
7
8import { hintFor, pick } from './lib'
9
10type Dollar = EngineInterface
11
12const COMMAND = 'skill-router'
13
14const isOff = atom({ plugin: 'skill-router', key: 'isOff' } as const, false)
15const hinted = atom({ plugin: 'skill-router', key: 'hinted' } as const, [])
16
17async function run($: Dollar, args: string): Promise<string> {
18  const word = args.trim().toLowerCase()
19  if (word === 'on' || word === 'off') {
20    await $.store.set('off', word === 'off')
21    await update($, isOff, () => word === 'off')
22    return `skill-router · ${word}`
23  }
24  if (word === '' || word === 'status') {
25    const list = await read($, hinted)
26    return `skill-router · ${(await read($, isOff)) ? 'off' : 'on'} · hinted this session: ${list.length === 0 ? 'none' : list.join(', ')}`
27  }
28  return 'skill-router · use /skill-router [on|off|status]; nothing changed'
29}
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    try {
34      const off = (await $.store.get('off')) === true
35      await update($, isOff, () => off)
36      await update($, hinted, () => [])
37    } catch {
38      // reads as on
39    }
40    try {
41      await $.command.register({ name: COMMAND, description: 'Hints the matching skill on a prompt that clearly asks for it: /skill-router [on|off|status]', argumentHint: '[on|off|status]', immediate: true })
42    } catch {
43      // fail-open: the mod still works without its command
44    }
45    return next(e)
46  })
47
48  on('command.run', { command: COMMAND }, async ($, e) => ({ text: await run($, e.args) })).catch(() => ({ text: 'skill-router · that did not work, nothing changed' }))
49
50  on('prompt.submit', async ($, e, next) => {
51    if ((await read($, isOff)) || e.origin?.kind !== 'user') return next(e)
52    const skill = pick(e.text, await read($, hinted))
53    if (skill === undefined) return next(e)
54    await update($, hinted, list => [...list, skill])
55    return next({ ...e, context: [...(e.context ?? []), hintFor(skill)] })
56  }).catch(($, e, next) => next(e))
57}
58
hooks/lib.ts 30 lines
1// Pure helpers for skill-router: which skill a prompt clearly asks for. Local keyword rules, first match wins.
2
3export type Rule = { skill: string; test: (text: string, words: number) => boolean }
4
5export const RULES: Rule[] = [
6  { skill: 'ahpcc', test: t => /\b(ahpcc|pinnacle|slurm|sbatch|squeue|on the (hpc|cluster)|hpc cluster)\b/i.test(t) },
7  { skill: 'assess-plugin', test: t => /\b(assess|vet|audit|is it safe|worth installing|safe to install|should i install)\b[^.?!]*\b(plugin|mod|mcp|skill)s?\b/i.test(t) },
8  { skill: 'agents-md-scaffold', test: t => /\bagents\.md\b|\b(bootstrap|scaffold|set ?up|start|create)\b[^.?!]*\b(new )?(repo|repository|project)\b/i.test(t) && /\b(rules|agents|guard|new)\b/i.test(t) },
9  { skill: 'review', test: t => /\b(review|second opinion|theory review|check (this|my|the)|look (this|it) over|before (i |we )?(commit|push))\b/i.test(t) },
10  { skill: 'plan-first', test: (t, n) => (n >= 12 && /\b(build|implement|create|design|develop|write)\b[^.?!]{0,60}\b(app|portal|pipeline|package|dashboard|tool|feature|workflow|simulation|analysis|framework)\b/i.test(t)) || /\b(plan|scope|align|spec)\b[^.?!]*\b(first|before)\b/i.test(t) },
11  { skill: 'writing', test: t => /\b(email|e-mail|abstract|cover letter|tagline|rewrite|reword|humani[sz]e|de-?slop|sounds? like (ai|chatgpt)|draft (a|an|the|my))\b/i.test(t) },
12]
13
14const wordCount = (s: string): number => s.trim().split(/\s+/).filter(Boolean).length
15
16/** The skill to hint at: none for a slash command, a short reply, or a prompt that already names the skill. */
17export function pick(text: string, already: readonly string[]): string | undefined {
18  const t = text.trim()
19  const n = wordCount(t)
20  if (t.startsWith('/') || t.startsWith('>') || n < 5) return undefined
21  for (const r of RULES) {
22    if (already.includes(r.skill) || new RegExp(`\\b${r.skill}\\s+skill\\b|\\bskill\\s+${r.skill}\\b`, 'i').test(t)) continue
23    if (r.test(t, n)) return r.skill
24  }
25  return undefined
26}
27
28export const hintFor = (skill: string): string =>
29  `skill-router: this looks like a job for the ${skill} skill. Load it with the Skill tool and follow it, unless the request clearly is not that.`
30
types/index.d.ts 11 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'skill-router': {
4      /** True while switched off (it ships on). */
5      isOff: boolean
6      /** Skills hinted this session, in order. */
7      hinted: string[]
8    }
9  }
10}
11