#!/usr/bin/env node /** * enforce-skill-journaler (Машина 1, Фаза 3 SE-K) — PostToolUse(Skill). * Пишет КАЖДЫЙ Skill-вызов в action-journal (op:'Skill') независимо от членства в плане — * seed/observe/escape стеной М2 НЕ журналятся, а K2 судьи М4 (навык-по-журналу, не по тексту) * обязан их видеть. Дедуп vs wall-pre-write плановых Skill: стена пишет op:'Skill' на plan-step * (PreToolUse) → журналер видит её хвостом цепи и пропускает. * Best-effort: PostToolUse — навык уже отработал, блокировать нечего. fail-CLOSE-дисциплина * живёт в потреблении K2 (Фаза 4); сам журналер числится в FAIL_CLOSE_DISCIPLINE_HOOKS * для манифест-самопроверки Фазы 6 (страж обязан быть зарегистрирован). */ import { readStdin, parseEventJson, exitDecision } from './enforce-hook-helpers.mjs'; /** * Дедуп: писать ли этот Skill в журнал. false если хвост цепи — уже та же op:'Skill' * запись (её записала стена на plan-step) — иначе плановый навык задвоился бы. */ export function shouldJournalSkill(skillName, journalEntries) { if (!skillName || typeof skillName !== 'string') return false; const arr = Array.isArray(journalEntries) ? journalEntries : []; const last = arr.length ? arr[arr.length - 1] : null; if (last && last.payload && last.payload.op === 'Skill' && last.payload.object === skillName) return false; return true; } /** Мост журнал → K2: имена (object) всех op:'Skill' записей по порядку (битые игнор). */ export function extractSkillCalls(entries) { if (!Array.isArray(entries)) return []; const out = []; for (const e of entries) { const p = e && e.payload; if (p && p.op === 'Skill' && typeof p.object === 'string' && p.object) out.push(p.object); } return out; } async function main() { try { const event = parseEventJson(await readStdin()); if (event.tool_name !== 'Skill') { exitDecision({ block: false }); return; } const skillName = String((event.tool_input && event.tool_input.skill) || ''); if (skillName) { const os = await import('node:os'); const { loadJournal, journalAppend } = await import('./action-journal.mjs'); const { resolveReceiptKey } = await import('./receipt-key-config.mjs'); const runtimeDir = `${os.homedir()}/.claude/runtime`; const sess = event.session_id || 'unknown'; const { entries } = loadJournal({ sessionId: sess, runtimeDir }); if (shouldJournalSkill(skillName, entries)) { journalAppend({ payload: { op: 'Skill', object: skillName, step: null, at: event.nowMs ?? null, source: 'skill-journaler' }, key: resolveReceiptKey(), sessionId: sess, runtimeDir, }); } } } catch { /* best-effort recorder — PostToolUse не блокирует */ } exitDecision({ block: false }); } import { fileURLToPath } from 'node:url'; const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isCli) main();