37ce1ae2ea
Новый 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>
60 lines
3.3 KiB
JavaScript
60 lines
3.3 KiB
JavaScript
#!/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();
|