diff --git a/README.md b/README.md index f16d0ea..6653a5c 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,85 @@ Add to `~/.claude/settings.json` under `mcpServers`: | `task_show` | Show task details including checklist and dependencies | | `task_close` | Mark task as completed | | `task_update` | Update task fields | +| `task_investigate` | Start investigation workflow (auto-links subsequent tasks) | | `task_similar` | Find semantically similar tasks using pgvector | | `task_context` | Get related tasks for current work context | | `task_link` | Create dependency between tasks | | `task_checklist_add` | Add checklist item to task | | `task_checklist_toggle` | Toggle checklist item | +## Investigation Workflow (CF-166) + +The investigation workflow helps organize multi-step problem analysis by automatically linking related tasks. + +### Starting an Investigation + +```typescript +// Start investigation +task_investigate({ + title: "Investigate $4,746 Brave API cost overrun", + project: "CF", + priority: "P1", + description: "Determine why Brave API costs are 10x expected" +}); +``` + +This creates an investigation task and sets it as the session's investigation parent. All subsequent tasks created in this session will automatically link to the investigation. + +### Auto-Linking Behavior + +When you create tasks during an active investigation: + +1. **Investigation Parent Linking**: New tasks automatically link to the investigation task +2. **Current Task Linking**: Tasks also link to the current working task (if different from investigation) +3. **Time-Based Linking**: If no investigation is active, tasks created within 1 hour auto-link to recent tasks in the same session + +### Task Types + +- `task`: General work item (default) +- `bug`: Bug fix +- `feature`: New feature +- `debt`: Technical debt +- `investigation`: Multi-step problem analysis (use with `task_investigate`) + +### Example Investigation Flow + +```typescript +// 1. Start investigation +task_investigate({ + title: "Investigate database performance degradation", + project: "CF" +}); +// Creates CF-100 (investigation task) + +// 2. Create subtasks - these auto-link to CF-100 +task_add({ + title: "Check slow query log", + project: "CF" +}); +// Creates CF-101, auto-links to CF-100 + +task_add({ + title: "Analyze connection pool metrics", + project: "CF" +}); +// Creates CF-102, auto-links to CF-100 + +// 3. View investigation with all linked tasks +task_show({ id: "CF-100" }); +// Shows investigation with all related subtasks +``` + +### Session Context + +The MCP server tracks session context to enable smart auto-linking: + +- **Current Task**: The task you're actively working on (set when status changes to `in_progress`) +- **Investigation Parent**: The investigation task all new tasks should link to +- **Auto-link Enabled**: Whether to automatically create task links (default: true) + +Session context is stored per session and cleared when tasks are completed or sessions end. + ## Build ```bash diff --git a/migrations/020_add_investigation_type.sql b/migrations/020_add_investigation_type.sql new file mode 100644 index 0000000..478635c --- /dev/null +++ b/migrations/020_add_investigation_type.sql @@ -0,0 +1,43 @@ +-- Migration 020: Add 'investigation' task type and auto-linking infrastructure +-- Implements CF-166: Enhanced investigation workflows + +-- 1. Add 'investigation' to task type CHECK constraint +-- First, check if constraint exists and drop it +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'tasks_type_check' + AND conrelid = 'tasks'::regclass + ) THEN + ALTER TABLE tasks DROP CONSTRAINT tasks_type_check; + END IF; +END $$; + +-- Add new constraint with investigation type +ALTER TABLE tasks ADD CONSTRAINT tasks_type_check + CHECK (type IN ('task', 'bug', 'feature', 'debt', 'investigation')); + +-- 2. Add new columns to session_context table for investigation tracking +-- Table already exists from previous migration, just add new columns +ALTER TABLE session_context ADD COLUMN IF NOT EXISTS investigation_parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL; +ALTER TABLE session_context ADD COLUMN IF NOT EXISTS auto_link_enabled BOOLEAN DEFAULT true; + +-- Create indexes for new columns +CREATE INDEX IF NOT EXISTS idx_session_context_investigation ON session_context(investigation_parent_id); + +-- Add comments (only if table was just created) +-- COMMENT ON TABLE session_context IS 'Tracks current working task per session for auto-linking (CF-166)'; +-- COMMENT ON COLUMN session_context.current_task_id IS 'Task currently being worked on in this session'; +-- COMMENT ON COLUMN session_context.investigation_parent_id IS 'Parent investigation task for auto-linking subtasks'; +-- COMMENT ON COLUMN session_context.auto_link_enabled IS 'Whether to automatically link new tasks to current context'; + +-- 3. Add metadata to task_links for auto-linking tracking +ALTER TABLE task_links ADD COLUMN IF NOT EXISTS auto_linked BOOLEAN DEFAULT false; +ALTER TABLE task_links ADD COLUMN IF NOT EXISTS linked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(); + +COMMENT ON COLUMN task_links.auto_linked IS 'Whether this link was created automatically (CF-166)'; +COMMENT ON COLUMN task_links.linked_at IS 'Timestamp when the link was created'; + +-- Create index for finding tasks created within time windows (for auto-linking) +CREATE INDEX IF NOT EXISTS idx_tasks_created_at_desc ON tasks(created_at DESC); diff --git a/src/index.ts b/src/index.ts index edabf84..2d9647c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,7 @@ import { import { testConnection, close } from './db.js'; import { toolDefinitions } from './tools/index.js'; -import { taskAdd, taskList, taskShow, taskClose, taskUpdate } from './tools/crud.js'; +import { taskAdd, taskList, taskShow, taskClose, taskUpdate, taskInvestigate } from './tools/crud.js'; import { taskSimilar, taskContext } from './tools/search.js'; import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js'; import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js'; @@ -139,6 +139,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { title: a.title, }); break; + case 'task_investigate': + result = await taskInvestigate({ + title: a.title, + project: a.project, + priority: a.priority, + description: a.description, + }); + break; // Search case 'task_similar': diff --git a/src/tools/crud.ts b/src/tools/crud.ts index 4927803..0af16b1 100644 --- a/src/tools/crud.ts +++ b/src/tools/crud.ts @@ -142,25 +142,75 @@ export async function taskAdd(args: TaskAddArgs): Promise { // Record activity for session tracking await recordActivity(taskId, 'created', undefined, 'open'); - // Check for session context and auto-link to current working task + // Enhanced auto-linking logic (CF-166) let autoLinkMessage = ''; try { - const sessionContext = await queryOne<{ current_task_id: string }>( - `SELECT current_task_id FROM session_context WHERE session_id = $1`, + const sessionContext = await queryOne<{ + current_task_id: string | null; + investigation_parent_id: string | null; + auto_link_enabled: boolean; + }>( + `SELECT current_task_id, investigation_parent_id, auto_link_enabled + FROM session_context WHERE session_id = $1`, [session_id] ); - if (sessionContext?.current_task_id) { - // Auto-link to current working task - await taskLink({ - from_id: taskId, - to_id: sessionContext.current_task_id, - link_type: 'relates_to' - }); - autoLinkMessage = `\n\nšŸ”— Auto-linked to: ${sessionContext.current_task_id} (current working task)`; + if (sessionContext?.auto_link_enabled !== false) { + const linkedTasks: string[] = []; + + // 1. Auto-link to investigation parent if this is created during an investigation + if (sessionContext?.investigation_parent_id) { + await execute( + `INSERT INTO task_links (from_task_id, to_task_id, link_type, auto_linked) + VALUES ($1, $2, 'relates_to', true) + ON CONFLICT DO NOTHING`, + [taskId, sessionContext.investigation_parent_id] + ); + linkedTasks.push(`${sessionContext.investigation_parent_id} (investigation)`); + } + + // 2. Auto-link to current working task if different from investigation parent + if (sessionContext?.current_task_id && + sessionContext.current_task_id !== sessionContext?.investigation_parent_id) { + await execute( + `INSERT INTO task_links (from_task_id, to_task_id, link_type, auto_linked) + VALUES ($1, $2, 'relates_to', true) + ON CONFLICT DO NOTHING`, + [taskId, sessionContext.current_task_id] + ); + linkedTasks.push(`${sessionContext.current_task_id} (current task)`); + } + + // 3. Time-based auto-linking: find tasks created within 1 hour in same session + if (!sessionContext?.investigation_parent_id && !sessionContext?.current_task_id) { + const recentTasks = await query<{ id: string; title: string }>( + `SELECT id, title FROM tasks + WHERE session_id = $1 AND id != $2 + AND created_at > NOW() - INTERVAL '1 hour' + AND status != 'completed' + ORDER BY created_at DESC + LIMIT 3`, + [session_id, taskId] + ); + + for (const task of recentTasks) { + await execute( + `INSERT INTO task_links (from_task_id, to_task_id, link_type, auto_linked) + VALUES ($1, $2, 'relates_to', true) + ON CONFLICT DO NOTHING`, + [taskId, task.id] + ); + linkedTasks.push(`${task.id} (recent)`); + } + } + + if (linkedTasks.length > 0) { + autoLinkMessage = `\n\nšŸ”— Auto-linked to: ${linkedTasks.join(', ')}`; + } } - } catch { - // Silently fail if session context unavailable + } catch (error) { + // Log but don't fail if auto-linking fails + console.error('Auto-linking failed:', error); } return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}${duplicateWarning}${autoLinkMessage}`; @@ -460,3 +510,44 @@ export async function taskUpdate(args: TaskUpdateArgs): Promise { return `Updated: ${id}`; } + +/** + * Start an investigation workflow (CF-166) + * Creates an investigation task and sets it as the session context parent + * All subsequent tasks will auto-link to this investigation + */ +export async function taskInvestigate(args: TaskAddArgs): Promise { + const { title, project, priority = 'P1', description = '' } = args; + + // Create investigation task + const taskResult = await taskAdd({ + title, + project, + type: 'investigation', + priority, + description: description || 'Investigation task to coordinate related subtasks', + }); + + // Extract task ID from result (format: "Created: XX-123\n...") + const taskIdMatch = taskResult.match(/Created: ([\w-]+)/); + if (!taskIdMatch) { + return taskResult; // Return original message if format unexpected + } + const taskId = taskIdMatch[1]; + + // Set as investigation parent in session context + const session_id = getSessionId(); + try { + await execute( + `INSERT INTO session_context (session_id, current_task_id, investigation_parent_id) + VALUES ($1, $2, $2) + ON CONFLICT (session_id) DO UPDATE + SET investigation_parent_id = $2, current_task_id = $2, updated_at = NOW()`, + [session_id, taskId] + ); + } catch (error) { + console.error('Failed to set investigation context:', error); + } + + return taskResult + '\n\nšŸ” Investigation started! All new tasks will auto-link to this investigation.'; +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 87b837f..dcff30f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -25,7 +25,7 @@ export const toolDefinitions = [ properties: { project: { type: 'string', description: 'Filter by project key' }, status: { type: 'string', enum: ['open', 'in_progress', 'testing', 'blocked', 'completed'], description: 'Filter by status' }, - type: { type: 'string', enum: ['task', 'bug', 'feature', 'debt'], description: 'Filter by type' }, + type: { type: 'string', enum: ['task', 'bug', 'feature', 'debt', 'investigation'], description: 'Filter by type' }, priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'Filter by priority' }, limit: { type: 'number', description: 'Max results (default: 20)' }, }, @@ -62,12 +62,26 @@ export const toolDefinitions = [ id: { type: 'string', description: 'Task ID to update' }, status: { type: 'string', enum: ['open', 'in_progress', 'testing', 'blocked', 'completed'], description: 'New status' }, priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'New priority' }, - type: { type: 'string', enum: ['task', 'bug', 'feature', 'debt'], description: 'New type' }, + type: { type: 'string', enum: ['task', 'bug', 'feature', 'debt', 'investigation'], description: 'New type' }, title: { type: 'string', description: 'New title' }, }, required: ['id'], }, }, + { + name: 'task_investigate', + description: 'Start an investigation workflow: creates an investigation task and auto-links all subsequent tasks to it. Use when beginning multi-step problem analysis.', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string', description: 'Investigation title (required)' }, + project: { type: 'string', description: 'Project key (e.g., ST, VPN). Auto-detected from CWD if not provided.' }, + priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'Priority level (default: P1)' }, + description: { type: 'string', description: 'Optional description of investigation scope' }, + }, + required: ['title'], + }, + }, // Semantic Search Tools { diff --git a/src/types.ts b/src/types.ts index c2dd97e..7fa4c4c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,7 @@ export interface Task { project: string; title: string; description?: string; - type: 'task' | 'bug' | 'feature' | 'debt'; + type: 'task' | 'bug' | 'feature' | 'debt' | 'investigation'; status: 'open' | 'in_progress' | 'testing' | 'blocked' | 'completed'; priority: 'P0' | 'P1' | 'P2' | 'P3'; version_id?: string;