mirror of
https://github.com/kochetkov-ma/claude-brewcode.git
synced 2026-09-14 20:16:41 +08:00
155 lines
5.6 KiB
JavaScript
155 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PreCompact hook
|
|
* Validates state, compacts knowledge, writes handoff entry before auto-compact
|
|
*
|
|
* ============================================================================
|
|
* IMPORTANT: SESSION_ID DOES NOT CHANGE AFTER COMPACT!
|
|
* ============================================================================
|
|
* Claude Code's auto-compact summarizes context within THE SAME session.
|
|
* The session_id remains identical before and after compact.
|
|
*
|
|
* Therefore:
|
|
* - Lock file keeps session_id bound (no release needed)
|
|
* - checkLock() will match after compact resumes
|
|
* - Status 'handoff' is for Claude to re-read TASK.md, not for new session
|
|
* ============================================================================
|
|
*/
|
|
import { existsSync, readdirSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import {
|
|
readStdin,
|
|
output,
|
|
getKnowledgePath,
|
|
getReportsDir,
|
|
parseTask,
|
|
updateTaskStatus,
|
|
getState,
|
|
saveState,
|
|
loadConfig,
|
|
checkLock,
|
|
log
|
|
} from './lib/utils.mjs';
|
|
import {
|
|
localCompact,
|
|
writeHandoffEntry
|
|
} from './lib/knowledge.mjs';
|
|
|
|
async function main() {
|
|
let cwd = null;
|
|
let session_id = null;
|
|
|
|
try {
|
|
cwd = process.cwd();
|
|
const input = await readStdin();
|
|
session_id = input.session_id;
|
|
cwd = input.cwd || cwd;
|
|
|
|
// Check if brewcode is active AND owned by this session
|
|
const lock = checkLock(cwd, session_id);
|
|
if (!lock) {
|
|
// No valid lock for this session - allow compact without handoff logic
|
|
output({ continue: true });
|
|
return;
|
|
}
|
|
|
|
// Get task path from lock (session-validated)
|
|
// Warn but continue: lock exists but task_path missing indicates corrupt state
|
|
if (!lock.task_path) {
|
|
log('warn', '[pre-compact]', 'Lock missing task_path', cwd, session_id);
|
|
output({ continue: true });
|
|
return;
|
|
}
|
|
const taskPath = join(cwd, lock.task_path);
|
|
|
|
// Parse task to get current state
|
|
const task = parseTask(taskPath, cwd);
|
|
if (!task) {
|
|
log('warn', '[pre-compact]', 'Failed to parse task file', cwd, session_id);
|
|
output({ continue: true });
|
|
return;
|
|
}
|
|
|
|
// If task is in a terminal state, allow compact without handoff logic
|
|
const TERMINAL_STATUSES = new Set(['finished', 'failed', 'cancelled', 'error']);
|
|
if (TERMINAL_STATUSES.has(task.status)) {
|
|
output({ continue: true });
|
|
return;
|
|
}
|
|
|
|
// VALIDATE STATE
|
|
const validationIssues = [];
|
|
|
|
// 1. Check artifacts directory exists for current phase
|
|
const artifactsDir = getReportsDir(taskPath);
|
|
const phasePattern = `${task.currentPhase}-`;
|
|
let hasPhaseDir = false;
|
|
if (existsSync(artifactsDir)) {
|
|
const entries = readdirSync(artifactsDir);
|
|
hasPhaseDir = entries.some(e => e.startsWith(phasePattern));
|
|
}
|
|
if (!hasPhaseDir) {
|
|
validationIssues.push(`Artifacts directory missing for phase ${task.currentPhase}`);
|
|
}
|
|
|
|
// If validation issues, warn but continue (don't block compact)
|
|
if (validationIssues.length > 0) {
|
|
log('debug', '[pre-compact]', `Validation warnings (agent may still be executing): ${validationIssues.join('; ')}`, cwd, session_id);
|
|
// Still continue - better to compact than crash
|
|
}
|
|
|
|
// COMPACT KNOWLEDGE
|
|
const knowledgePath = getKnowledgePath(taskPath);
|
|
const config = loadConfig(cwd);
|
|
if (existsSync(knowledgePath)) {
|
|
const compacted = localCompact(knowledgePath, config.knowledge.maxEntries, cwd);
|
|
if (compacted) {
|
|
log('info', '[pre-compact]', 'Knowledge compacted successfully', cwd, session_id);
|
|
}
|
|
}
|
|
|
|
// WRITE HANDOFF ENTRY
|
|
// Empirical: prepending CRITICAL: PRESERVE emphasizes priority for the
|
|
// autocompact summarizer LLM. Not a documented CC contract — best-effort
|
|
// emphasis, not guaranteed retention.
|
|
writeHandoffEntry(knowledgePath, task.currentPhase, 'CRITICAL: PRESERVE handoff state across context auto-compact');
|
|
|
|
// UPDATE STATUS to handoff
|
|
updateTaskStatus(taskPath, 'handoff');
|
|
|
|
// UPDATE STATE
|
|
const state = getState(cwd);
|
|
state.lastHandoff = new Date().toISOString();
|
|
state.lastPhase = task.currentPhase;
|
|
state.lastCompactAt = new Date().toISOString();
|
|
saveState(cwd, state);
|
|
|
|
log('info', '[pre-compact]', `Handoff to phase ${task.currentPhase}`, cwd, session_id);
|
|
|
|
// Detect v3: phases/ directory exists inside the task dir
|
|
const taskDir = dirname(taskPath);
|
|
const isV3 = existsSync(join(taskDir, 'phases'));
|
|
|
|
// Empirical: CRITICAL/PRESERVE prefixes mark this content for summarizer
|
|
// emphasis during autocompact. Not a documented CC heuristic — best-effort
|
|
// in-context emphasis only, retention not guaranteed.
|
|
const systemMessage = isV3
|
|
? `Marked for summarizer emphasis — brewcode compact handoff, phase ${task.currentPhase}/${task.totalPhases}. After compact: 1) TaskList() for current task state 2) Read PLAN.md for protocol 3) DO NOT read phases/ — they are for agents 4) Continue with current in_progress or next pending task 5) WRITE report → CALL coordinator after EVERY agent. Note: lock file session binding active.`
|
|
: `Marked for summarizer emphasis — brewcode compact handoff, phase ${task.currentPhase}/${task.totalPhases}. Note: lock file session binding active.`;
|
|
|
|
// Return continue to allow compact
|
|
// systemMessage = short status for user
|
|
// session-start.mjs (source='compact') handles Claude re-read instruction via additionalContext
|
|
output({
|
|
continue: true,
|
|
systemMessage
|
|
});
|
|
} catch (error) {
|
|
log('error', '[pre-compact]', `Error: ${error.message}`, cwd, session_id);
|
|
// On error, still allow compact (don't crash session)
|
|
output({ continue: true });
|
|
}
|
|
}
|
|
|
|
main();
|