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_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
|
||||
|
||||
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 { 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':
|
||||
|
||||
@@ -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)`);
|
||||
}
|
||||
} catch {
|
||||
// Silently fail if session context unavailable
|
||||
|
||||
// 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 (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.';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user