Files
portal/tools/enforce-decomposition-detector.mjs
T
Дмитрий 63686fa5b2 feat(router-gate-v4): Stream H Task 5 — decomposition-detector wrapper hook (PreToolUse, deferred activation)
Closes Stream H Task 5 (H6). Adds the PreToolUse wrapper around the pure
decomposition-detector module (Stream A Direction 3 / v4.1 §3.8).

What this catches:
- A feature secretly decomposed into 3+ small prompts whose primary_keywords
  overlap heavily AND no planning skill (writing-plans / brainstorming) has
  been invoked in the window. v4.1 hard-blocks mutating tools when the LLM
  judge confirms decomposition; soft-flags on legit-distinct verdict; allows
  when threshold not met or a planning skill was invoked.

Defensive design choices:
- decide() takes llmVerdict as an explicit string ('YES'|'NO'|null), not an
  async LLM call — keeps the function pure and unit-testable
  without network.
- llmVerdict=null degrades to soft_flag (with degraded:true), NOT hard_block.
  This avoids repeating the Stream G Task 8 self-lockout where a fail-CLOSE
  LLM hook bricked the session.
- main() is a no-op (exit 0) until the deferred wiring lands (history-ledger
  reader from observer Stop hook + LLM judge config from Stream D). Until
  then, the hook never blocks anything.

Regression: vitest tools 1748/1748 GREEN (was 1742; +6 wrapper-decide tests
under "enforce-decomposition-detector wrapper (Stream H Task 5)" describe
block, covering: empty history → allow, below threshold → allow, threshold
+ LLM YES → hard_block_mutating, threshold + LLM NO → soft_flag, threshold
+ skill present → allow, threshold + LLM unavailable → degraded soft_flag).

DEFERRED: .claude/settings.json registration (PreToolUse matcher
"Edit|Write|MultiEdit|NotebookEdit|Bash|Task", timeout 8000ms) AND main()
wiring (history-ledger reader + LLM judge integration). Batched with
H5/H7/H8 hook activations at end of Phase H-α/H-β.

Stream H Task 5 of 11. Plan: docs/superpowers/plans/2026-05-30-router-gate-v4-stream-H.md
2026-05-30 11:31:00 +03:00

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));
}