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