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