397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
4.2 KiB
JavaScript
114 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Rule #1 — Mandatory re-classification injection.
|
|
*
|
|
* UserPromptSubmit hook. Reads router-state-<session>.json (output of the
|
|
* existing router-prehook), reads rationalization flags from previous turns,
|
|
* and injects an `additionalContext` block into the conversation.
|
|
*
|
|
* The block:
|
|
* 1. Reminds: first line must be `coverage: <channel>:<id>`
|
|
* 2. Lists recommended node/skill from classifier
|
|
* 3. Surfaces previous-turn rationalization flags (if any)
|
|
*
|
|
* NEVER blocks the prompt — failed injection just means no reminder appears.
|
|
*
|
|
* Spec: docs/superpowers/specs/2026-05-25-enforce-hard-rules-design.md
|
|
*/
|
|
|
|
import {
|
|
readStdin,
|
|
parseEventJson,
|
|
readRouterState,
|
|
readRationalizationFlags,
|
|
findOverride,
|
|
loadOverrideVocab,
|
|
} from './enforce-hook-helpers.mjs';
|
|
|
|
const SUPPRESS_RULE = 'classifier-mismatch';
|
|
|
|
export function buildReminder({ classification, recentFlags, override }) {
|
|
const lines = ['## §17 Coverage / Discipline Reminder', ''];
|
|
if (override) {
|
|
lines.push(`Override phrase detected: "${override.phrase}". The following rules are suppressed for THIS prompt only:`);
|
|
lines.push(` ${override.suppresses.join(', ')}`);
|
|
lines.push('');
|
|
}
|
|
lines.push('**First line of your response MUST be:**');
|
|
lines.push(' `coverage: <channel>:<id>`');
|
|
lines.push('Channels: skill, node, chain, hook, agent, direct.');
|
|
lines.push('');
|
|
if (classification) {
|
|
lines.push(`**Classifier output:** task_type=${classification.task_type || 'unknown'}, confidence=${classification.confidence ?? 'n/a'}`);
|
|
if (classification.recommended_node) {
|
|
lines.push(`**Recommended node:** ${classification.recommended_node}`);
|
|
}
|
|
if (classification.recommended_chain) {
|
|
lines.push(`**Recommended chain:** ${classification.recommended_chain}`);
|
|
}
|
|
if (classification.task_type && /^(feature|bugfix|refactor|cleanup)$/i.test(classification.task_type)) {
|
|
lines.push(`**Plan required:** task type ${classification.task_type} requires either Skill(superpowers:writing-plans) invocation OR an existing plan file referenced before first production-code edit.`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
if (Array.isArray(recentFlags) && recentFlags.length > 0) {
|
|
const recent = recentFlags.slice(-3);
|
|
lines.push('**Previous turn flagged:**');
|
|
for (const f of recent) lines.push(` - ${f.kind}: ${typeof f.evidence === 'string' ? f.evidence.slice(0, 120) : ''}`);
|
|
lines.push('Adjust behaviour accordingly.');
|
|
lines.push('');
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
const raw = await readStdin();
|
|
const event = parseEventJson(raw);
|
|
const sessionId = event.session_id;
|
|
const userPrompt = event.prompt || '';
|
|
|
|
// Override does NOT suppress this injection (it just notes the override).
|
|
const vocab = loadOverrideVocab();
|
|
let override = null;
|
|
for (const p of (vocab.phrases || [])) {
|
|
if (!p.phrase) continue;
|
|
if (userPrompt.toLowerCase().includes(p.phrase.toLowerCase())) { override = p; break; }
|
|
}
|
|
|
|
// Wait up to ~600ms for router-prehook to write state.
|
|
let state = readRouterState(sessionId);
|
|
if (!state) {
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
for (let i = 0; i < 3 && !state; i++) {
|
|
await sleep(200);
|
|
state = readRouterState(sessionId);
|
|
}
|
|
}
|
|
const classification = state && state.classification ? {
|
|
task_type: state.classification.task_type,
|
|
confidence: state.classification.confidence,
|
|
recommended_node: state.classification.recommended_node || state.classification.recommendedNode,
|
|
recommended_chain: state.classification.recommended_chain || state.classification.recommendedChain,
|
|
} : null;
|
|
|
|
const flags = readRationalizationFlags(sessionId);
|
|
|
|
const reminder = buildReminder({ classification, recentFlags: flags, override });
|
|
|
|
process.stdout.write(JSON.stringify({
|
|
hookSpecificOutput: {
|
|
hookEventName: 'UserPromptSubmit',
|
|
additionalContext: reminder,
|
|
},
|
|
}));
|
|
process.exit(0);
|
|
} catch {
|
|
try { process.stdout.write('{}'); } catch { /* ignore */ }
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-prompt-injection.mjs');
|
|
if (isCli) main();
|