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:
73
README.md
73
README.md
@@ -39,12 +39,85 @@ Add to `~/.claude/settings.json` under `mcpServers`:
|
|||||||
| `task_show` | Show task details including checklist and dependencies |
|
| `task_show` | Show task details including checklist and dependencies |
|
||||||
| `task_close` | Mark task as completed |
|
| `task_close` | Mark task as completed |
|
||||||
| `task_update` | Update task fields |
|
| `task_update` | Update task fields |
|
||||||
|
| `task_investigate` | Start investigation workflow (auto-links subsequent tasks) |
|
||||||
| `task_similar` | Find semantically similar tasks using pgvector |
|
| `task_similar` | Find semantically similar tasks using pgvector |
|
||||||
| `task_context` | Get related tasks for current work context |
|
| `task_context` | Get related tasks for current work context |
|
||||||
| `task_link` | Create dependency between tasks |
|
| `task_link` | Create dependency between tasks |
|
||||||
| `task_checklist_add` | Add checklist item to task |
|
| `task_checklist_add` | Add checklist item to task |
|
||||||
| `task_checklist_toggle` | Toggle checklist item |
|
| `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
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
43
migrations/020_add_investigation_type.sql
Normal file
43
migrations/020_add_investigation_type.sql
Normal 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);
|
||||||
10
src/index.ts
10
src/index.ts
@@ -37,7 +37,7 @@ import {
|
|||||||
|
|
||||||
import { testConnection, close } from './db.js';
|
import { testConnection, close } from './db.js';
|
||||||
import { toolDefinitions } from './tools/index.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 { taskSimilar, taskContext } from './tools/search.js';
|
||||||
import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js';
|
import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js';
|
||||||
import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js';
|
import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js';
|
||||||
@@ -139,6 +139,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
title: a.title,
|
title: a.title,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'task_investigate':
|
||||||
|
result = await taskInvestigate({
|
||||||
|
title: a.title,
|
||||||
|
project: a.project,
|
||||||
|
priority: a.priority,
|
||||||
|
description: a.description,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
case 'task_similar':
|
case 'task_similar':
|
||||||
|
|||||||
@@ -142,25 +142,75 @@ export async function taskAdd(args: TaskAddArgs): Promise<string> {
|
|||||||
// Record activity for session tracking
|
// Record activity for session tracking
|
||||||
await recordActivity(taskId, 'created', undefined, 'open');
|
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 = '';
|
let autoLinkMessage = '';
|
||||||
try {
|
try {
|
||||||
const sessionContext = await queryOne<{ current_task_id: string }>(
|
const sessionContext = await queryOne<{
|
||||||
`SELECT current_task_id FROM session_context WHERE session_id = $1`,
|
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]
|
[session_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sessionContext?.current_task_id) {
|
if (sessionContext?.auto_link_enabled !== false) {
|
||||||
// Auto-link to current working task
|
const linkedTasks: string[] = [];
|
||||||
await taskLink({
|
|
||||||
from_id: taskId,
|
// 1. Auto-link to investigation parent if this is created during an investigation
|
||||||
to_id: sessionContext.current_task_id,
|
if (sessionContext?.investigation_parent_id) {
|
||||||
link_type: 'relates_to'
|
await execute(
|
||||||
});
|
`INSERT INTO task_links (from_task_id, to_task_id, link_type, auto_linked)
|
||||||
autoLinkMessage = `\n\n🔗 Auto-linked to: ${sessionContext.current_task_id} (current working task)`;
|
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 {
|
} catch (error) {
|
||||||
// Silently fail if session context unavailable
|
// 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}`;
|
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}`;
|
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.';
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const toolDefinitions = [
|
|||||||
properties: {
|
properties: {
|
||||||
project: { type: 'string', description: 'Filter by project key' },
|
project: { type: 'string', description: 'Filter by project key' },
|
||||||
status: { type: 'string', enum: ['open', 'in_progress', 'testing', 'blocked', 'completed'], description: 'Filter by status' },
|
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' },
|
priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'Filter by priority' },
|
||||||
limit: { type: 'number', description: 'Max results (default: 20)' },
|
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||||
},
|
},
|
||||||
@@ -62,12 +62,26 @@ export const toolDefinitions = [
|
|||||||
id: { type: 'string', description: 'Task ID to update' },
|
id: { type: 'string', description: 'Task ID to update' },
|
||||||
status: { type: 'string', enum: ['open', 'in_progress', 'testing', 'blocked', 'completed'], description: 'New status' },
|
status: { type: 'string', enum: ['open', 'in_progress', 'testing', 'blocked', 'completed'], description: 'New status' },
|
||||||
priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'New priority' },
|
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' },
|
title: { type: 'string', description: 'New title' },
|
||||||
},
|
},
|
||||||
required: ['id'],
|
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
|
// Semantic Search Tools
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface Task {
|
|||||||
project: string;
|
project: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
type: 'task' | 'bug' | 'feature' | 'debt';
|
type: 'task' | 'bug' | 'feature' | 'debt' | 'investigation';
|
||||||
status: 'open' | 'in_progress' | 'testing' | 'blocked' | 'completed';
|
status: 'open' | 'in_progress' | 'testing' | 'blocked' | 'completed';
|
||||||
priority: 'P0' | 'P1' | 'P2' | 'P3';
|
priority: 'P0' | 'P1' | 'P2' | 'P3';
|
||||||
version_id?: string;
|
version_id?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user