Skip to content

Instantly share code, notes, and snippets.

@jcfischer
Created January 20, 2026 19:57
Show Gist options
  • Select an option

  • Save jcfischer/fcbc302dd701d91116ecca463c6f3d39 to your computer and use it in GitHub Desktop.

Select an option

Save jcfischer/fcbc302dd701d91116ecca463c6f3d39 to your computer and use it in GitHub Desktop.
SkillEnforcer Hook
name description triggers
SpecFlow
Multi-agent orchestration for spec-driven development (SDD). Inverts traditional development - specifications become executable artifacts that directly generate implementations. USE WHEN user says "new feature", "spec out", "create spec", "specify", "specflow", or wants structured feature development.
pattern type priority
/specflow
command
100
pattern type priority
spec out
keyword
50
pattern type priority
create spec
keyword
50
pattern type priority
new feature
keyword
50
pattern type priority
specflow specify
keyword
50
pattern type priority
spec-driven
keyword
40

SpecFlow - Spec-Driven Development

Multi-agent orchestration for spec-driven development (SDD). Based on GitHub's spec-kit methodology.

Philosophy

[...]

"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/hooks/SkillEnforcer.hook.ts"
},
...
#!/usr/bin/env bun
/**
* SkillEnforcer.hook.ts - Deterministic Skill Surfacing (UserPromptSubmit)
*
* PURPOSE:
* Matches user prompts against skill triggers using deterministic pattern
* matching. Surfaces matched skills as context suggestions - does NOT
* enforce or mandate skill invocation.
*
* TRIGGER: UserPromptSubmit
*
* INPUT:
* - stdin: JSON with { prompt: string }
*
* OUTPUT:
* - stdout: <system-reminder> with matched skill suggestion(s)
* - exit(0): Always (non-blocking)
*
* PATTERN MATCHING ORDER:
* 1. Command patterns (/skill-name) - exact match, highest priority
* 2. Keyword patterns (create skill) - exact phrase match
* 3. Fuzzy patterns (deploy) - contains match, lowest priority
*
* PERFORMANCE:
* - Non-blocking: Yes
* - Typical execution: <10ms
* - No AI inference: Pure pattern matching
*
* INTER-HOOK RELATIONSHIPS:
* - DEPENDS ON: LoadContext (builds registry at session start)
* - MUST RUN BEFORE: Other UserPromptSubmit hooks
* - COORDINATES WITH: None
*/
import { readFileSync, existsSync } from 'fs';
import {
matchTriggers,
deserializeRegistry,
REGISTRY_PATH,
type SkillTrigger,
type SkillRegistry,
} from './lib/skill-registry';
// --- Output Formatting ---
/**
* Format a single skill suggestion as system-reminder
*/
export function formatSuggestion(trigger: SkillTrigger): string {
return `<system-reminder>
SKILL SUGGESTION: The **${trigger.skillName}** skill may help with this request.
To invoke: Use the Skill tool with \`skill: "${trigger.skillName}"\`
Matched: "${trigger.pattern}" (${trigger.type}, priority ${trigger.priority})
</system-reminder>`;
}
/**
* Format multiple skill suggestions as system-reminder
*/
export function formatMultipleSuggestions(triggers: SkillTrigger[]): string {
if (triggers.length === 0) {
return '';
}
if (triggers.length === 1) {
return formatSuggestion(triggers[0]);
}
const lines = triggers.map((t, i) =>
`${i + 1}. **${t.skillName}** - matched "${t.pattern}" (${t.type})`
);
return `<system-reminder>
SKILL SUGGESTIONS: Multiple skills may be relevant:
${lines.join('\n')}
Review and invoke the most appropriate skill.
</system-reminder>`;
}
// --- Main Hook Logic ---
async function main() {
try {
// Read prompt from stdin
const input = await Bun.stdin.text();
if (!input.trim()) {
// No input, silent exit
process.exit(0);
}
let prompt: string;
try {
const parsed = JSON.parse(input);
prompt = parsed.prompt || '';
} catch {
// If not JSON, treat the input as the prompt directly
prompt = input.trim();
}
if (!prompt) {
process.exit(0);
}
// Check if registry exists
if (!existsSync(REGISTRY_PATH)) {
// No registry yet, silent passthrough
process.exit(0);
}
// Load cached registry
let registry: SkillRegistry;
try {
const content = readFileSync(REGISTRY_PATH, 'utf-8');
registry = deserializeRegistry(JSON.parse(content));
} catch (error) {
// Invalid registry, silent passthrough
console.error('⚠️ SkillEnforcer: Failed to load registry');
process.exit(0);
}
// Match triggers (with deduplication by skill name)
const matches = matchTriggers(prompt, registry, { dedupe: true });
if (matches.length === 0) {
// No matches, silent passthrough
process.exit(0);
}
// Output suggestion(s)
const output = formatMultipleSuggestions(matches);
if (output) {
// Debug output to stderr (visible in terminal)
const skillNames = matches.map(m => m.skillName).join(', ');
console.error(`🎯 SkillEnforcer: Matched [${skillNames}] for "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`);
// Context injection via stdout
console.log(output);
}
process.exit(0);
} catch (error) {
// Non-blocking: always exit 0 even on errors
console.error('⚠️ SkillEnforcer error:', error);
process.exit(0);
}
}
// Only run main if this is the entry point (not imported for testing)
if (import.meta.main) {
main();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment