2026-05-15 17:17:27 +03:00
|
|
|
|
#!/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 = /(?<!\p{L})королев(?:ой|ою|а|у|е|ы)(?!\p{L})/iu;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Disqualifying prefixes — if one appears in the ~30 chars before a trigger
|
|
|
|
|
|
* match, the match is meta-discussion, not activation.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const DISCUSSION_PREFIXES = [
|
|
|
|
|
|
'что такое', 'что значит', 'как работает', 'как работают',
|
|
|
|
|
|
'не используй', 'не активируй', 'не запускай', 'не пиши',
|
|
|
|
|
|
'пример', 'вместо', 'забудь про',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-05-15 17:25:09 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* Discussion prefixes compiled with a leading Unicode word-boundary so a
|
|
|
|
|
|
* prefix never matches inside a larger word (e.g. `пример` must not match
|
|
|
|
|
|
* inside `например`).
|
|
|
|
|
|
*/
|
|
|
|
|
|
const DISCUSSION_PATTERNS = DISCUSSION_PREFIXES.map(
|
|
|
|
|
|
(kw) => new RegExp(`(?<!\\p{L})${kw}`, 'u'),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-05-15 17:17:27 +03:00
|
|
|
|
/** Extract the `prompt` string from UserPromptSubmit stdin JSON. Fail-open -> ''. */
|
|
|
|
|
|
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();
|
2026-05-15 17:25:09 +03:00
|
|
|
|
return DISCUSSION_PATTERNS.some((re) => re.test(prefix));
|
2026-05-15 17:17:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 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();
|
|
|
|
|
|
}
|