397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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));
|
||
}
|