Files
brain/tools/enforce-prompt-injection.mjs
T

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();