tractatus/.claude/hooks/plan-persistence-checker.js
TheFlow 92f70d7892 feat: Add plan persistence hooks for Claude Code
- Add plan-detection-hook.js: Detects planning discussions in user prompts
- Add plan-persistence-checker.js: Validates plan documentation on response
- Add plan-persistence-manager.js: Manages plan tracking state

Implements architectural enforcement for plan persistence (inst_comm_plan_001).
Prevents accidental loss of planning discussions in Claude Code sessions.

Incident Reference: 2025-12-01 lost plan documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 13:46:01 +13:00

155 lines
4 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* Plan Persistence Checker Hook (PreToolUse)
*
* Runs before tool execution to:
* 1. Check for unpersisted plans and remind Claude
* 2. Detect when plans are being written and mark them as persisted
*
* Part of the enforcement mechanism for inst_comm_plan_001.
*
* INCIDENT REFERENCE: 2025-12-01 lost plan documentation
*
* Hook Input (JSON via stdin):
* {
* "session_id": "abc123",
* "hook_event_name": "PreToolUse",
* "tool_name": "Write",
* "tool_input": { "file_path": "/path/to/file" }
* }
*/
const path = require('path');
const fs = require('fs');
// Import the plan persistence manager
const planManager = require('./plan-persistence-manager.js');
/**
* Read JSON input from stdin
*/
function readStdin() {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.on('data', chunk => { data += chunk; });
process.stdin.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(new Error('Invalid JSON input'));
}
});
process.stdin.on('error', reject);
});
}
/**
* Output hook response
*/
function outputResponse(decision, reason, systemMessage = null) {
const response = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: decision,
permissionDecisionReason: reason
},
continue: true,
suppressOutput: false
};
if (systemMessage) {
response.systemMessage = systemMessage;
}
console.log(JSON.stringify(response));
}
/**
* Main hook logic
*/
async function main() {
let input;
try {
input = await readStdin();
} catch (err) {
outputResponse('allow', 'Invalid hook input');
process.exit(0);
}
const { session_id, tool_name, tool_input } = input;
// Only check Write and Edit tools
if (!['Write', 'Edit'].includes(tool_name)) {
outputResponse('allow', 'Non-file tool, no plan check needed');
process.exit(0);
}
const filePath = tool_input.file_path || tool_input.path || '';
// Check if this is writing to docs/plans/ (plan being persisted)
if (filePath.includes('docs/plans/') && filePath.endsWith('.md')) {
const persistenceResult = planManager.checkPlanPersistence(filePath, session_id);
if (persistenceResult.markedAsPersisted) {
outputResponse(
'allow',
'Plan persistence detected',
`✅ PLAN PERSISTENCE: ${persistenceResult.message}\n\n` +
`${persistenceResult.plansUpdated} plan(s) marked as persisted. Good work following inst_comm_plan_001.`
);
process.exit(0);
}
// New plan file being created
outputResponse(
'allow',
'Writing new plan file',
`📄 Creating plan file: ${path.basename(filePath)}\n` +
`This will be tracked as a persisted plan.`
);
process.exit(0);
}
// Check for unpersisted plans
const unpersisted = planManager.checkUnpersistedPlans(session_id);
if (unpersisted.hasUnpersistedPlans) {
// Get stats for context
const stats = planManager.getSessionStats(session_id);
if (unpersisted.hasHighPriority) {
// HIGH PRIORITY: User explicitly requested plan documentation
// Don't block, but inject a strong reminder
outputResponse(
'allow',
'Unpersisted high-priority plan detected',
`🚨 CRITICAL REMINDER (inst_comm_plan_001)\n\n` +
`${unpersisted.warningMessage}\n\n` +
`Session Stats: ${stats.persisted}/${stats.totalPlans} plans persisted\n` +
`\n📊 This check runs on every file operation to ensure plans are not lost.`
);
} else if (unpersisted.plans.length > 0) {
// Regular unpersisted plans - gentle reminder
outputResponse(
'allow',
'Unpersisted plans detected',
unpersisted.warningMessage
);
} else {
outputResponse('allow', 'No plan concerns');
}
} else {
outputResponse('allow', 'No unpersisted plans');
}
process.exit(0);
}
// Run hook
main().catch(err => {
// Don't block on errors
outputResponse('allow', `Hook error: ${err.message}`);
process.exit(0);
});