56 lines
2.8 KiB
JavaScript
56 lines
2.8 KiB
JavaScript
|
|
#!/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));
|
|||
|
|
}
|