- 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>
155 lines
4 KiB
JavaScript
Executable file
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);
|
|
});
|