85 lines
3.2 KiB
JavaScript
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();
|
|
})();
|