#!/usr/bin/env node /** * standby-mode-control — UserPromptSubmit-хук штатного режима. * Двухтактное рукопожатие включения, выключение одной фразой, баннер пока активно. * Флаг пишется ТОЛЬКО здесь (реакция на промпт владельца). Fail-quiet: ошибка не ломает промпт. */ import { fileURLToPath } from 'url'; import { readStdin, parseEventJson, writeSentinel, readSentinel, removeSentinel, standbyActive } from './enforce-hook-helpers.mjs'; const ENABLE_RE = /штатн\p{L}*\s+режим/iu; const CONFIRM_RE = /(?:^|\s)да,?\s*штатн/iu; const DISABLE_RE = /выключ\p{L}*\s+штатн/iu; const BANNER = '⚠️ ШТАТНЫЙ РЕЖИМ — стен нет (оставлены: пол, точки возврата, проверка-перед-пушем). Верни стену фразой «выключи штатный».'; const CONFIRM_ASK = '⚠️ Включить ШТАТНЫЙ — стен нет (останутся пол + проверка пуша). Точно? Подтверди фразой: «да, штатный».'; const ENABLED_MSG = '✅ ШТАТНЫЙ ВКЛЮЧЁН. ' + BANNER; const DISABLED_MSG = '🔒 Штатный выключен — стена возвращена (предреализация).'; /** Чистое решение. Порядок проверок фиксирован: disable → confirm → enable-trigger → banner. */ export function decideControl({ text, active, pending }) { if (DISABLE_RE.test(text)) return { action: 'disable', context: DISABLED_MSG }; if (CONFIRM_RE.test(text) && pending) return { action: 'enable', context: ENABLED_MSG }; if (ENABLE_RE.test(text) && !active && !pending) return { action: 'pending', context: CONFIRM_ASK }; if (active) return { action: 'banner', context: BANNER }; return { action: 'none', context: null }; } function output(context) { if (context) { process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } })); } else { process.stdout.write('{}'); } process.exit(0); } async function main() { try { const event = parseEventJson(await readStdin()); const sess = event.session_id || 'unknown'; const text = String(event.prompt || event.user_prompt || ''); const active = standbyActive(sess); const pending = !!readSentinel('standby-pending', sess); const r = decideControl({ text, active, pending }); if (r.action === 'disable') { removeSentinel('standby-mode', sess); removeSentinel('standby-pending', sess); } else if (r.action === 'enable') { writeSentinel('standby-mode', sess, { active: true }); removeSentinel('standby-pending', sess); } else if (r.action === 'pending') { writeSentinel('standby-pending', sess, { pending: true }); } output(r.context); } catch { process.stdout.write('{}'); process.exit(0); // fail-quiet: ошибка не ломает промпт, режим не меняется } } const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isCli) main();