Session 374: Task-MCP and Delegation System Integration (Phase 4 & 6) - Add task_delegations tool: query delegations for a specific task - Add task_delegation_query tool: query across all tasks by status/backend - Enhance taskShow() to display recent delegation history - New delegations.ts module with getRecentDelegations helper Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
304 lines
8.1 KiB
TypeScript
304 lines
8.1 KiB
TypeScript
// CRUD operations for tasks
|
|
|
|
import { query, queryOne, execute, getNextTaskId, getProjectKey } from '../db.js';
|
|
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
|
import type { Task, ChecklistItem, TaskLink } from '../types.js';
|
|
import { getRecentDelegations } from './delegations.js';
|
|
|
|
interface TaskAddArgs {
|
|
title: string;
|
|
project?: string;
|
|
type?: string;
|
|
priority?: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface TaskListArgs {
|
|
project?: string;
|
|
status?: string;
|
|
type?: string;
|
|
priority?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
interface TaskUpdateArgs {
|
|
id: string;
|
|
status?: string;
|
|
priority?: string;
|
|
type?: string;
|
|
title?: string;
|
|
}
|
|
|
|
/**
|
|
* Create a new task
|
|
*/
|
|
export async function taskAdd(args: TaskAddArgs): Promise<string> {
|
|
const { title, project = 'Unknown', type = 'task', priority = 'P2', description = '' } = args;
|
|
|
|
// Get project key
|
|
const projectKey = await getProjectKey(project);
|
|
|
|
// Get next task ID
|
|
const taskId = await getNextTaskId(projectKey);
|
|
|
|
// Generate embedding
|
|
const embedText = description ? `${title}. ${description}` : title;
|
|
const embedding = await getEmbedding(embedText);
|
|
const embeddingValue = embedding ? formatEmbedding(embedding) : null;
|
|
|
|
// Insert task
|
|
if (embeddingValue) {
|
|
await execute(
|
|
`INSERT INTO tasks (id, project, title, description, type, status, priority, embedding)
|
|
VALUES ($1, $2, $3, $4, $5, 'open', $6, $7)`,
|
|
[taskId, projectKey, title, description, type, priority, embeddingValue]
|
|
);
|
|
} else {
|
|
await execute(
|
|
`INSERT INTO tasks (id, project, title, description, type, status, priority)
|
|
VALUES ($1, $2, $3, $4, $5, 'open', $6)`,
|
|
[taskId, projectKey, title, description, type, priority]
|
|
);
|
|
}
|
|
|
|
return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}`;
|
|
}
|
|
|
|
/**
|
|
* List tasks with filters
|
|
*/
|
|
export async function taskList(args: TaskListArgs): Promise<string> {
|
|
const { project, status, type, priority, limit = 20 } = args;
|
|
|
|
let whereClause = 'WHERE 1=1';
|
|
const params: unknown[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (project) {
|
|
const projectKey = await getProjectKey(project);
|
|
whereClause += ` AND project = $${paramIndex++}`;
|
|
params.push(projectKey);
|
|
}
|
|
if (status) {
|
|
whereClause += ` AND status = $${paramIndex++}`;
|
|
params.push(status);
|
|
}
|
|
if (type) {
|
|
whereClause += ` AND type = $${paramIndex++}`;
|
|
params.push(type);
|
|
}
|
|
if (priority) {
|
|
whereClause += ` AND priority = $${paramIndex++}`;
|
|
params.push(priority);
|
|
}
|
|
|
|
params.push(limit);
|
|
|
|
const tasks = await query<Task>(
|
|
`SELECT id, title, type, status, priority, project
|
|
FROM tasks
|
|
${whereClause}
|
|
ORDER BY
|
|
CASE priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END,
|
|
created_at DESC
|
|
LIMIT $${paramIndex}`,
|
|
params
|
|
);
|
|
|
|
if (tasks.length === 0) {
|
|
return `No tasks found${project ? ` for project ${project}` : ''}`;
|
|
}
|
|
|
|
const lines = tasks.map(t => {
|
|
const statusIcon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[>]' : t.status === 'blocked' ? '[!]' : '[ ]';
|
|
const typeLabel = t.type !== 'task' ? ` [${t.type}]` : '';
|
|
return `${statusIcon} ${t.priority} ${t.id}: ${t.title}${typeLabel}`;
|
|
});
|
|
|
|
return `Tasks${project ? ` (${project})` : ''}:\n\n${lines.join('\n')}`;
|
|
}
|
|
|
|
/**
|
|
* Show task details
|
|
*/
|
|
export async function taskShow(id: string): Promise<string> {
|
|
const task = await queryOne<Task>(
|
|
`SELECT id, project, title, description, type, status, priority,
|
|
to_char(created_at, 'YYYY-MM-DD HH24:MI') as created,
|
|
to_char(updated_at, 'YYYY-MM-DD HH24:MI') as updated,
|
|
to_char(completed_at, 'YYYY-MM-DD HH24:MI') as completed
|
|
FROM tasks WHERE id = $1`,
|
|
[id]
|
|
);
|
|
|
|
if (!task) {
|
|
return `Task not found: ${id}`;
|
|
}
|
|
|
|
let output = `# ${task.id}\n\n`;
|
|
output += `**Title:** ${task.title}\n`;
|
|
output += `**Project:** ${task.project}\n`;
|
|
output += `**Type:** ${task.type}\n`;
|
|
output += `**Status:** ${task.status}\n`;
|
|
output += `**Priority:** ${task.priority}\n`;
|
|
output += `**Created:** ${(task as unknown as { created: string }).created}\n`;
|
|
output += `**Updated:** ${(task as unknown as { updated: string }).updated}\n`;
|
|
|
|
if ((task as unknown as { completed: string }).completed) {
|
|
output += `**Completed:** ${(task as unknown as { completed: string }).completed}\n`;
|
|
}
|
|
|
|
if (task.description) {
|
|
output += `\n**Description:**\n${task.description}\n`;
|
|
}
|
|
|
|
// Get checklist
|
|
const checklist = await query<ChecklistItem>(
|
|
`SELECT id, item, checked FROM task_checklist
|
|
WHERE task_id = $1 ORDER BY position, id`,
|
|
[id]
|
|
);
|
|
|
|
if (checklist.length > 0) {
|
|
const done = checklist.filter(c => c.checked).length;
|
|
output += `\n**Checklist:** (${done}/${checklist.length})\n`;
|
|
for (const item of checklist) {
|
|
output += ` ${item.checked ? '[x]' : '[ ]'} ${item.item} (#${item.id})\n`;
|
|
}
|
|
}
|
|
|
|
// Get dependencies
|
|
const blockedBy = await query<{ id: string; title: string }>(
|
|
`SELECT t.id, t.title FROM task_links l
|
|
JOIN tasks t ON t.id = l.from_task_id
|
|
WHERE l.to_task_id = $1 AND l.link_type = 'blocks'`,
|
|
[id]
|
|
);
|
|
|
|
const blocks = await query<{ id: string; title: string }>(
|
|
`SELECT t.id, t.title FROM task_links l
|
|
JOIN tasks t ON t.id = l.to_task_id
|
|
WHERE l.from_task_id = $1 AND l.link_type = 'blocks'`,
|
|
[id]
|
|
);
|
|
|
|
if (blockedBy.length > 0) {
|
|
output += `\n**Blocked by:**\n`;
|
|
for (const t of blockedBy) {
|
|
output += ` - ${t.id}: ${t.title}\n`;
|
|
}
|
|
}
|
|
|
|
if (blocks.length > 0) {
|
|
output += `\n**Blocks:**\n`;
|
|
for (const t of blocks) {
|
|
output += ` - ${t.id}: ${t.title}\n`;
|
|
}
|
|
}
|
|
|
|
// Get related tasks (bidirectional - only need to query one direction since links are symmetric)
|
|
const relatesTo = await query<{ id: string; title: string }>(
|
|
`SELECT t.id, t.title FROM task_links l
|
|
JOIN tasks t ON t.id = l.to_task_id
|
|
WHERE l.from_task_id = $1 AND l.link_type = 'relates_to'`,
|
|
[id]
|
|
);
|
|
|
|
if (relatesTo.length > 0) {
|
|
output += `\n**Related:**\n`;
|
|
for (const t of relatesTo) {
|
|
output += ` - ${t.id}: ${t.title}\n`;
|
|
}
|
|
}
|
|
|
|
// Get duplicates (bidirectional)
|
|
const duplicates = await query<{ id: string; title: string }>(
|
|
`SELECT t.id, t.title FROM task_links l
|
|
JOIN tasks t ON t.id = l.to_task_id
|
|
WHERE l.from_task_id = $1 AND l.link_type = 'duplicates'`,
|
|
[id]
|
|
);
|
|
|
|
if (duplicates.length > 0) {
|
|
output += `\n**Duplicates:**\n`;
|
|
for (const t of duplicates) {
|
|
output += ` - ${t.id}: ${t.title}\n`;
|
|
}
|
|
}
|
|
|
|
// Get recent delegations
|
|
const delegationHistory = await getRecentDelegations(id);
|
|
if (delegationHistory) {
|
|
output += delegationHistory;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Close a task
|
|
*/
|
|
export async function taskClose(id: string): Promise<string> {
|
|
const result = await execute(
|
|
`UPDATE tasks
|
|
SET status = 'completed', completed_at = NOW(), updated_at = NOW()
|
|
WHERE id = $1`,
|
|
[id]
|
|
);
|
|
|
|
if (result === 0) {
|
|
return `Task not found: ${id}`;
|
|
}
|
|
|
|
return `Closed: ${id}`;
|
|
}
|
|
|
|
/**
|
|
* Update a task
|
|
*/
|
|
export async function taskUpdate(args: TaskUpdateArgs): Promise<string> {
|
|
const { id, status, priority, type, title } = args;
|
|
|
|
const updates: string[] = [];
|
|
const params: unknown[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (status) {
|
|
updates.push(`status = $${paramIndex++}`);
|
|
params.push(status);
|
|
if (status === 'completed') {
|
|
updates.push(`completed_at = NOW()`);
|
|
}
|
|
}
|
|
if (priority) {
|
|
updates.push(`priority = $${paramIndex++}`);
|
|
params.push(priority);
|
|
}
|
|
if (type) {
|
|
updates.push(`type = $${paramIndex++}`);
|
|
params.push(type);
|
|
}
|
|
if (title) {
|
|
updates.push(`title = $${paramIndex++}`);
|
|
params.push(title);
|
|
}
|
|
|
|
if (updates.length === 0) {
|
|
return 'No updates specified';
|
|
}
|
|
|
|
updates.push('updated_at = NOW()');
|
|
params.push(id);
|
|
|
|
const result = await execute(
|
|
`UPDATE tasks SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
|
|
params
|
|
);
|
|
|
|
if (result === 0) {
|
|
return `Task not found: ${id}`;
|
|
}
|
|
|
|
return `Updated: ${id}`;
|
|
}
|