397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
63 lines
3.3 KiB
JavaScript
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();
|