Files
brain/tools/enforce-decomposition-detector.mjs
T

56 lines
2.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* enforce-decomposition-detector — PreToolUse wrapper around the pure
* decomposition-detector module (router-gate v4 §3.8 + v4.1 Direction 3).
*
* Catches features secretly decomposed into 3+ small prompts with overlapping
* keywords WITHOUT a planning skill (writing-plans / brainstorming) ever
* being invoked. v4.1 hard-blocks mutating tools when LLM-judge confirms.
*
* Stream H Task 5 — adds the wrapper. Pure detection + decision logic live
* in decomposition-detector.mjs; this file is just the hook entry point.
*
* Settings.json registration deferred to Phase H-α/H-β batch step.
*/
import { detectDecompositionCandidate, decideDecomposition, V4_1_DECOMP_THRESHOLD } from './decomposition-detector.mjs';
/**
* Pure decision composing detector + decider with a degraded-allow fallback
* when the LLM verdict is missing (fail-open on the LLM layer — matches the
* same pattern as llm-judge-per-tool).
*
* @param {object} args
* @param {Array} args.history - prior prompt entries (oldest → newest)
* @param {object} args.currentEntry - the current prompt entry
* @param {string|null} args.llmVerdict - 'YES' | 'NO' | null
* @param {object} [args.threshold] - override the v4.1 thresholds
* @returns {{action:'allow'|'soft_flag'|'hard_block_mutating', reason?:string, degraded?:boolean}}
*/
export function decide({ history, currentEntry, llmVerdict, threshold = V4_1_DECOMP_THRESHOLD }) {
const candidate = detectDecompositionCandidate(history, currentEntry, threshold);
if (!candidate.candidate) return { action: 'allow' };
if (llmVerdict === null || llmVerdict === undefined) {
// Threshold met but no LLM verdict available — degrade to soft surface
// rather than hard-block (avoid the Stream G Task 8 self-lockout pattern
// where a fail-CLOSE LLM hook bricks the session).
return { action: 'soft_flag', reason: `${candidate.reason} (LLM judge unavailable — degraded allow)`, degraded: true };
}
return decideDecomposition(candidate, llmVerdict, threshold);
}
async function main() {
// Minimal main(): without an active LLM-judge config + history-ledger reader,
// this hook degrades to allow-with-soft-flag. Wiring full live behaviour is
// Phase H-α/H-β tail work (LLM judge config from Stream D, history ledger
// from observer Stop hook). Until then: exit 0 silently to avoid lockout.
let input = '';
for await (const chunk of process.stdin) input += chunk;
// Intentionally no decode/parse — the hook is a no-op until history-ledger
// + LLM-judge config are wired in the deferred batch.
process.exit(0);
}
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || (process.argv[1] || '').endsWith('enforce-decomposition-detector.mjs')) {
main().catch(() => process.exit(0));
}