#!/usr/bin/env node /** * enforce-domain-skill-discipline — детерминированный судья дисциплины навыков (подключение * готового domain-skill-discipline). Навыки, ОБЪЯВЛЕННЫЕ в ОПЛОМБИРОВАННОМ плане * (frozenPlan.skills — подписанное поле, Вариант 1), обязаны быть РЕАЛЬНО вызваны (журнал M1) * до первого мутирующего шага. Иначе блок с перечнем недостающих. fail-CLOSE (сбой → блок). * PreToolUse на мутирующие инструменты; регистрация в settings.json — шаг владельца. */ import { readStdin, parseEventJson, exitDecision, runtimeDir } from './enforce-hook-helpers.mjs'; import { domainCallDiscipline } from './domain-skill-discipline.mjs'; import { loadFrozenPlan } from './plan-lock.mjs'; import { loadJournal } from './action-journal.mjs'; import { extractSkillCalls } from './enforce-skill-journaler.mjs'; import { resolveSessionId } from './enforce-supreme-gate.mjs'; const suffix = (n) => { const s = String(n || '').toLowerCase(); return s.includes(':') ? s.split(':').pop() : s; }; // Объявленный навык покрыт вызовом, если: точное равенство ИЛИ вызванный начинается с // 'объявленный:' (объявлен плагин) ИЛИ совпали суффиксы (бэр-имена навыка). Зеркало // isPlanDeclaredSkill стены — declared↔invoked нормализуются одинаково. function callCovers(declared, invoked) { const d = String(declared || '').toLowerCase(); const i = String(invoked || '').toLowerCase(); if (!d || !i) return false; return i === d || i.startsWith(d + ':') || suffix(i) === suffix(d); } export function decide({ frozenPlan, journalSkillCalls = [] }) { const declared = (frozenPlan && Array.isArray(frozenPlan.skills)) ? frozenPlan.skills : []; if (declared.length === 0) return { block: false, reason: null }; const calls = journalSkillCalls || []; // Устойчивый матч ДО сверки: «фактически покрытые» объявленные навыки подаём как invoked // (точными строками declared), чтобы domainCallDiscipline (Set-равенство) дал верный uncalled/reason. const covered = declared.filter((dskill) => calls.some((c) => callCovers(dskill, c))); const r = domainCallDiscipline({ recommendedDomainSkills: declared, invokedSkills: covered }); return r.ok ? { block: false, reason: null } : { block: true, reason: r.reason }; } async function main() { try { const event = parseEventJson(await readStdin()); const sess = resolveSessionId(event); const dir = runtimeDir(); const frozenPlan = loadFrozenPlan({ sessionId: sess, runtimeDir: dir }); const { entries } = loadJournal({ sessionId: sess, runtimeDir: dir }); const journalSkillCalls = extractSkillCalls(entries); const r = decide({ frozenPlan, journalSkillCalls }); exitDecision({ block: r.block, message: r.block ? `[skill-discipline] ${r.reason}` : undefined }); } catch { exitDecision({ block: true, message: '[skill-discipline] внутренняя ошибка — fail-CLOSE' }); } } import { fileURLToPath } from 'node:url'; const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isCli) main();