Add investigation type and duplicate detection (CF-166 Phase 1)

Enhancements:

1. Investigation Task Type
   - Added 'investigation' to task type enum
   - Migration 013: Updated PostgreSQL constraint
   - Updated TypeScript schemas (task_add, task_list, task_update)
   - Enables tracking research/debugging workflows

2. Duplicate Detection
   - Enhanced task_add to check for similar tasks before creating
   - Uses pgvector semantic search (>70% similarity threshold)
   - Warns about potential duplicates with similarity scores
   - Suggests linking command for related tasks
   - Gracefully handles when embeddings unavailable

Example Output:
```
Created: CF-123
  Title: Fix API rate limiting
  Type: task
  Priority: P2
  Project: CF

⚠️  Similar tasks found:
  - CF-120: Add rate limit monitoring (85% match, in_progress)
  - CF-115: Implement API throttling (72% match, open)

Consider linking with: task link <from> <to> relates_to
```

Benefits:
- Prevents accidental duplicate tasks
- Surfaces related work automatically
- Reduces manual task linking
- Investigation type for research workflows

Files Changed:
- migrations/013_investigation_type.sql (new)
- src/tools/crud.ts (duplicate detection logic)
- src/tools/index.ts (investigation type in enums)

Remaining CF-166 Features (Phase 2):
- Session-aware task context
- Automatic linking within session
- Investigation workflow helper
- Task creation reminders

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-01-17 08:38:18 +02:00
parent 04395e8403
commit f7f9cfe3d8
3 changed files with 43 additions and 6 deletions

View File

@@ -0,0 +1,13 @@
-- Migration 013: Add investigation task type
-- Adds 'investigation' to task type enum for tracking research/debugging sessions
-- Drop existing constraint
ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_type_check;
-- Add new constraint with investigation type
ALTER TABLE tasks ADD CONSTRAINT tasks_type_check
CHECK (type IN ('task', 'bug', 'feature', 'debt', 'investigation'));
-- Record migration
INSERT INTO schema_migrations (version) VALUES ('013_investigation_type')
ON CONFLICT (version) DO NOTHING;

View File

@@ -88,14 +88,38 @@ export async function taskAdd(args: TaskAddArgs): Promise<string> {
// Get project key // Get project key
const projectKey = await getProjectKey(project); const projectKey = await getProjectKey(project);
// Get next task ID // Generate embedding for duplicate detection
const taskId = await getNextTaskId(projectKey);
// Generate embedding
const embedText = description ? `${title}. ${description}` : title; const embedText = description ? `${title}. ${description}` : title;
const embedding = await getEmbedding(embedText); const embedding = await getEmbedding(embedText);
const embeddingValue = embedding ? formatEmbedding(embedding) : null; const embeddingValue = embedding ? formatEmbedding(embedding) : null;
// Check for similar/duplicate tasks (only if embedding succeeded)
let duplicateWarning = '';
if (embeddingValue) {
const similarTasks = await query<{ id: string; title: string; status: string; similarity: number }>(
`SELECT id, title, status, 1 - (embedding <=> $1) as similarity
FROM tasks
WHERE project = $2 AND embedding IS NOT NULL AND status != 'completed'
ORDER BY embedding <=> $1
LIMIT 3`,
[embeddingValue, projectKey]
);
// Warn if highly similar tasks exist (>70% similarity)
const highSimilarity = similarTasks.filter(t => t.similarity > 0.70);
if (highSimilarity.length > 0) {
duplicateWarning = '\n\n⚠ Similar tasks found:\n';
for (const t of highSimilarity) {
const pct = Math.round(t.similarity * 100);
duplicateWarning += ` - ${t.id}: ${t.title} (${pct}% match, ${t.status})\n`;
}
duplicateWarning += '\nConsider linking with: task link <from> <to> relates_to';
}
}
// Get next task ID
const taskId = await getNextTaskId(projectKey);
// Insert task // Insert task
if (embeddingValue) { if (embeddingValue) {
await execute( await execute(
@@ -114,7 +138,7 @@ 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');
return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}`; return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}${duplicateWarning}`;
} }
/** /**

View File

@@ -10,7 +10,7 @@ export const toolDefinitions = [
properties: { properties: {
title: { type: 'string', description: 'Task title (required)' }, title: { type: 'string', description: 'Task title (required)' },
project: { type: 'string', description: 'Project key (e.g., ST, VPN). Auto-detected from CWD if not provided.' }, project: { type: 'string', description: 'Project key (e.g., ST, VPN). Auto-detected from CWD if not provided.' },
type: { type: 'string', enum: ['task', 'bug', 'feature', 'debt'], description: 'Task type (default: task)' }, type: { type: 'string', enum: ['task', 'bug', 'feature', 'debt', 'investigation'], description: 'Task type (default: task)' },
priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'Priority level (default: P2)' }, priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'Priority level (default: P2)' },
description: { type: 'string', description: 'Optional description' }, description: { type: 'string', description: 'Optional description' },
}, },