feat(CF-166): Add investigation workflow with auto-linking

Implements comprehensive investigation task management:

**Schema Changes (Migration 020):**
- Add 'investigation' task type to tasks table
- Add investigation_parent_id and auto_link_enabled columns to session_context
- Add auto_linked and linked_at columns to task_links for tracking
- Create indexes for efficient auto-linking queries

**Auto-Linking Logic:**
1. Investigation Parent Linking: Tasks auto-link to active investigation
2. Current Task Linking: Tasks link to current working task
3. Time-Based Linking: Tasks within 1 hour auto-link (fallback)

**New Tool:**
- task_investigate: Start investigation workflow, sets session context

**Documentation:**
- Add comprehensive investigation workflow guide to README
- Include examples and session context explanation

All checklist items completed:
✓ Investigation task type in schema
✓ Session context table enhancements
✓ Auto-linking implementation
✓ task_investigate command
✓ Documentation updates

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-01-19 19:15:25 +02:00
parent caf9356aad
commit 7be5878d35
6 changed files with 246 additions and 17 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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':

View File

@@ -142,25 +142,75 @@ export async function taskAdd(args: TaskAddArgs): Promise<string> {
// 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<string> {
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<string> {
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.';
}

View File

@@ -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
{

View File

@@ -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;