Files
brain/tools/router-stop-gate.mjs
T

88 lines
3.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Stop hook addition — router chain progress tracking.
* Stage 3 of router discipline overhaul.
*
* После каждого хода: обновляет state.chainProgress на основе вызванных в этом ходу скилов.
* Helper для observer-stop-hook — он сам решает, вызывать ли (зависит от того, есть ли router-state).
*/
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { fileURLToPath } from 'url';
import { readStdinAsUtf8 } from './router-stdin-helper.mjs';
export function extractSkillInvocations(events) {
return (events || [])
.filter((e) => e && e.tool_name === 'Skill')
.map((e) => {
const raw = e.tool_input?.skill || '';
const stripped = raw.includes(':') ? raw.split(':').pop() : raw;
return stripped;
})
.filter(Boolean);
}
export function updateChainProgress(state, skillsInvoked, chains) {
const chainId = state.classification?.recommendedChain;
if (!chainId || !chains[chainId]) return { ...state };
const sequence = chains[chainId].sequence || [];
const currentProgress = [...(state.chainProgress || [])];
for (const skill of skillsInvoked) {
const nextExpectedIdx = currentProgress.length;
if (nextExpectedIdx >= sequence.length) break;
if (sequence[nextExpectedIdx] === skill) {
currentProgress.push(skill);
}
}
return {
...state,
chainProgress: currentProgress,
chainCompleted: currentProgress.length === sequence.length && sequence.length > 0,
};
}
async function main() {
const input = await readStdinAsUtf8(process.stdin);
const event = JSON.parse(input || '{}');
const sessionId = event.session_id || 'unknown';
const events = event.turn_events || [];
const statePath = join(homedir(), '.claude', 'runtime', `router-state-${sessionId}.json`);
if (!existsSync(statePath)) { process.stdout.write('{}'); process.exit(0); return; }
try {
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
const { loadRegistry } = await import('./registry-load.mjs');
const registry = loadRegistry({ useCache: false });
const skills = extractSkillInvocations(events);
const updated = updateChainProgress(state, skills, registry.chains || {});
updated.skillInvokedThisTurn = skills.length > 0;
writeFileSync(statePath, JSON.stringify(updated, null, 2));
process.stdout.write('{}');
process.exit(0);
} catch (err) {
process.stderr.write(`[router-stop-gate] ${err.message}\n`);
process.stdout.write('{}');
process.exit(0);
}
}
// CLI guard — Windows-cyrillic path quirk: compare resolved path, not raw argv[1]
const isMain = (() => {
try {
return process.argv[1] &&
fileURLToPath(import.meta.url) === fileURLToPath(`file:///${process.argv[1].replace(/\\/g, '/')}`);
} catch {
return process.argv[1] && import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`;
}
})();
if (isMain) {
main();
}