site-mariage/.claude/hooks/drain-advisory.js
Corentin Joguet bff653acd6 first commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:30:37 +02:00

85 lines
3.2 KiB
JavaScript

#!/usr/bin/env node
// drain-advisory.js — Stop hook. At the end of each assistant turn, drain the
// outcome buffer into BYAN's ADVISORY ledgers (ELO trust, suitability). This is the
// automatic half of the closed learning loop: outcomes logged during the turn (via
// byan_outcome_log) are recorded with NO agent action. Behavior surfaces (routing /
// personas / mantras) are never touched — only advisory data is written.
//
// STRICTLY non-blocking. All work is wrapped in try/catch; the hook ALWAYS emits
// {continue:true} and exits 0, and never throws or exits 2. An advisory feed must
// never break a turn (the stage-to-byan.js contract: "staging must never break the
// session"). Idempotent via a line cursor, so a re-fired Stop (stop_hook_active)
// records nothing new.
//
// ESM/CJS: this hook is CommonJS. The ELO engine is CJS (require). The pure libs and
// the suitability store are ESM under a type:module package, reached via dynamic
// import() with a file:// URL.
const path = require('path');
const { pathToFileURL } = require('url');
function readStdin() {
return new Promise((resolve) => {
if (process.stdin.isTTY) return resolve('');
let data = '';
process.stdin.on('data', (c) => (data += c));
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', () => resolve(data));
});
}
function done() {
process.stdout.write(JSON.stringify({ continue: true }));
process.exit(0);
}
(async () => {
try {
await readStdin(); // the Stop payload is not needed — we drain disk state
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const esm = (rel) => import(pathToFileURL(path.join(root, rel)).href);
const af = await esm('_byan/mcp/byan-mcp-server/lib/advisory-autofeed.js');
const buf = await esm('_byan/mcp/byan-mcp-server/lib/outcome-buffer.js');
const outcomes = af.parseOutcomes(buf.readBuffer({ rootDir: root }));
const cursor = buf.readCursor({ rootDir: root });
const { pending, newCursor } = af.planDrain(outcomes, cursor);
if (!pending.length) return done();
let eloEngine = null;
let suitability = null;
for (const o of pending) {
const rec = af.classifyOutcome(o);
if (!rec) continue;
try {
if (rec.kind === 'elo') {
if (!eloEngine) {
const EloEngine = require(path.join(root, 'src', 'byan-v2', 'elo', 'index.js'));
eloEngine = new EloEngine({
storagePath: path.join(root, '_byan', 'memoire', 'elo-profile.json'),
});
}
eloEngine.recordResult(rec.domain, rec.result);
} else if (rec.kind === 'suitability') {
if (!suitability) {
suitability = await esm('_byan/mcp/byan-mcp-server/lib/suitability-store.js');
}
suitability.record({
model: rec.model,
leafId: rec.leafId,
success: rec.success,
source: 'autofeed',
projectRoot: root,
});
}
} catch {
// one bad record must not abort the drain or block the turn
}
}
buf.writeCursor(newCursor, { rootDir: root });
} catch {
// any failure degrades silently — the feed is housekeeping, never a blocker
}
done();
})();