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>
This commit is contained in:
parent
9bce9081c1
commit
92f70d7892
3 changed files with 755 additions and 0 deletions
172
.claude/hooks/plan-detection-hook.js
Executable file
172
.claude/hooks/plan-detection-hook.js
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Plan Detection Hook (UserPromptSubmit)
|
||||
*
|
||||
* Detects planning discussions in user prompts and tracks them.
|
||||
* Part of the enforcement mechanism for inst_comm_plan_001.
|
||||
*
|
||||
* INCIDENT REFERENCE: 2025-12-01 lost plan documentation
|
||||
*
|
||||
* When a user initiates a planning discussion:
|
||||
* 1. Detect planning keywords
|
||||
* 2. Track the plan as in-progress
|
||||
* 3. Inject a reminder to Claude about persisting plans
|
||||
*
|
||||
* Hook Input (JSON via stdin):
|
||||
* {
|
||||
* "session_id": "abc123",
|
||||
* "hook_event_name": "UserPromptSubmit",
|
||||
* "user_message": "Let's create a plan for the landing page redesign"
|
||||
* }
|
||||
*
|
||||
* Hook Output (JSON to stdout):
|
||||
* {
|
||||
* "hookSpecificOutput": { ... },
|
||||
* "systemMessage": "Planning discussion detected...",
|
||||
* "continue": true
|
||||
* }
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// 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(analysis, systemMessage = null) {
|
||||
const response = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'UserPromptSubmit',
|
||||
planDetection: analysis
|
||||
},
|
||||
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) {
|
||||
// Invalid input, allow execution
|
||||
outputResponse({ error: 'Invalid hook input' });
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { session_id, user_message } = input;
|
||||
|
||||
if (!user_message) {
|
||||
outputResponse({ detected: false });
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Clean up old plans periodically
|
||||
planManager.cleanupOldPlans();
|
||||
|
||||
// Detect if this is a planning discussion
|
||||
const detection = planManager.detectPlanningDiscussion(user_message);
|
||||
|
||||
if (!detection.isPlanningDiscussion) {
|
||||
// Not a planning discussion, check for unpersisted plans
|
||||
const unpersisted = planManager.checkUnpersistedPlans(session_id);
|
||||
|
||||
if (unpersisted.hasUnpersistedPlans && unpersisted.hasHighPriority) {
|
||||
// High priority unpersisted plans - CRITICAL warning
|
||||
outputResponse(
|
||||
{ detected: false, unpersistedCheck: unpersisted },
|
||||
unpersisted.warningMessage
|
||||
);
|
||||
} else {
|
||||
outputResponse({ detected: false });
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Track the plan in progress
|
||||
const trackedPlan = planManager.trackPlanInProgress(
|
||||
session_id,
|
||||
detection.topic,
|
||||
detection.confidence,
|
||||
detection.isHighPriority
|
||||
);
|
||||
|
||||
// Build system message
|
||||
let systemMessage;
|
||||
|
||||
if (detection.isHighPriority) {
|
||||
systemMessage =
|
||||
`🚨 PLAN PERSISTENCE REQUIRED (inst_comm_plan_001)\n\n` +
|
||||
`User has EXPLICITLY requested plan documentation.\n` +
|
||||
`Topic: "${detection.topic}"\n\n` +
|
||||
`BEFORE this session ends, you MUST:\n` +
|
||||
`1. Create a comprehensive plan document\n` +
|
||||
`2. Save it to: docs/plans/PLAN_[TOPIC]_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.md\n` +
|
||||
`3. This is MANDATORY - do NOT rely on internal memory\n\n` +
|
||||
`⚠️ INCIDENT 2025-12-01: User's plan was lost because Claude kept it in memory. Never again.`;
|
||||
} else if (detection.confidence >= 0.7) {
|
||||
systemMessage =
|
||||
`📋 Planning Discussion Detected (${Math.round(detection.confidence * 100)}% confidence)\n\n` +
|
||||
`Topic: "${detection.topic}"\n\n` +
|
||||
`Per inst_comm_plan_001: If this discussion produces a significant plan, ` +
|
||||
`save it to docs/plans/ directory before session ends.\n\n` +
|
||||
`Recommended file: docs/plans/PLAN_[TOPIC]_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.md`;
|
||||
} else {
|
||||
// Lower confidence - just note it
|
||||
systemMessage =
|
||||
`📝 Possible planning discussion detected.\n` +
|
||||
`If significant plans emerge, save to docs/plans/.`;
|
||||
}
|
||||
|
||||
outputResponse({
|
||||
detected: true,
|
||||
topic: detection.topic,
|
||||
confidence: detection.confidence,
|
||||
isHighPriority: detection.isHighPriority,
|
||||
tracked: true
|
||||
}, systemMessage);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run hook
|
||||
main().catch(err => {
|
||||
console.error(JSON.stringify({
|
||||
hookSpecificOutput: { error: err.message },
|
||||
continue: true,
|
||||
suppressOutput: false
|
||||
}));
|
||||
process.exit(0);
|
||||
});
|
||||
155
.claude/hooks/plan-persistence-checker.js
Executable file
155
.claude/hooks/plan-persistence-checker.js
Executable file
|
|
@ -0,0 +1,155 @@
|
|||
#!/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);
|
||||
});
|
||||
428
.claude/hooks/plan-persistence-manager.js
Executable file
428
.claude/hooks/plan-persistence-manager.js
Executable file
|
|
@ -0,0 +1,428 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Plan Persistence Manager
|
||||
*
|
||||
* Tracks planning discussions and ensures plans are persisted to MD files.
|
||||
* Part of the enforcement mechanism for inst_comm_plan_001.
|
||||
*
|
||||
* INCIDENT REFERENCE: 2025-12-01 lost plan documentation
|
||||
*
|
||||
* Three functions:
|
||||
* 1. detectPlanningDiscussion(userMessage) - Detect if user is starting a plan
|
||||
* 2. trackPlanInProgress(sessionId, planTopic) - Mark a plan as in progress
|
||||
* 3. checkUnpersistedPlans(sessionId) - Check for plans not yet saved to MD
|
||||
* 4. markPlanPersisted(sessionId, planTopic, filePath) - Mark plan as saved
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Session state file for tracking plans
|
||||
const SESSION_STATE_DIR = path.join(__dirname, '..', 'session-state');
|
||||
const PLANS_STATE_FILE = path.join(SESSION_STATE_DIR, 'plans-in-progress.json');
|
||||
|
||||
// Planning keywords that indicate a planning discussion
|
||||
const PLANNING_KEYWORDS = [
|
||||
// Direct plan requests
|
||||
'create a plan',
|
||||
'make a plan',
|
||||
'write a plan',
|
||||
'document the plan',
|
||||
'save the plan',
|
||||
'outline the plan',
|
||||
'plan for',
|
||||
'planning',
|
||||
|
||||
// Strategy discussions
|
||||
'strategy',
|
||||
'roadmap',
|
||||
'approach',
|
||||
'how should we',
|
||||
'let\'s discuss',
|
||||
'let\'s figure out',
|
||||
'design the',
|
||||
'architect',
|
||||
|
||||
// Implementation discussions
|
||||
'implement',
|
||||
'implementation plan',
|
||||
'step by step',
|
||||
'phases',
|
||||
'milestones',
|
||||
|
||||
// Feature discussions
|
||||
'redesign',
|
||||
'new feature',
|
||||
'feature request',
|
||||
'requirements',
|
||||
'spec',
|
||||
'specification',
|
||||
|
||||
// Decision discussions
|
||||
'decision',
|
||||
'decide',
|
||||
'trade-offs',
|
||||
'options',
|
||||
'alternatives',
|
||||
'pros and cons',
|
||||
|
||||
// Explicit requests to document
|
||||
'document this',
|
||||
'write this up',
|
||||
'save this',
|
||||
'record this',
|
||||
'keep a record',
|
||||
'md file',
|
||||
'markdown file'
|
||||
];
|
||||
|
||||
// High-priority keywords that ALWAYS trigger plan tracking
|
||||
const HIGH_PRIORITY_KEYWORDS = [
|
||||
'document the plan',
|
||||
'save the plan',
|
||||
'write a plan',
|
||||
'md file',
|
||||
'markdown file',
|
||||
'keep a record',
|
||||
'document this discussion'
|
||||
];
|
||||
|
||||
/**
|
||||
* Ensure session state directory exists
|
||||
*/
|
||||
function ensureStateDir() {
|
||||
if (!fs.existsSync(SESSION_STATE_DIR)) {
|
||||
fs.mkdirSync(SESSION_STATE_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current plans state
|
||||
*/
|
||||
function loadPlansState() {
|
||||
ensureStateDir();
|
||||
|
||||
if (fs.existsSync(PLANS_STATE_FILE)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(PLANS_STATE_FILE, 'utf-8'));
|
||||
} catch {
|
||||
return { plans: [], lastUpdated: null };
|
||||
}
|
||||
}
|
||||
|
||||
return { plans: [], lastUpdated: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Save plans state
|
||||
*/
|
||||
function savePlansState(state) {
|
||||
ensureStateDir();
|
||||
state.lastUpdated = new Date().toISOString();
|
||||
fs.writeFileSync(PLANS_STATE_FILE, JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a message indicates a planning discussion
|
||||
*
|
||||
* @param {string} userMessage - The user's message
|
||||
* @returns {Object} - { isPlanningDiscussion, confidence, topic, isHighPriority }
|
||||
*/
|
||||
function detectPlanningDiscussion(userMessage) {
|
||||
if (!userMessage || typeof userMessage !== 'string') {
|
||||
return { isPlanningDiscussion: false, confidence: 0, topic: null, isHighPriority: false };
|
||||
}
|
||||
|
||||
const lowerMessage = userMessage.toLowerCase();
|
||||
|
||||
// Check high-priority keywords first
|
||||
const isHighPriority = HIGH_PRIORITY_KEYWORDS.some(keyword =>
|
||||
lowerMessage.includes(keyword)
|
||||
);
|
||||
|
||||
if (isHighPriority) {
|
||||
// Extract topic from message (first 50 chars after keyword)
|
||||
const topic = extractTopic(userMessage);
|
||||
return {
|
||||
isPlanningDiscussion: true,
|
||||
confidence: 0.95,
|
||||
topic,
|
||||
isHighPriority: true
|
||||
};
|
||||
}
|
||||
|
||||
// Check regular planning keywords
|
||||
const matchedKeywords = PLANNING_KEYWORDS.filter(keyword =>
|
||||
lowerMessage.includes(keyword)
|
||||
);
|
||||
|
||||
if (matchedKeywords.length >= 2) {
|
||||
// Multiple keyword matches = high confidence
|
||||
return {
|
||||
isPlanningDiscussion: true,
|
||||
confidence: 0.85,
|
||||
topic: extractTopic(userMessage),
|
||||
isHighPriority: false
|
||||
};
|
||||
}
|
||||
|
||||
if (matchedKeywords.length === 1) {
|
||||
// Single keyword match = medium confidence
|
||||
return {
|
||||
isPlanningDiscussion: true,
|
||||
confidence: 0.6,
|
||||
topic: extractTopic(userMessage),
|
||||
isHighPriority: false
|
||||
};
|
||||
}
|
||||
|
||||
return { isPlanningDiscussion: false, confidence: 0, topic: null, isHighPriority: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a topic from a message
|
||||
*/
|
||||
function extractTopic(message) {
|
||||
// Take first 100 chars, remove special characters, truncate to first sentence
|
||||
let topic = message.substring(0, 100)
|
||||
.replace(/[^\w\s-]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Find first sentence boundary
|
||||
const sentenceEnd = topic.search(/[.!?]/);
|
||||
if (sentenceEnd > 10) {
|
||||
topic = topic.substring(0, sentenceEnd);
|
||||
}
|
||||
|
||||
return topic || 'Unnamed Plan';
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a plan as in progress
|
||||
*
|
||||
* @param {string} sessionId - Current session ID
|
||||
* @param {string} topic - Plan topic
|
||||
* @param {number} confidence - Detection confidence
|
||||
* @param {boolean} isHighPriority - Whether this is a high-priority plan request
|
||||
*/
|
||||
function trackPlanInProgress(sessionId, topic, confidence = 0.8, isHighPriority = false) {
|
||||
const state = loadPlansState();
|
||||
|
||||
// Check if plan already tracked
|
||||
const existingPlan = state.plans.find(p =>
|
||||
p.sessionId === sessionId && p.topic === topic
|
||||
);
|
||||
|
||||
if (existingPlan) {
|
||||
// Update existing plan
|
||||
existingPlan.lastMentioned = new Date().toISOString();
|
||||
existingPlan.mentionCount = (existingPlan.mentionCount || 1) + 1;
|
||||
if (isHighPriority && !existingPlan.isHighPriority) {
|
||||
existingPlan.isHighPriority = true;
|
||||
}
|
||||
} else {
|
||||
// Add new plan
|
||||
state.plans.push({
|
||||
sessionId,
|
||||
topic,
|
||||
confidence,
|
||||
isHighPriority,
|
||||
startedAt: new Date().toISOString(),
|
||||
lastMentioned: new Date().toISOString(),
|
||||
mentionCount: 1,
|
||||
persisted: false,
|
||||
persistedTo: null
|
||||
});
|
||||
}
|
||||
|
||||
savePlansState(state);
|
||||
|
||||
return state.plans.find(p => p.sessionId === sessionId && p.topic === topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for unpersisted plans in current session
|
||||
*
|
||||
* @param {string} sessionId - Current session ID
|
||||
* @returns {Object} - { hasUnpersistedPlans, plans, warningMessage }
|
||||
*/
|
||||
function checkUnpersistedPlans(sessionId) {
|
||||
const state = loadPlansState();
|
||||
|
||||
const unpersistedPlans = state.plans.filter(p =>
|
||||
p.sessionId === sessionId && !p.persisted
|
||||
);
|
||||
|
||||
if (unpersistedPlans.length === 0) {
|
||||
return { hasUnpersistedPlans: false, plans: [], warningMessage: null };
|
||||
}
|
||||
|
||||
// Check if any are high priority
|
||||
const highPriorityPlans = unpersistedPlans.filter(p => p.isHighPriority);
|
||||
|
||||
let warningMessage;
|
||||
|
||||
if (highPriorityPlans.length > 0) {
|
||||
warningMessage = `⚠️ CRITICAL: User explicitly requested plan documentation that has NOT been saved:\n` +
|
||||
highPriorityPlans.map(p => ` • "${p.topic}" (requested ${p.mentionCount} times)`).join('\n') +
|
||||
`\n\n🚨 INSTRUCTION inst_comm_plan_001 REQUIRES: Save plans to docs/plans/PLAN_[TOPIC]_[YYYYMMDD].md IMMEDIATELY.` +
|
||||
`\n DO NOT proceed with other work until this is done.`;
|
||||
} else {
|
||||
warningMessage = `📋 REMINDER: ${unpersistedPlans.length} plan(s) in progress may need to be saved:\n` +
|
||||
unpersistedPlans.map(p => ` • "${p.topic}"`).join('\n') +
|
||||
`\n\n Per inst_comm_plan_001: Save significant plans to docs/plans/ directory.`;
|
||||
}
|
||||
|
||||
return {
|
||||
hasUnpersistedPlans: true,
|
||||
plans: unpersistedPlans,
|
||||
warningMessage,
|
||||
hasHighPriority: highPriorityPlans.length > 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a plan as persisted
|
||||
*
|
||||
* @param {string} sessionId - Current session ID
|
||||
* @param {string} topic - Plan topic (partial match supported)
|
||||
* @param {string} filePath - Path where plan was saved
|
||||
*/
|
||||
function markPlanPersisted(sessionId, topic, filePath) {
|
||||
const state = loadPlansState();
|
||||
|
||||
// Find matching plan (partial topic match)
|
||||
const plan = state.plans.find(p =>
|
||||
p.sessionId === sessionId &&
|
||||
!p.persisted &&
|
||||
(p.topic.toLowerCase().includes(topic.toLowerCase()) ||
|
||||
topic.toLowerCase().includes(p.topic.toLowerCase()))
|
||||
);
|
||||
|
||||
if (plan) {
|
||||
plan.persisted = true;
|
||||
plan.persistedTo = filePath;
|
||||
plan.persistedAt = new Date().toISOString();
|
||||
savePlansState(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file write is a plan persistence
|
||||
*
|
||||
* @param {string} filePath - Path being written
|
||||
* @param {string} sessionId - Current session ID
|
||||
*/
|
||||
function checkPlanPersistence(filePath, sessionId) {
|
||||
// Check if writing to docs/plans/
|
||||
if (filePath && filePath.includes('docs/plans/') && filePath.endsWith('.md')) {
|
||||
const state = loadPlansState();
|
||||
|
||||
// Find any unpersisted plans for this session
|
||||
const unpersistedPlans = state.plans.filter(p =>
|
||||
p.sessionId === sessionId && !p.persisted
|
||||
);
|
||||
|
||||
// Mark the most recent/relevant one as persisted
|
||||
if (unpersistedPlans.length > 0) {
|
||||
// Mark all unpersisted plans as potentially persisted by this file
|
||||
unpersistedPlans.forEach(plan => {
|
||||
plan.persisted = true;
|
||||
plan.persistedTo = filePath;
|
||||
plan.persistedAt = new Date().toISOString();
|
||||
});
|
||||
|
||||
savePlansState(state);
|
||||
|
||||
return {
|
||||
markedAsPersisted: true,
|
||||
plansUpdated: unpersistedPlans.length,
|
||||
message: `✅ Plan(s) marked as persisted to: ${filePath}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { markedAsPersisted: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session statistics
|
||||
*/
|
||||
function getSessionStats(sessionId) {
|
||||
const state = loadPlansState();
|
||||
|
||||
const sessionPlans = state.plans.filter(p => p.sessionId === sessionId);
|
||||
const persisted = sessionPlans.filter(p => p.persisted);
|
||||
const unpersisted = sessionPlans.filter(p => !p.persisted);
|
||||
|
||||
return {
|
||||
totalPlans: sessionPlans.length,
|
||||
persisted: persisted.length,
|
||||
unpersisted: unpersisted.length,
|
||||
plans: sessionPlans
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old session plans (older than 24 hours)
|
||||
*/
|
||||
function cleanupOldPlans() {
|
||||
const state = loadPlansState();
|
||||
const cutoff = Date.now() - (24 * 60 * 60 * 1000); // 24 hours
|
||||
|
||||
state.plans = state.plans.filter(p => {
|
||||
const planDate = new Date(p.startedAt).getTime();
|
||||
return planDate > cutoff;
|
||||
});
|
||||
|
||||
savePlansState(state);
|
||||
}
|
||||
|
||||
// Export functions
|
||||
module.exports = {
|
||||
detectPlanningDiscussion,
|
||||
trackPlanInProgress,
|
||||
checkUnpersistedPlans,
|
||||
markPlanPersisted,
|
||||
checkPlanPersistence,
|
||||
getSessionStats,
|
||||
cleanupOldPlans,
|
||||
PLANNING_KEYWORDS,
|
||||
HIGH_PRIORITY_KEYWORDS
|
||||
};
|
||||
|
||||
// CLI mode for testing
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
switch (command) {
|
||||
case 'detect':
|
||||
const message = args.slice(1).join(' ');
|
||||
console.log(JSON.stringify(detectPlanningDiscussion(message), null, 2));
|
||||
break;
|
||||
|
||||
case 'check':
|
||||
const sessionId = args[1] || 'test-session';
|
||||
console.log(JSON.stringify(checkUnpersistedPlans(sessionId), null, 2));
|
||||
break;
|
||||
|
||||
case 'stats':
|
||||
const statsSessionId = args[1] || 'test-session';
|
||||
console.log(JSON.stringify(getSessionStats(statsSessionId), null, 2));
|
||||
break;
|
||||
|
||||
case 'cleanup':
|
||||
cleanupOldPlans();
|
||||
console.log('Old plans cleaned up');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Usage: node plan-persistence-manager.js <detect|check|stats|cleanup> [args]');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue