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

56 lines
2.8 KiB
JavaScript
Raw Normal View History

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