Files
brain/tools/enforce-todowrite-skill-verifier.mjs
T
Дмитрий abf2060328 feat standby: штатный режим - флаг, управляющий хук, сброс, страж в 12 хуков
Сессионный флаг standby-mode + управляющий UserPromptSubmit-хук рукопожатия + SessionStart-сброс. Страж if standbyActive в 12 блокирующих хуках; рельсы floor/snapshot/verify-gate не тронуты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:07:04 +03:00

84 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* PreToolUse-страж: TodoWrite заявил навык → должен быть в ЖУРНАЛЕ вызовов (М7 Фаза 4a, §4.2).
* Выполненный todo, claim'ящий Skill, не подтверждённый журналом (канал М1) → блок следующего
* мутирующего. Session-scope (выполненный todo мог закрыться в прошлом ходе — навык должен быть
* в журнале за сессию, не обязательно в этом ходе; отличие от coverage, которое turn-scoped).
*
* fail-CLOSE (М7 Фаза 0 правило 1). PreToolUse (предотвращение до действия, §4.2 [Pre]).
*/
import { fileURLToPath } from 'url';
import {
readStdin,
parseEventJson,
readTranscript,
runtimeDir,
exitDisciplineDecision,
} from './enforce-hook-helpers.mjs';
import {
extractSkillMentions,
hardSyncCheck,
} from './todowrite-skill-verifier.mjs';
import { loadJournal } from './action-journal.mjs';
import { extractSkillCalls } from './enforce-skill-journaler.mjs';
/** Find the latest TodoWrite tool_use entries in a transcript. */
export function lastTodoItems(transcript) {
const recs = transcript || [];
for (let i = recs.length - 1; i >= 0; i--) {
const r = recs[i];
if (r && r.type === 'tool_use' && r.name === 'TodoWrite') {
return r.input && Array.isArray(r.input.todos) ? r.input.todos : [];
}
}
return [];
}
/**
* Дедуп regex-артефактов extractSkillMentions: "invoke superpowers:brainstorming" даёт и полное
* "superpowers:brainstorming", и частичное "superpowers" (invoke-паттерн стопится на двоеточии).
* Частичное избыточно, если из того же текста есть mention, начинающийся с "<partial>:".
*/
export function deduplicateMentions(mentions) {
return mentions.filter((m) => {
const nameColon = m.skill_name + ':';
const coveredByLonger = mentions.some(
(other) => other !== m && other.text === m.text && other.skill_name.startsWith(nameColon),
);
return !coveredByLonger;
});
}
export function decide({ todoItems, journalSkillCalls = [] }) {
const items = Array.isArray(todoItems) ? todoItems : [];
const rawMentions = extractSkillMentions(items);
const mentions = deduplicateMentions(rawMentions);
const calls = (journalSkillCalls || []).map((n) => ({ skill_name: n }));
const r = hardSyncCheck(mentions, calls);
if (r.action === 'hard_block_next_mutating') {
return { block: true, reason: r.reason };
}
return { block: false, reason: null };
}
async function main() {
const event = parseEventJson(await readStdin());
{ const __h = await import('./enforce-hook-helpers.mjs'); if (__h.standbyActive((event && event.session_id) || 'unknown')) return __h.exitDecision({ block: false }); }
await exitDisciplineDecision(
() => {
const transcript = readTranscript(event.transcript_path);
const todoItems = lastTodoItems(transcript);
const { entries } = loadJournal({
sessionId: event.session_id || 'unknown',
runtimeDir: runtimeDir(),
});
const journalSkillCalls = extractSkillCalls(entries);
const r = decide({ todoItems, journalSkillCalls });
return { block: r.block, message: r.block ? `[todowrite-skill-verifier] ${r.reason}` : undefined };
},
{ label: 'todowrite-skill-verifier' },
);
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();