#!/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)); }