site-mariage/.claude/hooks/pre-compact-save.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

148 lines
4.2 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* PreCompact hook — snapshot critical BYAN state before Claude Code
* compacts the context window.
*
* Writes _byan-output/compact-snapshots/<timestamp>.md with :
* - active FD state (phase, backlog, dispatch table)
* - last 50 tool-log.jsonl entries (activity trail)
* - soul-memory.md head (who BYAN is, red lines)
* - recent commits (git log -10)
*
* Emits a short systemMessage telling Claude : compact happened, key
* state preserved at <path>, read it if you lose track mid-session.
*
* Never blocks compaction. Exit 0 always, even on partial failure.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const outDir = path.join(projectDir, '_byan-output', 'compact-snapshots');
function readTextSafe(filePath, maxBytes = 4000) {
try {
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, 'utf8');
return raw.length > maxBytes ? raw.slice(0, maxBytes) + '\n\n... [truncated]' : raw;
} catch {
return null;
}
}
function readJsonSafe(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function tailJsonl(filePath, n) {
try {
if (!fs.existsSync(filePath)) return [];
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
return lines.slice(-n).map((l) => {
try {
return JSON.parse(l);
} catch {
return null;
}
}).filter(Boolean);
} catch {
return [];
}
}
function recentCommits() {
try {
return execSync('git log -10 --oneline', {
cwd: projectDir,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
return '(git log unavailable)';
}
}
function stamp() {
const d = new Date();
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}
function renderSnapshot() {
const lines = [];
lines.push(`# BYAN pre-compact snapshot — ${new Date().toISOString()}`);
lines.push('');
const fd = readJsonSafe(path.join(projectDir, '_byan-output', 'fd-state.json'));
if (fd) {
lines.push('## Active FD state');
lines.push('');
lines.push('```json');
lines.push(JSON.stringify(fd, null, 2).slice(0, 3000));
lines.push('```');
lines.push('');
} else {
lines.push('## Active FD state');
lines.push('');
lines.push('(none)');
lines.push('');
}
lines.push('## Recent commits');
lines.push('');
lines.push('```');
lines.push(recentCommits());
lines.push('```');
lines.push('');
const tool = tailJsonl(path.join(projectDir, '_byan-output', 'tool-log.jsonl'), 50);
lines.push(`## Last ${tool.length} tool calls`);
lines.push('');
for (const e of tool) {
const ts = e.timestamp || '?';
const phase = e.phase || '?';
const summary = e.summary || (e.ok === false ? `FAIL ${e.failure_kind}` : 'ok');
lines.push(`- ${ts} ${phase} ${e.tool || '?'}${String(summary).slice(0, 120)}`);
}
lines.push('');
const soul = readTextSafe(path.join(projectDir, '_byan', 'soul.md'), 2000);
if (soul) {
lines.push('## Soul (head)');
lines.push('');
lines.push(soul);
lines.push('');
}
return lines.join('\n');
}
(function main() {
try {
fs.mkdirSync(outDir, { recursive: true });
const outPath = path.join(outDir, `${stamp()}.md`);
fs.writeFileSync(outPath, renderSnapshot());
const relative = path.relative(projectDir, outPath);
process.stdout.write(
JSON.stringify({
systemMessage: `BYAN pre-compact snapshot written to ${relative}. Critical state (FD phase, commits, tool activity, soul) preserved outside the context window. Read it after compaction if you lose track.`,
})
);
} catch (err) {
// Must never block compaction
process.stdout.write(
JSON.stringify({
systemMessage: `pre-compact-save hook error (non-blocking) : ${err.message}`,
})
);
}
process.exit(0);
})();