From 3745a13eaf11b43251d044ced8162767c27529f5 Mon Sep 17 00:00:00 2001 From: Christian Gick Date: Mon, 19 Jan 2026 10:13:57 +0200 Subject: [PATCH] Add session documentation system (migration + MCP tools) Phase 1-2 complete: Database schema + 9 MCP tools for session docs Database Changes (migration 016): - session_notes table (accomplishments, decisions, gotchas, etc.) - session_plans table (plan mode plans with lifecycle tracking) - project_documentation table (persistent project docs) - sessions.documentation column (auto-generated markdown) - HNSW indexes for semantic search across all doc types MCP Tools Added (session-docs.ts): 1. session_note_add - Add structured notes to session 2. session_notes_list - List notes by type 3. session_plan_save - Save plan with embedding 4. session_plan_update_status - Track plan lifecycle 5. session_plan_list - List session plans 6. project_doc_upsert - Create/update project docs 7. project_doc_get - Get specific doc by type 8. project_doc_list - List all project docs 9. session_documentation_generate - Auto-generate markdown Replaces: CLAUDE.md files, ~/.claude/plans/ directory Next: Update session-start/end scripts for temp file management Co-Authored-By: Claude Sonnet 4.5 --- migrations/016_session_documentation.sql | 211 +++++++++++ regenerate_tool_docs_embeddings.mjs | 90 +++++ src/index.ts | 87 +++++ src/tools/index.ts | 114 ++++++ src/tools/session-docs.ts | 424 +++++++++++++++++++++++ 5 files changed, 926 insertions(+) create mode 100644 migrations/016_session_documentation.sql create mode 100644 regenerate_tool_docs_embeddings.mjs create mode 100644 src/tools/session-docs.ts diff --git a/migrations/016_session_documentation.sql b/migrations/016_session_documentation.sql new file mode 100644 index 0000000..41ecfe6 --- /dev/null +++ b/migrations/016_session_documentation.sql @@ -0,0 +1,211 @@ +-- Migration 016: Session Documentation System +-- Purpose: Migrate from file-based documentation (CLAUDE.md, plan files) to database +-- Dependencies: 010_sessions.sql (sessions table), 001_base_schema.sql (pgvector) + +-- ============================================================================ +-- SESSION NOTES: Structured notes within sessions +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS session_notes ( + id SERIAL PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + note_type TEXT NOT NULL CHECK (note_type IN ( + 'accomplishment', + 'decision', + 'gotcha', + 'next_steps', + 'context' + )), + content TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + embedding vector(1024) -- For semantic search of notes +); + +-- Indexes for efficient querying +CREATE INDEX idx_session_notes_session ON session_notes(session_id); +CREATE INDEX idx_session_notes_type ON session_notes(note_type); +CREATE INDEX idx_session_notes_created ON session_notes(created_at DESC); + +-- HNSW index for semantic similarity search +CREATE INDEX idx_session_notes_embedding ON session_notes + USING hnsw (embedding vector_cosine_ops); + +COMMENT ON TABLE session_notes IS 'Structured notes within sessions (replaces ad-hoc note files)'; +COMMENT ON COLUMN session_notes.note_type IS 'Category of note: accomplishment, decision, gotcha, next_steps, context'; +COMMENT ON COLUMN session_notes.embedding IS 'Vector embedding for semantic search across notes'; + +-- ============================================================================ +-- SESSION PLANS: Plan mode plans with lifecycle tracking +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS session_plans ( + id SERIAL PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + plan_file_name TEXT, -- Original filename (e.g., "transient-forging-reddy.md") + plan_content TEXT NOT NULL, + status TEXT DEFAULT 'draft' CHECK (status IN ( + 'draft', + 'approved', + 'executed', + 'abandoned' + )), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + approved_at TIMESTAMP WITH TIME ZONE, + completed_at TIMESTAMP WITH TIME ZONE, + embedding vector(1024) -- For semantic search of plans +); + +-- Indexes for efficient querying +CREATE INDEX idx_session_plans_session ON session_plans(session_id); +CREATE INDEX idx_session_plans_status ON session_plans(status); +CREATE INDEX idx_session_plans_created ON session_plans(created_at DESC); + +-- HNSW index for semantic similarity search +CREATE INDEX idx_session_plans_embedding ON session_plans + USING hnsw (embedding vector_cosine_ops); + +COMMENT ON TABLE session_plans IS 'Plan mode plans with lifecycle tracking (replaces ~/.claude/plans/)'; +COMMENT ON COLUMN session_plans.plan_file_name IS 'Original filename from plan mode (e.g., eloquent-yellow-cat.md)'; +COMMENT ON COLUMN session_plans.status IS 'Plan lifecycle: draft → approved → executed/abandoned'; +COMMENT ON COLUMN session_plans.embedding IS 'Vector embedding for semantic search across plans'; + +-- ============================================================================ +-- PROJECT DOCUMENTATION: Persistent project-level docs +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS project_documentation ( + id SERIAL PRIMARY KEY, + project TEXT NOT NULL REFERENCES projects(key) ON DELETE CASCADE, + doc_type TEXT NOT NULL CHECK (doc_type IN ( + 'overview', + 'architecture', + 'guidelines', + 'history' + )), + title TEXT NOT NULL, + content TEXT NOT NULL, + last_updated_session TEXT REFERENCES sessions(id) ON DELETE SET NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(project, doc_type) -- One doc of each type per project +); + +-- Indexes for efficient querying +CREATE INDEX idx_project_docs_project ON project_documentation(project); +CREATE INDEX idx_project_docs_type ON project_documentation(doc_type); +CREATE INDEX idx_project_docs_updated ON project_documentation(updated_at DESC); + +COMMENT ON TABLE project_documentation IS 'Persistent project documentation (replaces CLAUDE.md sections)'; +COMMENT ON COLUMN project_documentation.doc_type IS 'Type of documentation: overview, architecture, guidelines, history'; +COMMENT ON COLUMN project_documentation.last_updated_session IS 'Session that last updated this documentation'; + +-- ============================================================================ +-- SESSIONS TABLE ENHANCEMENTS +-- ============================================================================ + +-- Add documentation column for full markdown documentation +ALTER TABLE sessions + ADD COLUMN IF NOT EXISTS documentation TEXT; + +-- Add migration flag to track CLAUDE.md migration status +ALTER TABLE sessions + ADD COLUMN IF NOT EXISTS claude_md_migrated BOOLEAN DEFAULT FALSE; + +COMMENT ON COLUMN sessions.documentation IS 'Full markdown documentation for session (auto-generated at end)'; +COMMENT ON COLUMN sessions.claude_md_migrated IS 'Flag indicating this session migrated from CLAUDE.md file'; + +-- ============================================================================ +-- HELPER VIEWS +-- ============================================================================ + +-- View: Session with note counts +CREATE OR REPLACE VIEW session_documentation_summary AS +SELECT + s.id, + s.project, + s.session_number, + s.started_at, + s.ended_at, + s.status, + s.summary, + COUNT(DISTINCT sn.id) as note_count, + COUNT(DISTINCT sp.id) as plan_count, + ARRAY_AGG(DISTINCT sn.note_type) FILTER (WHERE sn.note_type IS NOT NULL) as note_types, + MAX(sn.created_at) as last_note_at, + MAX(sp.created_at) as last_plan_at +FROM sessions s +LEFT JOIN session_notes sn ON s.id = sn.session_id +LEFT JOIN session_plans sp ON s.id = sp.session_id +GROUP BY s.id, s.project, s.session_number, s.started_at, s.ended_at, s.status, s.summary; + +COMMENT ON VIEW session_documentation_summary IS 'Sessions with note and plan counts for quick overview'; + +-- View: Latest project documentation +CREATE OR REPLACE VIEW latest_project_docs AS +SELECT + pd.project, + pd.doc_type, + pd.title, + LEFT(pd.content, 200) as content_preview, + pd.updated_at, + s.session_number as last_updated_session_number, + s.started_at as last_updated_session_date +FROM project_documentation pd +LEFT JOIN sessions s ON pd.last_updated_session = s.id +ORDER BY pd.project, pd.doc_type; + +COMMENT ON VIEW latest_project_docs IS 'Latest project documentation with session context'; + +-- ============================================================================ +-- MAINTENANCE FUNCTIONS +-- ============================================================================ + +-- Function to clean up old session notes (optional retention policy) +CREATE OR REPLACE FUNCTION cleanup_old_session_notes(retention_days INTEGER DEFAULT 180) +RETURNS INTEGER AS $$ +DECLARE + deleted_count INTEGER; +BEGIN + DELETE FROM session_notes + WHERE created_at < NOW() - (retention_days || ' days')::INTERVAL + AND session_id IN ( + SELECT id FROM sessions WHERE status = 'completed' + ); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + RETURN deleted_count; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION cleanup_old_session_notes IS 'Clean up notes from completed sessions older than retention period (default 180 days)'; + +-- Function to generate session documentation stats +CREATE OR REPLACE FUNCTION session_documentation_stats(p_project TEXT DEFAULT NULL) +RETURNS TABLE( + total_sessions BIGINT, + sessions_with_notes BIGINT, + sessions_with_plans BIGINT, + total_notes BIGINT, + total_plans BIGINT, + avg_notes_per_session NUMERIC, + avg_plans_per_session NUMERIC +) AS $$ +BEGIN + RETURN QUERY + SELECT + COUNT(DISTINCT s.id) as total_sessions, + COUNT(DISTINCT CASE WHEN sn.id IS NOT NULL THEN s.id END) as sessions_with_notes, + COUNT(DISTINCT CASE WHEN sp.id IS NOT NULL THEN s.id END) as sessions_with_plans, + COUNT(sn.id) as total_notes, + COUNT(sp.id) as total_plans, + ROUND(COUNT(sn.id)::NUMERIC / NULLIF(COUNT(DISTINCT s.id), 0), 2) as avg_notes_per_session, + ROUND(COUNT(sp.id)::NUMERIC / NULLIF(COUNT(DISTINCT s.id), 0), 2) as avg_plans_per_session + FROM sessions s + LEFT JOIN session_notes sn ON s.id = sn.session_id + LEFT JOIN session_plans sp ON s.id = sp.session_id + WHERE (p_project IS NULL OR s.project = p_project) + AND s.status = 'completed'; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION session_documentation_stats IS 'Generate statistics about session documentation usage'; diff --git a/regenerate_tool_docs_embeddings.mjs b/regenerate_tool_docs_embeddings.mjs new file mode 100644 index 0000000..94e64b4 --- /dev/null +++ b/regenerate_tool_docs_embeddings.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +import pg from 'pg'; +const { Pool } = pg; + +// Database configuration +const pool = new Pool({ + host: 'infra.agiliton.internal', + port: 5432, + database: 'agiliton', + user: 'agiliton', + password: 'QtqiwCOAUpQNF6pjzOMAREzUny2bY8V1', + max: 5, +}); + +// LiteLLM API configuration +const LLM_API_URL = 'https://api.agiliton.cloud/llm'; +const LLM_API_KEY = 'sk-litellm-master-key'; + +async function getEmbedding(text) { + try { + const response = await fetch(`${LLM_API_URL}/v1/embeddings`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${LLM_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'mxbai-embed-large', + input: text, + }), + }); + + if (!response.ok) { + console.error('Embedding API error:', response.status, await response.text()); + return null; + } + + const data = await response.json(); + return data.data?.[0]?.embedding || null; + } catch (error) { + console.error('Embedding generation failed:', error); + return null; + } +} + +async function regenerateEmbeddings() { + try { + // Fetch all tool_docs + const result = await pool.query( + 'SELECT id, tool_name, title, description, notes, tags FROM tool_docs WHERE embedding IS NULL' + ); + + console.log(`Found ${result.rows.length} tool_docs without embeddings`); + + let successCount = 0; + let failCount = 0; + + for (const row of result.rows) { + const embedText = `${row.title}. ${row.description}. ${row.tags?.join(' ') || ''}. ${row.notes || ''}`; + + console.log(`Generating embedding for: ${row.tool_name}`); + const embedding = await getEmbedding(embedText); + + if (embedding) { + const embeddingStr = `[${embedding.join(',')}]`; + await pool.query( + 'UPDATE tool_docs SET embedding = $1 WHERE id = $2', + [embeddingStr, row.id] + ); + successCount++; + console.log(` ✓ ${row.tool_name} (${successCount}/${result.rows.length})`); + } else { + failCount++; + console.log(` ✗ ${row.tool_name} - Failed to generate embedding`); + } + + // Small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)); + } + + console.log(`\n✅ Complete: ${successCount} successful, ${failCount} failed`); + } catch (error) { + console.error('Error:', error); + } finally { + await pool.end(); + } +} + +regenerateEmbeddings(); diff --git a/src/index.ts b/src/index.ts index e288adb..0c2a7ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,17 @@ import { buildRecord, sessionCommitLink, } from './tools/sessions.js'; +import { + sessionNoteAdd, + sessionNotesList, + sessionPlanSave, + sessionPlanUpdateStatus, + sessionPlanList, + projectDocUpsert, + projectDocGet, + projectDocList, + sessionDocumentationGenerate, +} from './tools/session-docs.js'; // Create MCP server const server = new Server( @@ -476,6 +487,82 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ); break; + // Session Documentation + case 'session_note_add': + result = await sessionNoteAdd({ + session_id: a.session_id, + note_type: a.note_type, + content: a.content, + }); + break; + case 'session_notes_list': + result = JSON.stringify( + await sessionNotesList({ + session_id: a.session_id, + note_type: a.note_type, + }), + null, + 2 + ); + break; + case 'session_plan_save': + result = await sessionPlanSave({ + session_id: a.session_id, + plan_content: a.plan_content, + plan_file_name: a.plan_file_name, + status: a.status, + }); + break; + case 'session_plan_update_status': + result = await sessionPlanUpdateStatus({ + plan_id: a.plan_id, + status: a.status, + }); + break; + case 'session_plan_list': + result = JSON.stringify( + await sessionPlanList({ + session_id: a.session_id, + status: a.status, + }), + null, + 2 + ); + break; + case 'project_doc_upsert': + result = await projectDocUpsert({ + project: a.project, + doc_type: a.doc_type, + title: a.title, + content: a.content, + session_id: a.session_id, + }); + break; + case 'project_doc_get': + result = JSON.stringify( + await projectDocGet({ + project: a.project, + doc_type: a.doc_type, + }), + null, + 2 + ); + break; + case 'project_doc_list': + result = JSON.stringify( + await projectDocList({ + project: a.project, + }), + null, + 2 + ); + break; + case 'session_documentation_generate': + result = await sessionDocumentationGenerate({ + session_id: a.session_id, + }); + break; + default: throw new Error(`Unknown tool: ${name}`); } diff --git a/src/tools/index.ts b/src/tools/index.ts index a0b7867..6407e67 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -825,4 +825,118 @@ export const toolDefinitions = [ required: ['session_id', 'commit_sha', 'repo'], }, }, + + // Session Documentation Tools + { + name: 'session_note_add', + description: 'Add a note to current session with auto-generated embedding', + inputSchema: { + type: 'object', + properties: { + session_id: { type: 'string', description: 'Session ID' }, + note_type: { type: 'string', enum: ['accomplishment', 'decision', 'gotcha', 'next_steps', 'context'], description: 'Category of note' }, + content: { type: 'string', description: 'Note content' }, + }, + required: ['session_id', 'note_type', 'content'], + }, + }, + { + name: 'session_notes_list', + description: 'List all notes for a session, optionally filtered by type', + inputSchema: { + type: 'object', + properties: { + session_id: { type: 'string', description: 'Session ID' }, + note_type: { type: 'string', enum: ['accomplishment', 'decision', 'gotcha', 'next_steps', 'context'], description: 'Filter by note type (optional)' }, + }, + required: ['session_id'], + }, + }, + { + name: 'session_plan_save', + description: 'Save a plan to database with semantic embedding', + inputSchema: { + type: 'object', + properties: { + session_id: { type: 'string', description: 'Session ID' }, + plan_content: { type: 'string', description: 'Plan content in markdown' }, + plan_file_name: { type: 'string', description: 'Original filename (e.g., eloquent-yellow-cat.md) - optional' }, + status: { type: 'string', enum: ['draft', 'approved', 'executed', 'abandoned'], description: 'Plan status (default: draft)' }, + }, + required: ['session_id', 'plan_content'], + }, + }, + { + name: 'session_plan_update_status', + description: 'Update plan status (draft → approved → executed)', + inputSchema: { + type: 'object', + properties: { + plan_id: { type: 'number', description: 'Plan ID to update' }, + status: { type: 'string', enum: ['draft', 'approved', 'executed', 'abandoned'], description: 'New status' }, + }, + required: ['plan_id', 'status'], + }, + }, + { + name: 'session_plan_list', + description: 'List plans for a session', + inputSchema: { + type: 'object', + properties: { + session_id: { type: 'string', description: 'Session ID' }, + status: { type: 'string', enum: ['draft', 'approved', 'executed', 'abandoned'], description: 'Filter by status (optional)' }, + }, + required: ['session_id'], + }, + }, + { + name: 'project_doc_upsert', + description: 'Create or update project documentation (replaces CLAUDE.md sections)', + inputSchema: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project key (e.g., CF, VPN)' }, + doc_type: { type: 'string', enum: ['overview', 'architecture', 'guidelines', 'history'], description: 'Documentation type' }, + title: { type: 'string', description: 'Document title' }, + content: { type: 'string', description: 'Document content in markdown' }, + session_id: { type: 'string', description: 'Session ID (optional)' }, + }, + required: ['project', 'doc_type', 'title', 'content'], + }, + }, + { + name: 'project_doc_get', + description: 'Get specific project documentation by type', + inputSchema: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project key' }, + doc_type: { type: 'string', enum: ['overview', 'architecture', 'guidelines', 'history'], description: 'Documentation type' }, + }, + required: ['project', 'doc_type'], + }, + }, + { + name: 'project_doc_list', + description: 'List all documentation for a project', + inputSchema: { + type: 'object', + properties: { + project: { type: 'string', description: 'Project key' }, + }, + required: ['project'], + }, + }, + { + name: 'session_documentation_generate', + description: 'Auto-generate full markdown documentation for a session (tasks, commits, notes, plans)', + inputSchema: { + type: 'object', + properties: { + session_id: { type: 'string', description: 'Session ID' }, + }, + required: ['session_id'], + }, + }, ]; diff --git a/src/tools/session-docs.ts b/src/tools/session-docs.ts new file mode 100644 index 0000000..c296dfb --- /dev/null +++ b/src/tools/session-docs.ts @@ -0,0 +1,424 @@ +// Session documentation operations - notes, plans, and project documentation +// Replaces file-based CLAUDE.md and plan files with database storage + +import { query, queryOne, execute } from '../db.js'; +import { getEmbedding, formatEmbedding } from '../embeddings.js'; + +// ============================================================================ +// SESSION NOTES +// ============================================================================ + +interface SessionNoteAddArgs { + session_id: string; + note_type: 'accomplishment' | 'decision' | 'gotcha' | 'next_steps' | 'context'; + content: string; +} + +interface SessionNotesListArgs { + session_id: string; + note_type?: 'accomplishment' | 'decision' | 'gotcha' | 'next_steps' | 'context'; +} + +interface SessionNote { + id: number; + session_id: string; + note_type: string; + content: string; + created_at: string; +} + +/** + * Add a note to current session + * Auto-generates embedding for semantic search + */ +export async function sessionNoteAdd(args: SessionNoteAddArgs): Promise { + const { session_id, note_type, content } = args; + + // Generate embedding for semantic search + const embedding = await getEmbedding(content); + const embeddingFormatted = embedding ? formatEmbedding(embedding) : null; + + await execute( + `INSERT INTO session_notes (session_id, note_type, content, embedding) + VALUES ($1, $2, $3, $4)`, + [session_id, note_type, content, embeddingFormatted] + ); + + return `Note added to session ${session_id} (type: ${note_type})`; +} + +/** + * List all notes for a session, optionally filtered by type + */ +export async function sessionNotesList(args: SessionNotesListArgs): Promise { + const { session_id, note_type } = args; + + let sql = ` + SELECT id, session_id, note_type, content, created_at + FROM session_notes + WHERE session_id = $1 + `; + + const params: unknown[] = [session_id]; + + if (note_type) { + sql += ` AND note_type = $2`; + params.push(note_type); + } + + sql += ` ORDER BY created_at ASC`; + + const notes = await query(sql, params); + return notes; +} + +// ============================================================================ +// SESSION PLANS +// ============================================================================ + +interface SessionPlanSaveArgs { + session_id: string; + plan_content: string; + plan_file_name?: string; + status?: 'draft' | 'approved' | 'executed' | 'abandoned'; +} + +interface SessionPlanUpdateStatusArgs { + plan_id: number; + status: 'draft' | 'approved' | 'executed' | 'abandoned'; +} + +interface SessionPlanListArgs { + session_id: string; + status?: 'draft' | 'approved' | 'executed' | 'abandoned'; +} + +interface SessionPlan { + id: number; + session_id: string; + plan_file_name: string | null; + plan_content: string; + status: string; + created_at: string; + approved_at: string | null; + completed_at: string | null; +} + +/** + * Save a plan to database + * Extracts embedding from plan content for semantic search + */ +export async function sessionPlanSave(args: SessionPlanSaveArgs): Promise { + const { session_id, plan_content, plan_file_name, status = 'draft' } = args; + + // Generate embedding for semantic search + const embedding = await getEmbedding(plan_content); + const embeddingFormatted = embedding ? formatEmbedding(embedding) : null; + + const result = await queryOne<{ id: number }>( + `INSERT INTO session_plans (session_id, plan_file_name, plan_content, status, embedding) + VALUES ($1, $2, $3, $4, $5) + RETURNING id`, + [session_id, plan_file_name || null, plan_content, status, embeddingFormatted] + ); + + const planId = result?.id || 0; + + return `Plan saved to session ${session_id} (plan_id: ${planId}, status: ${status})`; +} + +/** + * Update plan status (draft → approved → executed) + */ +export async function sessionPlanUpdateStatus(args: SessionPlanUpdateStatusArgs): Promise { + const { plan_id, status } = args; + + // Set timestamps based on status + let sql = 'UPDATE session_plans SET status = $1'; + const params: unknown[] = [status, plan_id]; + + if (status === 'approved') { + sql += ', approved_at = NOW()'; + } else if (status === 'executed' || status === 'abandoned') { + sql += ', completed_at = NOW()'; + } + + sql += ' WHERE id = $2'; + + await execute(sql, params); + + return `Plan ${plan_id} status updated to: ${status}`; +} + +/** + * List plans for a session + */ +export async function sessionPlanList(args: SessionPlanListArgs): Promise { + const { session_id, status } = args; + + let sql = ` + SELECT id, session_id, plan_file_name, plan_content, status, + created_at, approved_at, completed_at + FROM session_plans + WHERE session_id = $1 + `; + + const params: unknown[] = [session_id]; + + if (status) { + sql += ` AND status = $2`; + params.push(status); + } + + sql += ` ORDER BY created_at DESC`; + + const plans = await query(sql, params); + return plans; +} + +// ============================================================================ +// PROJECT DOCUMENTATION +// ============================================================================ + +interface ProjectDocUpsertArgs { + project: string; + doc_type: 'overview' | 'architecture' | 'guidelines' | 'history'; + title: string; + content: string; + session_id?: string; +} + +interface ProjectDocGetArgs { + project: string; + doc_type: 'overview' | 'architecture' | 'guidelines' | 'history'; +} + +interface ProjectDocListArgs { + project: string; +} + +interface ProjectDoc { + id: number; + project: string; + doc_type: string; + title: string; + content: string; + last_updated_session: string | null; + created_at: string; + updated_at: string; +} + +/** + * Create or update project documentation + * Links to current session + */ +export async function projectDocUpsert(args: ProjectDocUpsertArgs): Promise { + const { project, doc_type, title, content, session_id } = args; + + await execute( + `INSERT INTO project_documentation (project, doc_type, title, content, last_updated_session, updated_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (project, doc_type) + DO UPDATE SET + title = EXCLUDED.title, + content = EXCLUDED.content, + last_updated_session = EXCLUDED.last_updated_session, + updated_at = NOW()`, + [project, doc_type, title, content, session_id || null] + ); + + return `Project documentation updated: ${project} (${doc_type})`; +} + +/** + * Get specific project documentation by type + */ +export async function projectDocGet(args: ProjectDocGetArgs): Promise { + const { project, doc_type } = args; + + const doc = await queryOne( + `SELECT id, project, doc_type, title, content, last_updated_session, created_at, updated_at + FROM project_documentation + WHERE project = $1 AND doc_type = $2`, + [project, doc_type] + ); + + return doc; +} + +/** + * List all documentation for a project + */ +export async function projectDocList(args: ProjectDocListArgs): Promise { + const { project } = args; + + const docs = await query( + `SELECT id, project, doc_type, title, content, last_updated_session, created_at, updated_at + FROM project_documentation + WHERE project = $1 + ORDER BY doc_type`, + [project] + ); + + return docs; +} + +// ============================================================================ +// SESSION DOCUMENTATION GENERATION +// ============================================================================ + +interface SessionDocumentationGenerateArgs { + session_id: string; +} + +/** + * Auto-generate full markdown documentation for a session + * Aggregates: tasks, commits, notes, decisions, plans + * Stores in sessions.documentation field + */ +export async function sessionDocumentationGenerate(args: SessionDocumentationGenerateArgs): Promise { + const { session_id } = args; + + // Fetch session details + const session = await queryOne<{ + id: string; + project: string; + session_number: number; + started_at: string; + ended_at: string; + duration_minutes: number; + summary: string; + git_branch: string; + }>( + `SELECT id, project, session_number, started_at, ended_at, duration_minutes, summary, git_branch + FROM sessions + WHERE id = $1`, + [session_id] + ); + + if (!session) { + throw new Error(`Session not found: ${session_id}`); + } + + // Fetch tasks worked on + const tasks = await query<{ task_id: string; title: string; status: string }>( + `SELECT DISTINCT t.id as task_id, t.title, t.status + FROM task_activity ta + JOIN tasks t ON ta.task_id = t.id + WHERE ta.session_id = $1 + ORDER BY t.id`, + [session_id] + ); + + // Fetch commits made + const commits = await query<{ commit_sha: string; commit_message: string }>( + `SELECT commit_sha, commit_message + FROM session_commits + WHERE session_id = $1 + ORDER BY committed_at`, + [session_id] + ); + + // Fetch notes + const notes = await query<{ note_type: string; content: string }>( + `SELECT note_type, content + FROM session_notes + WHERE session_id = $1 + ORDER BY created_at`, + [session_id] + ); + + // Fetch plans + const plans = await query<{ plan_content: string; status: string }>( + `SELECT plan_content, status + FROM session_plans + WHERE session_id = $1 + ORDER BY created_at DESC + LIMIT 1`, + [session_id] + ); + + // Build markdown documentation + let doc = `# Session ${session.session_number} - ${session.project}\n\n`; + doc += `**Date:** ${new Date(session.started_at).toLocaleDateString()}\n`; + doc += `**Duration:** ${session.duration_minutes || 0} minutes\n`; + doc += `**Status:** ${session.summary ? '✅ Complete' : '⚠️ In Progress'}\n`; + if (session.git_branch) { + doc += `**Branch:** ${session.git_branch}\n`; + } + doc += `\n---\n\n`; + + // Summary + if (session.summary) { + doc += `## Summary\n\n${session.summary}\n\n`; + } + + // Tasks + if (tasks.length > 0) { + doc += `## Tasks Completed\n\n`; + tasks.forEach(task => { + doc += `- **${task.task_id}**: ${task.title} (${task.status})\n`; + }); + doc += `\n`; + } + + // Commits + if (commits.length > 0) { + doc += `## Commits\n\n`; + commits.forEach(commit => { + const shortSha = commit.commit_sha.substring(0, 7); + doc += `- \`${shortSha}\` - ${commit.commit_message || 'No message'}\n`; + }); + doc += `\n`; + } + + // Notes by type + const notesByType: Record = {}; + notes.forEach(note => { + if (!notesByType[note.note_type]) { + notesByType[note.note_type] = []; + } + notesByType[note.note_type].push(note.content); + }); + + if (notesByType.accomplishment && notesByType.accomplishment.length > 0) { + doc += `## Accomplishments\n\n`; + notesByType.accomplishment.forEach(content => { + doc += `${content}\n\n`; + }); + } + + if (notesByType.decision && notesByType.decision.length > 0) { + doc += `## Key Decisions\n\n`; + notesByType.decision.forEach(content => { + doc += `${content}\n\n`; + }); + } + + if (notesByType.gotcha && notesByType.gotcha.length > 0) { + doc += `## Gotchas & Learnings\n\n`; + notesByType.gotcha.forEach(content => { + doc += `${content}\n\n`; + }); + } + + if (notesByType.next_steps && notesByType.next_steps.length > 0) { + doc += `## Next Steps\n\n`; + notesByType.next_steps.forEach(content => { + doc += `${content}\n\n`; + }); + } + + // Plan (if exists) + if (plans.length > 0 && plans[0].status === 'executed') { + doc += `## Plan\n\n${plans[0].plan_content}\n\n`; + } + + // Store documentation in sessions table + await execute( + `UPDATE sessions SET documentation = $1 WHERE id = $2`, + [doc, session_id] + ); + + return `Documentation generated for session ${session_id} (${doc.length} characters)`; +}