Files
brain/tools/enforce-skill-journaler.mjs
T

63 lines
3.3 KiB
JavaScript

#!/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();