Files
portal/tools/enforce-mentor-then-judge.mjs
T
Дмитрий 37ce1ae2ea feat(wall): оркестратор наставник-судья - строгая последовательность печати
Новый enforce-mentor-then-judge.mjs запускает наставника дочерним процессом до конца, потом судью (свежий mentor-GO/вердикт) - убирает гонку параллельных PostToolUse-хуков. Машины enforce-mentor-on-plan-write/enforce-judge-gate байт-в-байт не тронуты. Зарегистрирован в settings.json. TDD +5 тестов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:15:37 +03:00

60 lines
3.3 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.
#!/usr/bin/env node
/**
* enforce-mentor-then-judge — оркестратор строгой последовательности «наставник → судья».
* Заменяет ДВА параллельных PostToolUse Write-хука одним: наставник до конца, ПОТОМ судья
* (читает свежий mentor-GO/вердикт). Машины enforce-mentor-on-plan-write / enforce-judge-gate
* НЕ меняются — запускаются как дочерние процессы последовательно, событие в stdin.
* Регистрация (один блок вместо двух) — шаг владельца.
*/
import { readStdin, exitDecision } from './enforce-hook-helpers.mjs';
import { spawn } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export const MENTOR_HOOK = join(__dirname, 'enforce-mentor-on-plan-write.mjs');
export const JUDGE_HOOK = join(__dirname, 'enforce-judge-gate.mjs');
/**
* Запустить хук-процесс, передать событие в stdin, вернуть {code, stderr}.
* Протокол (exitDecision): block → stderr + exit 2; allow → stdout '{}' + exit 0.
* stdout дочки гасим (не льём в свой stdout). Не смог запуститься → code 0 (producer fail-safe).
*/
export function spawnHookProc(scriptPath, eventJson) {
return new Promise((resolve) => {
let ch;
try { ch = spawn(process.execPath, [scriptPath], { stdio: ['pipe', 'pipe', 'pipe'] }); }
catch { resolve({ code: 0, stderr: '' }); return; }
let stderr = '';
ch.stderr.on('data', (d) => { stderr += String(d); });
ch.on('close', (code) => resolve({ code: code == null ? 0 : code, stderr: stderr.trim() }));
ch.on('error', () => resolve({ code: 0, stderr: '' }));
try { ch.stdin.write(eventJson); ch.stdin.end(); } catch { /* дочка закрыла stdin */ }
});
}
/**
* Чистое ядро: наставник ДО КОНЦА, потом судья (строго после, свежий mentor-GO/вердикт).
* Наставник exit-2 → block с его stderr, судья НЕ запускается (круг L1 первым). Наставник
* allow (любой не-2 код — producer fail-safe) → судья запускается, его решение наружу.
*/
export async function runMentorThenJudge(eventJson, { spawnHook } = {}) {
const m = await spawnHook(MENTOR_HOOK, eventJson);
if (m && m.code === 2) return { block: true, message: m.stderr };
const j = await spawnHook(JUDGE_HOOK, eventJson);
return { block: !!(j && j.code === 2), message: (j && j.code === 2) ? j.stderr : undefined };
}
async function main() {
try {
const raw = await readStdin();
const r = await runMentorThenJudge(raw, { spawnHook: spawnHookProc });
exitDecision({ block: r.block, message: r.block ? r.message : undefined });
} catch {
exitDecision({ block: false }); // обёртка-оркестратор никогда не кирпичит на своей ошибке
}
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();