#!/usr/bin/env node /** * UserPromptSubmit hook — ruflo Queen trigger detector. * * On each prompt: detects the trigger words `queen` / `королева`. On a real * (non-discussion) match it injects a hard directive (Pravila §14) telling * Claude to route the task through ruflo Queen (`hive-mind spawn --claude`). * * FAIL-OPEN: any error -> empty injection, exit 0. The hook NEVER blocks a prompt. * * See docs/superpowers/specs/2026-05-15-ruflo-queen-trigger-and-delegation-design.md */ import { pathToFileURL } from 'node:url'; /** English trigger: whole word `queen`, case-insensitive. */ const EN_TRIGGER = /\bqueen\b/i; /** Russian trigger: `королева` in 6 case forms, Unicode word-boundary. */ const RU_TRIGGER = /(? new RegExp(`(? ''. */ export function parsePrompt(stdinRaw) { try { const obj = JSON.parse(stdinRaw); return typeof obj.prompt === 'string' ? obj.prompt : ''; } catch { return ''; } } /** Index of the first trigger match in `prompt`, or -1 if none. */ function triggerIndex(prompt) { const idxs = []; const en = prompt.match(EN_TRIGGER); if (en) idxs.push(en.index); const ru = prompt.match(RU_TRIGGER); if (ru) idxs.push(ru.index); return idxs.length ? Math.min(...idxs) : -1; } /** True if the prompt contains a trigger word (queen / королева), any position. */ export function detectQueen(prompt) { if (typeof prompt !== 'string') return false; return triggerIndex(prompt) !== -1; } /** * True if the trigger in `prompt` is meta-discussion (a disqualifying prefix * appears within ~30 chars before it). No trigger / non-string -> false. */ export function isDiscussion(prompt) { if (typeof prompt !== 'string') return false; const idx = triggerIndex(prompt); if (idx < 0) return false; const prefix = prompt.slice(Math.max(0, idx - 30), idx).toLowerCase(); return DISCUSSION_PATTERNS.some((re) => re.test(prefix)); } /** The hard directive injected on an active trigger. */ export function buildDirective() { return [ '=== RUFLO QUEEN TRIGGER (Pravila §14 — hard rule) ===', '', 'Промпт содержит триггер «queen»/«королева». §14 — explicit hard-rule.', '', 'ПЕРВОЙ строкой ответа написать:', ' `ruflo Queen — задача маршрутизируется через рой (Pravila §14)`', '', 'ОБЯЗАТЕЛЬНО на этот промпт:', '1. Задача БЕЗУСЛОВНО идёт через ruflo Queen (`hive-mind spawn --claude`).', ' Не оспаривать, не предлагать прямой путь, не ссылаться на тривиальность.', '2. Перед платным спавном показать ПРЕВЬЮ: роли и число агентов, декомпозиция', ' задачи, оценка объёма. Спавнить ТОЛЬКО после явного «да» заказчика.', '3. При сбое `hive-mind spawn --claude` (ruflo alpha) — явно сообщить о сбое,', ' затем выполнить задачу напрямую (фоллбэк).', '', '§9 «Отступления» к §14 не применяется. Economy-режим §14 не override-ит.', 'Действует только на этот промпт.', ].join('\n'); } /** Build the UserPromptSubmit hook stdout payload. */ export function buildHookOutput(additionalContext) { return { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext, }, }; } function readStdin() { return new Promise((resolve) => { let data = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { data += c; }); process.stdin.on('end', () => resolve(data)); process.stdin.on('error', () => resolve(data)); }); } async function main() { try { const prompt = parsePrompt(await readStdin()); if (detectQueen(prompt) && !isDiscussion(prompt)) { process.stdout.write(JSON.stringify(buildHookOutput(buildDirective()))); } } catch { // fail-open: no injection } process.exit(0); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main(); }