feat(CF-762): Complete Jira migration - consolidate projects, cleanup
- Remove task CRUD/epic/search/relation/version tools (moved to Jira) - Add migration scripts: migrate-tasks-to-jira, jira-admin, prepare-all-projects - Add consolidate-projects.ts for merging duplicate Jira projects - Add validate-migration.ts for post-migration integrity checks - Add jira_issue_key columns migration (030) - Consolidate 11 duplicate projects (LIT→LITE, CARD→CS, etc.) - Delete 92 placeholder issues, 11 empty source projects - Remove SG project completely - 2,798 tasks migrated across 46 Jira projects Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
207
src/index.ts
207
src/index.ts
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Task MCP Server
|
||||
* Session MCP Server
|
||||
*
|
||||
* Exposes task management tools via Model Context Protocol.
|
||||
* Uses PostgreSQL with pgvector for semantic search.
|
||||
* Forked from task-mcp (CF-762): Sessions, memory, archives, infrastructure.
|
||||
* Task management now handled by Jira Cloud via mcp-atlassian.
|
||||
*
|
||||
* Requires SSH tunnel to infra VM on port 5433:
|
||||
* ssh -L 5433:localhost:5432 -i ~/.ssh/hetzner_mash_deploy root@46.224.188.157 -N &
|
||||
* Uses PostgreSQL with pgvector for semantic search on sessions/memories.
|
||||
*/
|
||||
|
||||
// Load environment variables from .env file
|
||||
@@ -19,17 +18,14 @@ const __dirname = dirname(__filename);
|
||||
const envPath = join(__dirname, '..', '.env');
|
||||
const result = dotenv.config({ path: envPath, override: true });
|
||||
|
||||
// Initialize Sentry for error tracking (with MCP-aware filtering and PII scrubbing)
|
||||
// Initialize Sentry for error tracking
|
||||
import { initSentry } from './sentry.js';
|
||||
initSentry(process.env.SENTRY_ENVIRONMENT || 'production');
|
||||
|
||||
// Log environment loading status (goes to MCP server logs)
|
||||
if (result.error) {
|
||||
console.error('Failed to load .env from:', envPath, result.error);
|
||||
} else {
|
||||
console.error('Loaded .env from:', envPath);
|
||||
console.error('LLM_API_KEY present:', !!process.env.LLM_API_KEY);
|
||||
console.error('LLM_API_URL:', process.env.LLM_API_URL);
|
||||
}
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
@@ -41,13 +37,10 @@ import {
|
||||
|
||||
import { testConnection, close } from './db.js';
|
||||
import { toolDefinitions } from './tools/index.js';
|
||||
import { taskAdd, taskList, taskShow, taskClose, taskUpdate, taskInvestigate, taskMoveProject } from './tools/crud.js';
|
||||
import { taskSimilar, taskContext, taskSessionContext } from './tools/search.js';
|
||||
import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js';
|
||||
import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js';
|
||||
|
||||
// Kept tools (sessions, memory, archives, infrastructure, docs, delegations, commits)
|
||||
import { taskDelegations, taskDelegationQuery } from './tools/delegations.js';
|
||||
import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js';
|
||||
import { versionAdd, versionList, versionShow, versionUpdate, versionRelease, versionAssignTask } from './tools/versions.js';
|
||||
import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js';
|
||||
import { changelogAdd, changelogSinceSession, changelogList } from './tools/changelog.js';
|
||||
import {
|
||||
@@ -93,7 +86,7 @@ import { projectArchive } from './tools/project-archive.js';
|
||||
|
||||
// Create MCP server
|
||||
const server = new Server(
|
||||
{ name: 'task-mcp', version: '1.0.0' },
|
||||
{ name: 'session-mcp', version: '1.0.0' },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
@@ -112,134 +105,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const a = args as any;
|
||||
|
||||
switch (name) {
|
||||
// CRUD
|
||||
case 'task_add':
|
||||
result = await taskAdd({
|
||||
title: a.title,
|
||||
project: a.project,
|
||||
type: a.type,
|
||||
priority: a.priority,
|
||||
description: a.description,
|
||||
planning_mode_required: a.planning_mode_required,
|
||||
});
|
||||
break;
|
||||
case 'task_list':
|
||||
result = await taskList({
|
||||
project: a.project,
|
||||
status: a.status,
|
||||
type: a.type,
|
||||
priority: a.priority,
|
||||
limit: a.limit,
|
||||
});
|
||||
break;
|
||||
case 'task_show':
|
||||
result = await taskShow(a.id);
|
||||
break;
|
||||
case 'task_close':
|
||||
result = await taskClose(a.id);
|
||||
break;
|
||||
case 'task_update':
|
||||
result = await taskUpdate({
|
||||
id: a.id,
|
||||
status: a.status,
|
||||
priority: a.priority,
|
||||
type: a.type,
|
||||
title: a.title,
|
||||
planning_mode_required: a.planning_mode_required,
|
||||
});
|
||||
break;
|
||||
case 'task_investigate':
|
||||
result = await taskInvestigate({
|
||||
title: a.title,
|
||||
project: a.project,
|
||||
priority: a.priority,
|
||||
description: a.description,
|
||||
});
|
||||
break;
|
||||
case 'task_move_project':
|
||||
result = await taskMoveProject({
|
||||
id: a.id,
|
||||
target_project: a.target_project,
|
||||
reason: a.reason,
|
||||
});
|
||||
break;
|
||||
|
||||
// Search
|
||||
case 'task_similar':
|
||||
result = await taskSimilar({
|
||||
query: a.query,
|
||||
project: a.project,
|
||||
limit: a.limit,
|
||||
});
|
||||
break;
|
||||
case 'task_context':
|
||||
result = await taskContext({
|
||||
description: a.description,
|
||||
project: a.project,
|
||||
limit: a.limit,
|
||||
});
|
||||
break;
|
||||
case 'task_session_context':
|
||||
result = await taskSessionContext({
|
||||
id: a.id,
|
||||
});
|
||||
break;
|
||||
|
||||
// Relations
|
||||
case 'task_link':
|
||||
result = await taskLink({
|
||||
from_id: a.from_id,
|
||||
to_id: a.to_id,
|
||||
link_type: a.link_type,
|
||||
});
|
||||
break;
|
||||
case 'task_checklist_add':
|
||||
result = await checklistAdd({
|
||||
task_id: a.task_id,
|
||||
item: a.item,
|
||||
});
|
||||
break;
|
||||
case 'task_checklist_toggle':
|
||||
result = await checklistToggle({
|
||||
item_id: a.item_id,
|
||||
checked: a.checked,
|
||||
});
|
||||
break;
|
||||
case 'task_resolve_duplicate':
|
||||
result = await taskResolveDuplicate({
|
||||
duplicate_id: a.duplicate_id,
|
||||
dominant_id: a.dominant_id,
|
||||
});
|
||||
break;
|
||||
|
||||
// Epics
|
||||
case 'epic_add':
|
||||
result = await epicAdd({
|
||||
title: a.title,
|
||||
project: a.project,
|
||||
description: a.description,
|
||||
});
|
||||
break;
|
||||
case 'epic_list':
|
||||
result = await epicList({
|
||||
project: a.project,
|
||||
status: a.status,
|
||||
limit: a.limit,
|
||||
});
|
||||
break;
|
||||
case 'epic_show':
|
||||
result = await epicShow(a.id);
|
||||
break;
|
||||
case 'epic_assign':
|
||||
result = await epicAssign({
|
||||
task_id: a.task_id,
|
||||
epic_id: a.epic_id,
|
||||
});
|
||||
break;
|
||||
case 'epic_close':
|
||||
result = await epicClose(a.id);
|
||||
break;
|
||||
|
||||
// Delegations
|
||||
case 'task_delegations':
|
||||
result = await taskDelegations({ task_id: a.task_id });
|
||||
@@ -277,49 +142,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
result = await projectContext();
|
||||
break;
|
||||
|
||||
// Versions
|
||||
case 'version_add':
|
||||
result = await versionAdd({
|
||||
project: a.project,
|
||||
version: a.version,
|
||||
build_number: a.build_number,
|
||||
status: a.status,
|
||||
release_notes: a.release_notes,
|
||||
});
|
||||
break;
|
||||
case 'version_list':
|
||||
result = await versionList({
|
||||
project: a.project,
|
||||
status: a.status,
|
||||
limit: a.limit,
|
||||
});
|
||||
break;
|
||||
case 'version_show':
|
||||
result = await versionShow(a.id);
|
||||
break;
|
||||
case 'version_update':
|
||||
result = await versionUpdate({
|
||||
id: a.id,
|
||||
status: a.status,
|
||||
git_tag: a.git_tag,
|
||||
git_sha: a.git_sha,
|
||||
release_notes: a.release_notes,
|
||||
release_date: a.release_date,
|
||||
});
|
||||
break;
|
||||
case 'version_release':
|
||||
result = await versionRelease({
|
||||
id: a.id,
|
||||
git_tag: a.git_tag,
|
||||
});
|
||||
break;
|
||||
case 'version_assign_task':
|
||||
result = await versionAssignTask({
|
||||
task_id: a.task_id,
|
||||
version_id: a.version_id,
|
||||
});
|
||||
break;
|
||||
|
||||
// Commits
|
||||
case 'task_commit_add':
|
||||
result = await taskCommitAdd({
|
||||
@@ -484,6 +306,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
working_directory: a.working_directory,
|
||||
git_branch: a.git_branch,
|
||||
initial_prompt: a.initial_prompt,
|
||||
jira_issue_key: a.jira_issue_key,
|
||||
});
|
||||
break;
|
||||
case 'session_update':
|
||||
@@ -720,7 +543,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
|
||||
// Main entry point
|
||||
async function main() {
|
||||
// Set up cleanup
|
||||
process.on('SIGINT', async () => {
|
||||
await close();
|
||||
process.exit(0);
|
||||
@@ -731,23 +553,20 @@ async function main() {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start server FIRST - respond to MCP protocol immediately
|
||||
// This is critical: Claude Code sends initialize before we finish DB connection
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('task-mcp: Server started');
|
||||
console.error('session-mcp: Server started');
|
||||
|
||||
// Test database connection in background (lazy - will connect on first tool call anyway)
|
||||
testConnection().then((connected) => {
|
||||
if (connected) {
|
||||
console.error('task-mcp: Connected to database');
|
||||
console.error('session-mcp: Connected to database');
|
||||
} else {
|
||||
console.error('task-mcp: Warning - database not reachable, will retry on tool calls');
|
||||
console.error('session-mcp: Warning - database not reachable, will retry on tool calls');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('task-mcp: Fatal error:', error);
|
||||
console.error('session-mcp: Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,718 +0,0 @@
|
||||
// CRUD operations for tasks
|
||||
|
||||
import { query, queryOne, execute, getNextTaskId, getProjectKey, detectProjectFromCwd, getClient } from '../db.js';
|
||||
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||
import type { Task, ChecklistItem, TaskLink } from '../types.js';
|
||||
import { getRecentDelegations } from './delegations.js';
|
||||
import { getTaskCommits } from './commits.js';
|
||||
import { taskLink } from './relations.js';
|
||||
import { sessionNoteAdd } from './session-docs.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
/**
|
||||
* Get current session ID from environment or cache file
|
||||
*/
|
||||
export function getSessionId(): string {
|
||||
// Check environment first
|
||||
if (process.env.CLAUDE_SESSION_ID) {
|
||||
return process.env.CLAUDE_SESSION_ID;
|
||||
}
|
||||
|
||||
// Try to read from cache file (session-memory format)
|
||||
const cacheFile = path.join(os.homedir(), '.cache', 'session-memory', 'current_session');
|
||||
try {
|
||||
const sessionId = fs.readFileSync(cacheFile, 'utf-8').trim();
|
||||
if (sessionId) return sessionId;
|
||||
} catch {
|
||||
// File doesn't exist or can't be read
|
||||
}
|
||||
|
||||
// Generate a new session ID
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 15);
|
||||
return `session_${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record task activity for session tracking
|
||||
*/
|
||||
async function recordActivity(
|
||||
task_id: string,
|
||||
activity_type: 'created' | 'updated' | 'status_change' | 'closed',
|
||||
old_value?: string,
|
||||
new_value?: string
|
||||
): Promise<void> {
|
||||
const session_id = getSessionId();
|
||||
try {
|
||||
await execute(
|
||||
`INSERT INTO task_activity (task_id, session_id, activity_type, old_value, new_value)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[task_id, session_id, activity_type, old_value || null, new_value || null]
|
||||
);
|
||||
} catch {
|
||||
// Don't fail the main operation if activity tracking fails
|
||||
console.error('Failed to record task activity');
|
||||
}
|
||||
}
|
||||
|
||||
interface TaskAddArgs {
|
||||
title: string;
|
||||
project?: string;
|
||||
type?: string;
|
||||
priority?: string;
|
||||
description?: string;
|
||||
planning_mode_required?: boolean | null;
|
||||
}
|
||||
|
||||
interface TaskListArgs {
|
||||
project?: string;
|
||||
status?: string;
|
||||
type?: string;
|
||||
priority?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
interface TaskUpdateArgs {
|
||||
id: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
type?: string;
|
||||
title?: string;
|
||||
planning_mode_required?: boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task
|
||||
*/
|
||||
export async function taskAdd(args: TaskAddArgs): Promise<string> {
|
||||
const { title, project = 'Unknown', type = 'task', priority = 'P2', description = '', planning_mode_required } = args;
|
||||
|
||||
// Get project key
|
||||
const projectKey = await getProjectKey(project);
|
||||
|
||||
// Generate embedding for duplicate detection
|
||||
const embedText = description ? `${title}. ${description}` : title;
|
||||
const embedding = await getEmbedding(embedText);
|
||||
const embeddingValue = embedding ? formatEmbedding(embedding) : null;
|
||||
|
||||
// Check for similar/duplicate tasks (only if embedding succeeded)
|
||||
// CF-450: Check both open AND completed tasks to avoid circular work
|
||||
let duplicateWarning = '';
|
||||
if (embeddingValue) {
|
||||
const similarTasks = await query<{ id: string; title: string; status: string; description: string; similarity: number }>(
|
||||
`SELECT id, title, status, description, 1 - (embedding <=> $1) as similarity
|
||||
FROM tasks
|
||||
WHERE project = $2 AND embedding IS NOT NULL
|
||||
ORDER BY embedding <=> $1
|
||||
LIMIT 5`,
|
||||
[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';
|
||||
|
||||
const openTasks = highSimilarity.filter(t => t.status !== 'completed');
|
||||
const completedTasks = highSimilarity.filter(t => t.status === 'completed');
|
||||
|
||||
if (openTasks.length > 0) {
|
||||
duplicateWarning += '\n**Open/In Progress:**\n';
|
||||
for (const t of openTasks) {
|
||||
const pct = Math.round(t.similarity * 100);
|
||||
duplicateWarning += ` - ${t.id}: ${t.title} (${pct}% match, ${t.status})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (completedTasks.length > 0) {
|
||||
duplicateWarning += '\n**Previously Completed:**\n';
|
||||
for (const t of completedTasks) {
|
||||
const pct = Math.round(t.similarity * 100);
|
||||
duplicateWarning += ` - ${t.id}: ${t.title} (${pct}% match)\n`;
|
||||
|
||||
// Show snippet of solution/outcome from description
|
||||
if (t.description) {
|
||||
const snippet = t.description.substring(0, 150).replace(/\n/g, ' ').replace(/"/g, '\\"');
|
||||
const ellipsis = t.description.length > 150 ? '...' : '';
|
||||
duplicateWarning += ` Context: "${snippet}${ellipsis}"\n`;
|
||||
}
|
||||
}
|
||||
duplicateWarning += '\n 💡 Use "task show <id>" to see full solution before recreating work\n';
|
||||
}
|
||||
|
||||
duplicateWarning += '\nConsider linking with: task link <new-id> <related-id> relates_to';
|
||||
}
|
||||
}
|
||||
|
||||
// Get next task ID
|
||||
const taskId = await getNextTaskId(projectKey);
|
||||
|
||||
// Get current session ID for linking
|
||||
const session_id = getSessionId();
|
||||
|
||||
// Insert task with session_id
|
||||
const planningValue = planning_mode_required !== undefined ? planning_mode_required : null;
|
||||
|
||||
if (embeddingValue) {
|
||||
await execute(
|
||||
`INSERT INTO tasks (id, project, title, description, type, status, priority, session_id, embedding, planning_mode_required)
|
||||
VALUES ($1, $2, $3, $4, $5, 'open', $6, $7, $8, $9)`,
|
||||
[taskId, projectKey, title, description, type, priority, session_id, embeddingValue, planningValue]
|
||||
);
|
||||
} else {
|
||||
await execute(
|
||||
`INSERT INTO tasks (id, project, title, description, type, status, priority, session_id, planning_mode_required)
|
||||
VALUES ($1, $2, $3, $4, $5, 'open', $6, $7, $8)`,
|
||||
[taskId, projectKey, title, description, type, priority, session_id, planningValue]
|
||||
);
|
||||
}
|
||||
|
||||
// Record activity for session tracking
|
||||
await recordActivity(taskId, 'created', undefined, 'open');
|
||||
|
||||
// CF-572 Phase 3: Auto-capture conversation context as session note
|
||||
// Ensures task context is preserved even if session exits abnormally
|
||||
if (session_id) {
|
||||
try {
|
||||
const contextNote = description
|
||||
? `Created task: ${title}\n\nDescription:\n${description}`
|
||||
: `Created task: ${title}`;
|
||||
|
||||
await sessionNoteAdd({
|
||||
session_id,
|
||||
note_type: 'context',
|
||||
content: contextNote,
|
||||
});
|
||||
} catch (err) {
|
||||
// Silently fail context capture - don't block task creation
|
||||
console.error('Failed to capture task context for session:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced auto-linking logic (CF-166)
|
||||
let autoLinkMessage = '';
|
||||
try {
|
||||
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?.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 (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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* List tasks with filters
|
||||
* Auto-detects project from CWD if not explicitly provided
|
||||
*/
|
||||
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;
|
||||
|
||||
// Auto-detect project from CWD if not explicitly provided
|
||||
const effectiveProject = project || detectProjectFromCwd();
|
||||
if (effectiveProject) {
|
||||
const projectKey = await getProjectKey(effectiveProject);
|
||||
whereClause += ` AND project = $${paramIndex++}`;
|
||||
params.push(projectKey);
|
||||
}
|
||||
if (status) {
|
||||
whereClause += ` AND status = $${paramIndex++}`;
|
||||
params.push(status);
|
||||
} else {
|
||||
// Default: exclude completed tasks when no status filter provided
|
||||
whereClause += ` AND status != 'completed'`;
|
||||
}
|
||||
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${effectiveProject ? ` for project ${effectiveProject}` : ''}`;
|
||||
}
|
||||
|
||||
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${effectiveProject ? ` (${effectiveProject})` : ''}:\n\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show task details
|
||||
*/
|
||||
export async function taskShow(id: string): Promise<string> {
|
||||
const task = await queryOne<Task & { session_id?: string }>(
|
||||
`SELECT id, project, title, description, type, status, priority, session_id, planning_mode_required,
|
||||
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.session_id) {
|
||||
output += `**Created in session:** ${task.session_id}\n`;
|
||||
}
|
||||
|
||||
const planningLabel = task.planning_mode_required === true ? 'required' : task.planning_mode_required === false ? 'skipped' : 'auto-detect';
|
||||
output += `**Planning:** ${planningLabel}\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 commits
|
||||
const commitHistory = await getTaskCommits(id);
|
||||
if (commitHistory) {
|
||||
output += commitHistory;
|
||||
}
|
||||
|
||||
// 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> {
|
||||
// Get current status for activity tracking
|
||||
const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]);
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
// Record activity
|
||||
await recordActivity(id, 'closed', task?.status, 'completed');
|
||||
|
||||
return `Closed: ${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task
|
||||
*/
|
||||
export async function taskUpdate(args: TaskUpdateArgs): Promise<string> {
|
||||
const { id, status, priority, type, title, planning_mode_required } = args;
|
||||
|
||||
// Get current values for activity tracking
|
||||
const task = await queryOne<{ status: string }>(`SELECT status FROM tasks WHERE id = $1`, [id]);
|
||||
if (!task) {
|
||||
return `Task not found: ${id}`;
|
||||
}
|
||||
|
||||
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 (planning_mode_required !== undefined) {
|
||||
updates.push(`planning_mode_required = $${paramIndex++}`);
|
||||
params.push(planning_mode_required);
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
// Record activity
|
||||
if (status && status !== task.status) {
|
||||
await recordActivity(id, 'status_change', task.status, status);
|
||||
} else {
|
||||
await recordActivity(id, 'updated');
|
||||
}
|
||||
|
||||
// Manage session context based on status changes
|
||||
if (status) {
|
||||
const session_id = getSessionId();
|
||||
try {
|
||||
if (status === 'in_progress') {
|
||||
// Set as current working task
|
||||
await execute(
|
||||
`INSERT INTO session_context (session_id, current_task_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (session_id) DO UPDATE SET current_task_id = $2, updated_at = NOW()`,
|
||||
[session_id, id]
|
||||
);
|
||||
} else if (status === 'completed') {
|
||||
// Clear if this is the current working task
|
||||
await execute(
|
||||
`DELETE FROM session_context
|
||||
WHERE session_id = $1 AND current_task_id = $2`,
|
||||
[session_id, id]
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Silently fail if session context unavailable
|
||||
}
|
||||
}
|
||||
|
||||
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.';
|
||||
}
|
||||
|
||||
interface TaskMoveProjectArgs {
|
||||
id: string;
|
||||
target_project: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move task to different project while preserving history (CF-301)
|
||||
* Creates new task with next ID in target project and transfers all related data
|
||||
*/
|
||||
export async function taskMoveProject(args: TaskMoveProjectArgs): Promise<string> {
|
||||
const { id, target_project, reason } = args;
|
||||
|
||||
// Validate source task exists
|
||||
const task = await queryOne<{ project: string; status: string }>(
|
||||
`SELECT project, status FROM tasks WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (!task) {
|
||||
return `Task not found: ${id}`;
|
||||
}
|
||||
|
||||
if (task.project === target_project) {
|
||||
return `Task ${id} is already in project ${target_project}`;
|
||||
}
|
||||
|
||||
// Validate target project exists
|
||||
const targetProj = await queryOne<{ key: string }>(
|
||||
`SELECT key FROM projects WHERE key = $1`,
|
||||
[target_project]
|
||||
);
|
||||
if (!targetProj) {
|
||||
return `Target project not found: ${target_project}`;
|
||||
}
|
||||
|
||||
// Generate new ID using getNextTaskId
|
||||
const newId = await getNextTaskId(target_project);
|
||||
|
||||
// Execute move in transaction
|
||||
const client = await getClient();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Insert new task (copy of old)
|
||||
await client.query(`
|
||||
INSERT INTO tasks (id, project, title, description, type, status, priority,
|
||||
version_id, epic_id, embedding, created_at, updated_at,
|
||||
completed_at, session_id)
|
||||
SELECT $1, $2, title, description, type, status, priority,
|
||||
version_id, epic_id, embedding, created_at, NOW(), completed_at, session_id
|
||||
FROM tasks WHERE id = $3
|
||||
`, [newId, target_project, id]);
|
||||
|
||||
// Transfer all related records
|
||||
const transfers = [
|
||||
`UPDATE task_checklist SET task_id = $1 WHERE task_id = $2`,
|
||||
`UPDATE task_commits SET task_id = $1 WHERE task_id = $2`,
|
||||
`UPDATE task_delegations SET task_id = $1 WHERE task_id = $2`,
|
||||
`UPDATE task_activity SET task_id = $1 WHERE task_id = $2`,
|
||||
`UPDATE task_links SET from_task_id = $1 WHERE from_task_id = $2`,
|
||||
`UPDATE task_links SET to_task_id = $1 WHERE to_task_id = $2`,
|
||||
`UPDATE deployments SET task_id = $1 WHERE task_id = $2`,
|
||||
`UPDATE memories SET task_id = $1 WHERE task_id = $2`,
|
||||
`UPDATE session_context SET current_task_id = $1 WHERE current_task_id = $2`,
|
||||
`UPDATE session_context SET investigation_parent_id = $1 WHERE investigation_parent_id = $2`,
|
||||
`UPDATE task_learning_effectiveness SET task_id = $1 WHERE task_id = $2`,
|
||||
];
|
||||
|
||||
for (const sql of transfers) {
|
||||
await client.query(sql, [newId, id]);
|
||||
}
|
||||
|
||||
// Record activity
|
||||
await client.query(`
|
||||
INSERT INTO task_activity (task_id, activity_type, old_value, new_value, note, created_at)
|
||||
VALUES ($1, 'project_moved', $2, $3, $4, NOW())
|
||||
`, [newId, task.project, target_project, reason || 'Moved via task_move_project']);
|
||||
|
||||
// Update old task
|
||||
await client.query(`
|
||||
UPDATE tasks
|
||||
SET status = 'completed',
|
||||
completed_at = NOW(),
|
||||
updated_at = NOW(),
|
||||
description = COALESCE(description, '') || $1
|
||||
WHERE id = $2
|
||||
`, [`\n\n---\n**Moved to ${newId}**${reason ? ` (Reason: ${reason})` : ''}`, id]);
|
||||
|
||||
// Add duplicate link
|
||||
await client.query(`
|
||||
INSERT INTO task_links (from_task_id, to_task_id, link_type, created_at)
|
||||
VALUES ($1, $2, 'duplicates', NOW())
|
||||
`, [id, newId]);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return `Moved ${id} → ${newId} (project: ${task.project} → ${target_project})`;
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
// Epic operations for task management
|
||||
|
||||
import { query, queryOne, execute, getProjectKey, detectProjectFromCwd } from '../db.js';
|
||||
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||
import type { Epic, Task } from '../types.js';
|
||||
|
||||
interface EpicAddArgs {
|
||||
title: string;
|
||||
project?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface EpicListArgs {
|
||||
project?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
interface EpicAssignArgs {
|
||||
task_id: string;
|
||||
epic_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next epic ID for a project
|
||||
*/
|
||||
async function getNextEpicId(projectKey: string): Promise<string> {
|
||||
const result = await queryOne<{ next_id: number }>(
|
||||
`INSERT INTO epic_sequences (project, next_id) VALUES ($1, 1)
|
||||
ON CONFLICT (project) DO UPDATE SET next_id = epic_sequences.next_id + 1
|
||||
RETURNING next_id`,
|
||||
[projectKey]
|
||||
);
|
||||
return `${projectKey}-E${result?.next_id || 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new epic
|
||||
*/
|
||||
export async function epicAdd(args: EpicAddArgs): Promise<string> {
|
||||
const { title, project = 'Unknown', description = '' } = args;
|
||||
|
||||
// Get project key
|
||||
const projectKey = await getProjectKey(project);
|
||||
|
||||
// Get next epic ID
|
||||
const epicId = await getNextEpicId(projectKey);
|
||||
|
||||
// Generate embedding
|
||||
const embedText = description ? `${title}. ${description}` : title;
|
||||
const embedding = await getEmbedding(embedText);
|
||||
const embeddingValue = embedding ? formatEmbedding(embedding) : null;
|
||||
|
||||
// Insert epic
|
||||
if (embeddingValue) {
|
||||
await execute(
|
||||
`INSERT INTO epics (id, project, title, description, embedding)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[epicId, projectKey, title, description, embeddingValue]
|
||||
);
|
||||
} else {
|
||||
await execute(
|
||||
`INSERT INTO epics (id, project, title, description)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[epicId, projectKey, title, description]
|
||||
);
|
||||
}
|
||||
|
||||
return `Created epic: ${epicId}\n Title: ${title}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* List epics with filters
|
||||
* Auto-detects project from CWD if not explicitly provided
|
||||
*/
|
||||
export async function epicList(args: EpicListArgs): Promise<string> {
|
||||
const { project, status, limit = 20 } = args;
|
||||
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Auto-detect project from CWD if not explicitly provided
|
||||
const effectiveProject = project || detectProjectFromCwd();
|
||||
if (effectiveProject) {
|
||||
const projectKey = await getProjectKey(effectiveProject);
|
||||
whereClause += ` AND e.project = $${paramIndex++}`;
|
||||
params.push(projectKey);
|
||||
}
|
||||
if (status) {
|
||||
whereClause += ` AND e.status = $${paramIndex++}`;
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
params.push(limit);
|
||||
|
||||
const epics = await query<Epic & { task_count: number; open_count: number }>(
|
||||
`SELECT e.id, e.title, e.status, e.project,
|
||||
COUNT(t.id) as task_count,
|
||||
COUNT(t.id) FILTER (WHERE t.status != 'completed') as open_count
|
||||
FROM epics e
|
||||
LEFT JOIN tasks t ON t.epic_id = e.id
|
||||
${whereClause}
|
||||
GROUP BY e.id, e.title, e.status, e.project, e.created_at
|
||||
ORDER BY
|
||||
CASE e.status WHEN 'in_progress' THEN 0 WHEN 'open' THEN 1 ELSE 2 END,
|
||||
e.created_at DESC
|
||||
LIMIT $${paramIndex}`,
|
||||
params
|
||||
);
|
||||
|
||||
if (epics.length === 0) {
|
||||
return `No epics found${effectiveProject ? ` for project ${effectiveProject}` : ''}`;
|
||||
}
|
||||
|
||||
const lines = epics.map(e => {
|
||||
const statusIcon = e.status === 'completed' ? '[x]' : e.status === 'in_progress' ? '[>]' : '[ ]';
|
||||
const progress = e.task_count > 0 ? ` (${e.task_count - e.open_count}/${e.task_count} done)` : '';
|
||||
return `${statusIcon} ${e.id}: ${e.title}${progress}`;
|
||||
});
|
||||
|
||||
return `Epics${effectiveProject ? ` (${effectiveProject})` : ''}:\n\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show epic details with tasks
|
||||
*/
|
||||
export async function epicShow(id: string): Promise<string> {
|
||||
const epic = await queryOne<Epic & { created: string }>(
|
||||
`SELECT id, project, title, description, status,
|
||||
to_char(created_at, 'YYYY-MM-DD HH24:MI') as created
|
||||
FROM epics WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!epic) {
|
||||
return `Epic not found: ${id}`;
|
||||
}
|
||||
|
||||
let output = `# ${epic.id}\n\n`;
|
||||
output += `**Title:** ${epic.title}\n`;
|
||||
output += `**Project:** ${epic.project}\n`;
|
||||
output += `**Status:** ${epic.status}\n`;
|
||||
output += `**Created:** ${epic.created}\n`;
|
||||
|
||||
if (epic.description) {
|
||||
output += `\n**Description:**\n${epic.description}\n`;
|
||||
}
|
||||
|
||||
// Get tasks in this epic
|
||||
const tasks = await query<Task>(
|
||||
`SELECT id, title, status, priority, type
|
||||
FROM tasks
|
||||
WHERE epic_id = $1
|
||||
ORDER BY
|
||||
CASE status WHEN 'in_progress' THEN 0 WHEN 'open' THEN 1 WHEN 'blocked' THEN 2 ELSE 3 END,
|
||||
CASE priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (tasks.length > 0) {
|
||||
const done = tasks.filter(t => t.status === 'completed').length;
|
||||
output += `\n**Tasks:** (${done}/${tasks.length} done)\n`;
|
||||
for (const t of tasks) {
|
||||
const statusIcon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[>]' : t.status === 'blocked' ? '[!]' : '[ ]';
|
||||
output += ` ${statusIcon} ${t.priority} ${t.id}: ${t.title}\n`;
|
||||
}
|
||||
} else {
|
||||
output += `\n**Tasks:** None assigned\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a task to an epic
|
||||
*/
|
||||
export async function epicAssign(args: EpicAssignArgs): Promise<string> {
|
||||
const { task_id, epic_id } = args;
|
||||
|
||||
// Verify epic exists
|
||||
const epic = await queryOne<{ id: string }>(`SELECT id FROM epics WHERE id = $1`, [epic_id]);
|
||||
if (!epic) {
|
||||
return `Epic not found: ${epic_id}`;
|
||||
}
|
||||
|
||||
// Update task
|
||||
const result = await execute(
|
||||
`UPDATE tasks SET epic_id = $1, updated_at = NOW() WHERE id = $2`,
|
||||
[epic_id, task_id]
|
||||
);
|
||||
|
||||
if (result === 0) {
|
||||
return `Task not found: ${task_id}`;
|
||||
}
|
||||
|
||||
return `Assigned ${task_id} to epic ${epic_id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign a task from its epic
|
||||
*/
|
||||
export async function epicUnassign(task_id: string): Promise<string> {
|
||||
const result = await execute(
|
||||
`UPDATE tasks SET epic_id = NULL, updated_at = NOW() WHERE id = $1`,
|
||||
[task_id]
|
||||
);
|
||||
|
||||
if (result === 0) {
|
||||
return `Task not found: ${task_id}`;
|
||||
}
|
||||
|
||||
return `Unassigned ${task_id} from its epic`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close an epic (mark as completed)
|
||||
*/
|
||||
export async function epicClose(id: string): Promise<string> {
|
||||
// Verify epic exists
|
||||
const epic = await queryOne<{ id: string; title: string; status: string }>(
|
||||
`SELECT id, title, status FROM epics WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!epic) {
|
||||
return `Epic not found: ${id}`;
|
||||
}
|
||||
|
||||
if (epic.status === 'completed') {
|
||||
return `Epic already completed: ${id}`;
|
||||
}
|
||||
|
||||
// Update epic status
|
||||
await execute(
|
||||
`UPDATE epics SET status = 'completed', updated_at = NOW() WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
return `Closed: ${id} (${epic.title})`;
|
||||
}
|
||||
@@ -1,342 +1,16 @@
|
||||
// Tool definitions for task-mcp
|
||||
// Tool definitions for session-mcp
|
||||
// Forked from task-mcp (CF-762): Removed task/epic/version/search/relations tools
|
||||
// Those are now handled by Jira Cloud via mcp-atlassian
|
||||
|
||||
export const toolDefinitions = [
|
||||
// CRUD Tools
|
||||
{
|
||||
name: 'task_add',
|
||||
description: 'Create a new task with auto-generated ID and semantic embedding',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Task title (required)' },
|
||||
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', 'investigation'], description: 'Task type (default: task)' },
|
||||
priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'], description: 'Priority level (default: P2)' },
|
||||
description: { type: 'string', description: 'Optional description' },
|
||||
planning_mode_required: { type: 'boolean', description: 'Override planning mode: true=always plan, false=never plan, omit=auto-detect' },
|
||||
},
|
||||
required: ['title'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_list',
|
||||
description: 'List tasks with optional filters',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Filter by project key' },
|
||||
status: { type: 'string', enum: ['open', 'in_progress', 'testing', 'blocked', 'completed'], description: 'Filter by exact status value. IMPORTANT: "open" only matches status=open, NOT in_progress/blocked/testing. Omit this parameter entirely to show ALL non-completed tasks.' },
|
||||
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)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_show',
|
||||
description: 'Show task details including checklist and dependencies',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Task ID (e.g., ST-1, VPN-45)' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_close',
|
||||
description: 'Mark a task as completed',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Task ID to close' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_update',
|
||||
description: 'Update task fields (status, priority, type, title)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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', 'investigation'], description: 'New type' },
|
||||
title: { type: 'string', description: 'New title' },
|
||||
planning_mode_required: { type: 'boolean', description: 'Override planning mode: true=always plan, false=never plan, null=auto-detect' },
|
||||
},
|
||||
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'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_move_project',
|
||||
description: 'Move task to different project while preserving history',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Task ID to move (e.g., CF-295)' },
|
||||
target_project: { type: 'string', description: 'Target project key (e.g., VPN, ST, GB)' },
|
||||
reason: { type: 'string', description: 'Optional reason for move' },
|
||||
},
|
||||
required: ['id', 'target_project'],
|
||||
},
|
||||
},
|
||||
|
||||
// Semantic Search Tools
|
||||
{
|
||||
name: 'task_similar',
|
||||
description: 'Find semantically similar tasks using pgvector',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
project: { type: 'string', description: 'Filter by project (optional)' },
|
||||
limit: { type: 'number', description: 'Max results (default: 5)' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_context',
|
||||
description: 'Get related tasks for current work context (useful for delegations)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
description: { type: 'string', description: 'Description of current work' },
|
||||
project: { type: 'string', description: 'Current project' },
|
||||
limit: { type: 'number', description: 'Max related tasks (default: 3)' },
|
||||
},
|
||||
required: ['description'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_session_context',
|
||||
description: 'Get session context for a task - retrieves notes, decisions, and related tasks from the session where the task was created. Use this to understand the original context and requirements.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Task ID (e.g., CF-570)' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
|
||||
// Relation Tools
|
||||
{
|
||||
name: 'task_link',
|
||||
description: 'Create dependency between tasks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from_id: { type: 'string', description: 'Source task ID' },
|
||||
to_id: { type: 'string', description: 'Target task ID' },
|
||||
link_type: { type: 'string', enum: ['blocks', 'relates_to', 'duplicates', 'depends_on', 'needs', 'implements', 'fixes', 'causes', 'subtask_of'], description: 'Relationship type' },
|
||||
},
|
||||
required: ['from_id', 'to_id', 'link_type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_checklist_add',
|
||||
description: 'Add a checklist item to a task',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: { type: 'string', description: 'Task ID' },
|
||||
item: { type: 'string', description: 'Checklist item text' },
|
||||
},
|
||||
required: ['task_id', 'item'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_checklist_toggle',
|
||||
description: 'Toggle a checklist item (check/uncheck)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
item_id: { type: 'number', description: 'Checklist item ID' },
|
||||
checked: { type: 'boolean', description: 'New checked state' },
|
||||
},
|
||||
required: ['item_id', 'checked'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_resolve_duplicate',
|
||||
description: 'Resolve a duplicate issue by closing it and linking to the dominant issue',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
duplicate_id: { type: 'string', description: 'The duplicate task ID to close' },
|
||||
dominant_id: { type: 'string', description: 'The dominant/original task ID to keep' },
|
||||
},
|
||||
required: ['duplicate_id', 'dominant_id'],
|
||||
},
|
||||
},
|
||||
|
||||
// Epic Tools
|
||||
{
|
||||
name: 'epic_add',
|
||||
description: 'Create a new epic (session-scoped work bundle) with auto-generated ID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Epic title (required)' },
|
||||
project: { type: 'string', description: 'Project key (e.g., VPN, ST). Auto-detected if not provided.' },
|
||||
description: { type: 'string', description: 'Optional description of the epic scope' },
|
||||
},
|
||||
required: ['title'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'epic_list',
|
||||
description: 'List epics with task counts and progress',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Filter by project key' },
|
||||
status: { type: 'string', enum: ['open', 'in_progress', 'completed'], description: 'Filter by status' },
|
||||
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'epic_show',
|
||||
description: 'Show epic details with all assigned tasks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Epic ID (e.g., VPN-E1, ST-E3)' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'epic_assign',
|
||||
description: 'Assign a task to an epic',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: { type: 'string', description: 'Task ID to assign' },
|
||||
epic_id: { type: 'string', description: 'Epic ID to assign to' },
|
||||
},
|
||||
required: ['task_id', 'epic_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'epic_close',
|
||||
description: 'Close an epic (mark as completed)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Epic ID to close (e.g., VPN-E1, ST-E3)' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
|
||||
// Version Tools
|
||||
{
|
||||
name: 'version_add',
|
||||
description: 'Create a new version/release for a project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Project key (e.g., VPN, ST)' },
|
||||
version: { type: 'string', description: 'Version number (e.g., 1.0.0, 2.1.0-beta)' },
|
||||
build_number: { type: 'number', description: 'Optional build number' },
|
||||
status: { type: 'string', enum: ['planned', 'in_progress', 'released', 'archived'], description: 'Version status (default: planned)' },
|
||||
release_notes: { type: 'string', description: 'Optional release notes' },
|
||||
},
|
||||
required: ['project', 'version'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'version_list',
|
||||
description: 'List versions with optional filters',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Filter by project key' },
|
||||
status: { type: 'string', enum: ['planned', 'in_progress', 'released', 'archived'], description: 'Filter by status' },
|
||||
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'version_show',
|
||||
description: 'Show version details with assigned tasks and epics',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Version ID (e.g., VPN-v1.0.0)' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'version_update',
|
||||
description: 'Update version fields (status, git_tag, git_sha, release_notes)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Version ID to update' },
|
||||
status: { type: 'string', enum: ['planned', 'in_progress', 'released', 'archived'], description: 'New status' },
|
||||
git_tag: { type: 'string', description: 'Git tag name (e.g., v1.0.0)' },
|
||||
git_sha: { type: 'string', description: 'Git commit SHA for this version' },
|
||||
release_notes: { type: 'string', description: 'Release notes' },
|
||||
release_date: { type: 'string', description: 'Release date (ISO format)' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'version_release',
|
||||
description: 'Mark a version as released (sets status and release_date)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Version ID to release' },
|
||||
git_tag: { type: 'string', description: 'Optional git tag to associate' },
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'version_assign_task',
|
||||
description: 'Assign a task to a version',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: { type: 'string', description: 'Task ID to assign' },
|
||||
version_id: { type: 'string', description: 'Version ID to assign to' },
|
||||
},
|
||||
required: ['task_id', 'version_id'],
|
||||
},
|
||||
},
|
||||
|
||||
// Delegation Tools
|
||||
// Delegation Tools (kept for tracking code generation jobs)
|
||||
{
|
||||
name: 'task_delegations',
|
||||
description: 'List delegations for a specific task (quality scores, backends, status)',
|
||||
description: 'List delegations for a specific Jira issue (quality scores, backends, status)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: { type: 'string', description: 'Task ID (e.g., ST-123)' },
|
||||
task_id: { type: 'string', description: 'Jira issue key (e.g., CF-123)' },
|
||||
},
|
||||
required: ['task_id'],
|
||||
},
|
||||
@@ -354,14 +28,14 @@ export const toolDefinitions = [
|
||||
},
|
||||
},
|
||||
|
||||
// Commit Tools
|
||||
// Commit Tools (kept for git-session linking)
|
||||
{
|
||||
name: 'task_commit_add',
|
||||
description: 'Link a git commit to a task (SHA reference only, Gitea MCP has full commit data)',
|
||||
description: 'Link a git commit to a Jira issue key (SHA reference only)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: { type: 'string', description: 'Task ID (e.g., VPN-123)' },
|
||||
task_id: { type: 'string', description: 'Jira issue key (e.g., CF-123)' },
|
||||
commit_sha: { type: 'string', description: 'Git commit SHA (full or short)' },
|
||||
repo: { type: 'string', description: 'Repository (e.g., christian/VPN)' },
|
||||
source: { type: 'string', enum: ['manual', 'parsed', 'pr_merge'], description: 'How the link was created (default: manual)' },
|
||||
@@ -371,11 +45,11 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'task_commit_remove',
|
||||
description: 'Remove a commit link from a task',
|
||||
description: 'Remove a commit link from a Jira issue',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: { type: 'string', description: 'Task ID' },
|
||||
task_id: { type: 'string', description: 'Jira issue key' },
|
||||
commit_sha: { type: 'string', description: 'Commit SHA to unlink' },
|
||||
},
|
||||
required: ['task_id', 'commit_sha'],
|
||||
@@ -383,18 +57,18 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'task_commits_list',
|
||||
description: 'List commits linked to a task',
|
||||
description: 'List commits linked to a Jira issue',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
task_id: { type: 'string', description: 'Task ID' },
|
||||
task_id: { type: 'string', description: 'Jira issue key' },
|
||||
},
|
||||
required: ['task_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'task_link_commits',
|
||||
description: 'Parse commit messages for task references and create links (batch operation)',
|
||||
description: 'Parse commit messages for Jira issue references and create links (batch operation)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -418,11 +92,11 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_tasks',
|
||||
description: 'List tasks worked on in a session (from task_activity tracking)',
|
||||
description: 'List Jira issues worked on in a session (from task_activity tracking)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
session_id: { type: 'string', description: 'Session ID (supports * wildcard, e.g., session_20260110_*)' },
|
||||
session_id: { type: 'string', description: 'Session ID (supports * wildcard)' },
|
||||
limit: { type: 'number', description: 'Max results (default: 20)' },
|
||||
},
|
||||
required: ['session_id'],
|
||||
@@ -442,7 +116,7 @@ export const toolDefinitions = [
|
||||
impact: { type: 'string', description: 'Effects on existing infrastructure' },
|
||||
actions_required: { type: 'string', description: 'Steps developers need to take (optional)' },
|
||||
session_id: { type: 'string', description: 'Session that implemented change (optional)' },
|
||||
task_ids: { type: 'array', items: { type: 'string' }, description: 'Related task IDs (optional)' },
|
||||
task_ids: { type: 'array', items: { type: 'string' }, description: 'Related Jira issue keys (optional)' },
|
||||
},
|
||||
required: ['date', 'title', 'change_description', 'impact'],
|
||||
},
|
||||
@@ -460,7 +134,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'changelog_list',
|
||||
description: 'List recent infrastructure changes by time period (fallback)',
|
||||
description: 'List recent infrastructure changes by time period',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -473,7 +147,7 @@ export const toolDefinitions = [
|
||||
// Project Lock Tools
|
||||
{
|
||||
name: 'project_lock',
|
||||
description: 'Lock a project for exclusive session access. Prevents other sessions from working on it.',
|
||||
description: 'Lock a project for exclusive session access.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -510,7 +184,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'project_context',
|
||||
description: 'Get project context from current directory - returns detected project, open tasks, epics, and lock status. Use at session start.',
|
||||
description: 'Get project context from current directory - returns detected project, lock status, recent sessions.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -524,11 +198,11 @@ export const toolDefinitions = [
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Unique component ID (e.g., propertymap-scraper, gridbot-conductor)' },
|
||||
id: { type: 'string', description: 'Unique component ID' },
|
||||
name: { type: 'string', description: 'Human-readable name' },
|
||||
type: { type: 'string', enum: ['service', 'script', 'config', 'database', 'api', 'ui', 'library'], description: 'Component type' },
|
||||
path: { type: 'string', description: 'File system path or Docker container name' },
|
||||
repo: { type: 'string', description: 'Git repository (e.g., christian/propertymap)' },
|
||||
repo: { type: 'string', description: 'Git repository' },
|
||||
description: { type: 'string', description: 'What this component does' },
|
||||
health_check: { type: 'string', description: 'Command or URL to check health' },
|
||||
},
|
||||
@@ -552,7 +226,7 @@ export const toolDefinitions = [
|
||||
type: 'object',
|
||||
properties: {
|
||||
component_id: { type: 'string', description: 'Source component ID' },
|
||||
depends_on: { type: 'string', description: 'Target component ID (what source depends on)' },
|
||||
depends_on: { type: 'string', description: 'Target component ID' },
|
||||
dependency_type: { type: 'string', enum: ['hard', 'soft', 'config', 'data'], description: 'Type of dependency' },
|
||||
description: { type: 'string', description: 'Description of the dependency' },
|
||||
},
|
||||
@@ -566,7 +240,7 @@ export const toolDefinitions = [
|
||||
type: 'object',
|
||||
properties: {
|
||||
component_id: { type: 'string', description: 'Component ID' },
|
||||
file_pattern: { type: 'string', description: 'File pattern (e.g., src/services/*.py, docker-compose.yml)' },
|
||||
file_pattern: { type: 'string', description: 'File pattern (e.g., src/services/*.py)' },
|
||||
},
|
||||
required: ['component_id', 'file_pattern'],
|
||||
},
|
||||
@@ -578,7 +252,7 @@ export const toolDefinitions = [
|
||||
type: 'object',
|
||||
properties: {
|
||||
component_id: { type: 'string', description: 'Component ID' },
|
||||
name: { type: 'string', description: 'Check name (e.g., health-endpoint, container-running)' },
|
||||
name: { type: 'string', description: 'Check name' },
|
||||
check_type: { type: 'string', enum: ['command', 'http', 'tcp', 'file'], description: 'Type of check' },
|
||||
check_command: { type: 'string', description: 'Command/URL to execute' },
|
||||
expected_result: { type: 'string', description: 'Expected output or status' },
|
||||
@@ -593,33 +267,29 @@ export const toolDefinitions = [
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
changed_files: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of changed file paths',
|
||||
},
|
||||
changed_files: { type: 'array', items: { type: 'string' }, description: 'List of changed file paths' },
|
||||
},
|
||||
required: ['changed_files'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'impact_learn',
|
||||
description: 'Record a learned impact relationship (when we discover a missed dependency)',
|
||||
description: 'Record a learned impact relationship',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
changed_component: { type: 'string', description: 'Component that was changed' },
|
||||
affected_component: { type: 'string', description: 'Component that was unexpectedly affected' },
|
||||
impact_description: { type: 'string', description: 'What went wrong' },
|
||||
error_id: { type: 'string', description: 'Related error ID from error memory' },
|
||||
task_id: { type: 'string', description: 'Related task ID' },
|
||||
error_id: { type: 'string', description: 'Related error ID' },
|
||||
task_id: { type: 'string', description: 'Related Jira issue key' },
|
||||
},
|
||||
required: ['changed_component', 'affected_component', 'impact_description'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'component_graph',
|
||||
description: 'Get component dependency graph (for visualization)',
|
||||
description: 'Get component dependency graph',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -631,24 +301,24 @@ export const toolDefinitions = [
|
||||
// Memory Tools
|
||||
{
|
||||
name: 'memory_add',
|
||||
description: 'Store a learning/memory for future sessions. Use at session end to persist insights.',
|
||||
description: 'Store a learning/memory for future sessions.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: { type: 'string', enum: ['pattern', 'fix', 'preference', 'gotcha', 'architecture'], description: 'Memory category' },
|
||||
title: { type: 'string', description: 'Short title for the memory' },
|
||||
title: { type: 'string', description: 'Short title' },
|
||||
content: { type: 'string', description: 'The learning/insight to remember' },
|
||||
context: { type: 'string', description: 'When/where this applies (optional)' },
|
||||
project: { type: 'string', description: 'Project this relates to (optional)' },
|
||||
session_id: { type: 'string', description: 'Session ID to link memory to (optional)' },
|
||||
task_id: { type: 'string', description: 'Task ID to link memory to (optional)' },
|
||||
project: { type: 'string', description: 'Project (optional)' },
|
||||
session_id: { type: 'string', description: 'Session ID (optional)' },
|
||||
task_id: { type: 'string', description: 'Jira issue key (optional)' },
|
||||
},
|
||||
required: ['category', 'title', 'content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'memory_search',
|
||||
description: 'Search memories semantically. Returns relevant learnings for current context.',
|
||||
description: 'Search memories semantically.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -674,12 +344,12 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'memory_context',
|
||||
description: 'Get memories relevant to current session context. Use at session start.',
|
||||
description: 'Get memories relevant to current session context.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Current project' },
|
||||
task_description: { type: 'string', description: 'Description of planned work (for semantic matching)' },
|
||||
task_description: { type: 'string', description: 'Description of planned work' },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -694,10 +364,10 @@ export const toolDefinitions = [
|
||||
tool_name: { type: 'string', description: 'Tool or command name' },
|
||||
category: { type: 'string', enum: ['mcp', 'cli', 'script', 'internal', 'deprecated'], description: 'Tool category' },
|
||||
title: { type: 'string', description: 'Short descriptive title' },
|
||||
description: { type: 'string', description: 'Detailed description of what the tool does' },
|
||||
description: { type: 'string', description: 'Detailed description' },
|
||||
usage_example: { type: 'string', description: 'Usage example (optional)' },
|
||||
parameters: { type: 'object', description: 'Parameter definitions (optional)' },
|
||||
notes: { type: 'string', description: 'Additional notes, gotchas, tips (optional)' },
|
||||
notes: { type: 'string', description: 'Additional notes (optional)' },
|
||||
tags: { type: 'array', items: { type: 'string' }, description: 'Searchable tags (optional)' },
|
||||
source_file: { type: 'string', description: 'Original source file (optional)' },
|
||||
},
|
||||
@@ -743,7 +413,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'tool_doc_export',
|
||||
description: 'Export all tool documentation as markdown (for backup/migration)',
|
||||
description: 'Export all tool documentation as markdown',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -753,12 +423,13 @@ export const toolDefinitions = [
|
||||
// Session Management Tools
|
||||
{
|
||||
name: 'session_start',
|
||||
description: 'Start a new session with metadata tracking',
|
||||
description: 'Start a new session with metadata tracking. Links to Jira issue key.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
session_id: { type: 'string', description: 'Session ID (auto-generated if not provided)' },
|
||||
project: { type: 'string', description: 'Project key (e.g., CF, VPN)' },
|
||||
jira_issue_key: { type: 'string', description: 'Jira issue key being worked on (e.g., CF-123)' },
|
||||
working_directory: { type: 'string', description: 'Current working directory' },
|
||||
git_branch: { type: 'string', description: 'Current git branch' },
|
||||
initial_prompt: { type: 'string', description: 'First user message' },
|
||||
@@ -775,11 +446,7 @@ export const toolDefinitions = [
|
||||
session_id: { type: 'string', description: 'Session ID to update' },
|
||||
message_count: { type: 'number', description: 'Number of messages exchanged' },
|
||||
token_count: { type: 'number', description: 'Total tokens used' },
|
||||
tools_used: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of tool names used',
|
||||
},
|
||||
tools_used: { type: 'array', items: { type: 'string' }, description: 'Array of tool names used' },
|
||||
},
|
||||
required: ['session_id'],
|
||||
},
|
||||
@@ -825,7 +492,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_context',
|
||||
description: 'Get complete context: tasks, commits, builds, memories',
|
||||
description: 'Get complete context: Jira issues, commits, builds, memories',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -836,7 +503,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'build_record',
|
||||
description: 'Record build information linked to session and version',
|
||||
description: 'Record build information linked to session',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -852,13 +519,13 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_commit_link',
|
||||
description: 'Link a commit to a session (automatically called when commits are made)',
|
||||
description: 'Link a commit to a session',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
session_id: { type: 'string', description: 'Session ID' },
|
||||
commit_sha: { type: 'string', description: 'Git commit SHA' },
|
||||
repo: { type: 'string', description: 'Repository (e.g., christian/ClaudeFramework)' },
|
||||
repo: { type: 'string', description: 'Repository' },
|
||||
commit_message: { type: 'string', description: 'Commit message (optional)' },
|
||||
committed_at: { type: 'string', description: 'Commit timestamp (ISO format, optional)' },
|
||||
},
|
||||
@@ -867,7 +534,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_recover_orphaned',
|
||||
description: 'Recover abandoned/orphaned sessions (CF-572). Detects sessions active for >2 hours and marks as abandoned',
|
||||
description: 'Recover abandoned/orphaned sessions (active >2 hours)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -877,7 +544,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_recover_temp_notes',
|
||||
description: 'Recover notes from temp files for a specific session (CF-572)',
|
||||
description: 'Recover notes from temp files for a specific session',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -904,7 +571,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_notes_list',
|
||||
description: 'List all notes for a session, optionally filtered by type',
|
||||
description: 'List all notes for a session',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -922,7 +589,7 @@ export const toolDefinitions = [
|
||||
properties: {
|
||||
session_id: { type: 'string', description: 'Session ID' },
|
||||
plan_content: { type: 'string', description: 'Plan content in markdown' },
|
||||
plan_file_name: { type: 'string', description: 'Original filename (e.g., eloquent-yellow-cat.md) - optional' },
|
||||
plan_file_name: { type: 'string', description: 'Original filename (optional)' },
|
||||
status: { type: 'string', enum: ['draft', 'approved', 'executed', 'abandoned'], description: 'Plan status (default: draft)' },
|
||||
},
|
||||
required: ['session_id', 'plan_content'],
|
||||
@@ -954,11 +621,11 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'project_doc_upsert',
|
||||
description: 'Create or update project documentation (replaces CLAUDE.md sections)',
|
||||
description: 'Create or update project documentation',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Project key (e.g., CF, VPN)' },
|
||||
project: { type: 'string', description: 'Project key' },
|
||||
doc_type: { type: 'string', enum: ['overview', 'architecture', 'guidelines', 'history', 'configuration', 'workflow'], description: 'Documentation type' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
content: { type: 'string', description: 'Document content in markdown' },
|
||||
@@ -992,7 +659,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_documentation_generate',
|
||||
description: 'Auto-generate full markdown documentation for a session (tasks, commits, notes, plans)',
|
||||
description: 'Auto-generate full markdown documentation for a session',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1003,7 +670,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_semantic_search',
|
||||
description: 'Semantic search across all session documentation using vector similarity',
|
||||
description: 'Semantic search across all session documentation',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1016,7 +683,7 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_productivity_analytics',
|
||||
description: 'Get productivity metrics (avg duration, tasks/commits per session, etc.)',
|
||||
description: 'Get productivity metrics',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1027,12 +694,12 @@ export const toolDefinitions = [
|
||||
},
|
||||
{
|
||||
name: 'session_pattern_detection',
|
||||
description: 'Detect patterns across sessions (tool usage, task types)',
|
||||
description: 'Detect patterns across sessions',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Filter by project (optional)' },
|
||||
pattern_type: { type: 'string', enum: ['tool_usage', 'task_types', 'error_frequency'], description: 'Type of pattern to detect (default: tool_usage)' },
|
||||
pattern_type: { type: 'string', enum: ['tool_usage', 'task_types', 'error_frequency'], description: 'Type of pattern to detect' },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1040,17 +707,17 @@ export const toolDefinitions = [
|
||||
// Archive Tools
|
||||
{
|
||||
name: 'archive_add',
|
||||
description: 'Archive content to database with semantic embedding. Replaces filesystem archives.',
|
||||
description: 'Archive content to database with semantic embedding.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: { type: 'string', description: 'Project key (e.g., CF, VPN)' },
|
||||
project: { type: 'string', description: 'Project key' },
|
||||
archive_type: { type: 'string', enum: ['session', 'research', 'audit', 'investigation', 'completed', 'migration'], description: 'Archive type' },
|
||||
title: { type: 'string', description: 'Archive title' },
|
||||
content: { type: 'string', description: 'Archive content (markdown)' },
|
||||
original_path: { type: 'string', description: 'Original file path (optional)' },
|
||||
file_size: { type: 'number', description: 'File size in bytes (optional)' },
|
||||
archived_by_session: { type: 'string', description: 'Session ID that archived it (optional)' },
|
||||
archived_by_session: { type: 'string', description: 'Session ID (optional)' },
|
||||
metadata: { type: 'object', description: 'Additional metadata (optional)' },
|
||||
},
|
||||
required: ['project', 'archive_type', 'title', 'content'],
|
||||
@@ -1098,11 +765,11 @@ export const toolDefinitions = [
|
||||
// Project Archival
|
||||
{
|
||||
name: 'project_archive',
|
||||
description: 'Archive complete project to S3 with database tracking. Creates tarball, uploads to s3://agiliton-archive/projects/, updates database, and optionally deletes local copy.',
|
||||
description: 'Archive complete project to S3 with database tracking.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_key: { type: 'string', description: 'Project key (must exist in database)' },
|
||||
project_key: { type: 'string', description: 'Project key' },
|
||||
project_path: { type: 'string', description: 'Absolute path to project directory' },
|
||||
delete_local: { type: 'boolean', description: 'Delete local project after successful archive (default: false)' },
|
||||
session_id: { type: 'string', description: 'Session ID performing the archival (optional)' },
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
// Task relations: dependencies and checklists
|
||||
|
||||
import { query, queryOne, execute } from '../db.js';
|
||||
|
||||
interface TaskLinkArgs {
|
||||
from_id: string;
|
||||
to_id: string;
|
||||
link_type: string;
|
||||
}
|
||||
|
||||
interface ChecklistAddArgs {
|
||||
task_id: string;
|
||||
item: string;
|
||||
}
|
||||
|
||||
interface ChecklistToggleArgs {
|
||||
item_id: number;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dependency between tasks
|
||||
* - blocks: unidirectional (A blocks B)
|
||||
* - relates_to: bidirectional (A relates to B = B relates to A)
|
||||
* - duplicates: bidirectional (A duplicates B = B duplicates A)
|
||||
*/
|
||||
export async function taskLink(args: TaskLinkArgs): Promise<string> {
|
||||
const { from_id, to_id, link_type } = args;
|
||||
|
||||
try {
|
||||
// Create the primary link
|
||||
await execute(
|
||||
`INSERT INTO task_links (from_task_id, to_task_id, link_type)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (from_task_id, to_task_id, link_type) DO NOTHING`,
|
||||
[from_id, to_id, link_type]
|
||||
);
|
||||
|
||||
// For symmetric relationships, create reverse link
|
||||
if (link_type === 'relates_to' || link_type === 'duplicates') {
|
||||
await execute(
|
||||
`INSERT INTO task_links (from_task_id, to_task_id, link_type)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (from_task_id, to_task_id, link_type) DO NOTHING`,
|
||||
[to_id, from_id, link_type]
|
||||
);
|
||||
}
|
||||
|
||||
return `Linked: ${from_id} ${link_type} ${to_id}`;
|
||||
} catch (error) {
|
||||
return `Error creating link: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a checklist item to a task
|
||||
*/
|
||||
export async function checklistAdd(args: ChecklistAddArgs): Promise<string> {
|
||||
const { task_id, item } = args;
|
||||
|
||||
// Get next position
|
||||
const result = await queryOne<{ max: number }>(
|
||||
`SELECT COALESCE(MAX(position), 0) + 1 as max
|
||||
FROM task_checklist WHERE task_id = $1`,
|
||||
[task_id]
|
||||
);
|
||||
|
||||
const position = result?.max || 1;
|
||||
|
||||
await execute(
|
||||
`INSERT INTO task_checklist (task_id, item, position)
|
||||
VALUES ($1, $2, $3)`,
|
||||
[task_id, item, position]
|
||||
);
|
||||
|
||||
return `Added to ${task_id}: ${item}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a checklist item
|
||||
*/
|
||||
export async function checklistToggle(args: ChecklistToggleArgs): Promise<string> {
|
||||
const { item_id, checked } = args;
|
||||
|
||||
const result = await execute(
|
||||
`UPDATE task_checklist SET checked = $1 WHERE id = $2`,
|
||||
[checked, item_id]
|
||||
);
|
||||
|
||||
if (result === 0) {
|
||||
return `Checklist item not found: ${item_id}`;
|
||||
}
|
||||
|
||||
return `${checked ? 'Checked' : 'Unchecked'}: item #${item_id}`;
|
||||
}
|
||||
|
||||
interface ResolveDuplicateArgs {
|
||||
duplicate_id: string;
|
||||
dominant_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a duplicate issue by closing it and linking to the dominant issue
|
||||
* - Closes the duplicate task (sets status to completed)
|
||||
* - Creates bidirectional "duplicates" link between the two tasks
|
||||
*/
|
||||
export async function taskResolveDuplicate(args: ResolveDuplicateArgs): Promise<string> {
|
||||
const { duplicate_id, dominant_id } = args;
|
||||
|
||||
try {
|
||||
// Close the duplicate task
|
||||
const closeResult = await execute(
|
||||
`UPDATE tasks
|
||||
SET status = 'completed', completed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
[duplicate_id]
|
||||
);
|
||||
|
||||
if (closeResult === 0) {
|
||||
return `Duplicate task not found: ${duplicate_id}`;
|
||||
}
|
||||
|
||||
// Create bidirectional duplicates link
|
||||
await execute(
|
||||
`INSERT INTO task_links (from_task_id, to_task_id, link_type)
|
||||
VALUES ($1, $2, 'duplicates')
|
||||
ON CONFLICT (from_task_id, to_task_id, link_type) DO NOTHING`,
|
||||
[duplicate_id, dominant_id]
|
||||
);
|
||||
|
||||
await execute(
|
||||
`INSERT INTO task_links (from_task_id, to_task_id, link_type)
|
||||
VALUES ($1, $2, 'duplicates')
|
||||
ON CONFLICT (from_task_id, to_task_id, link_type) DO NOTHING`,
|
||||
[dominant_id, duplicate_id]
|
||||
);
|
||||
|
||||
return `Resolved duplicate: ${duplicate_id} → ${dominant_id}\n Closed: ${duplicate_id}\n Linked: duplicates ${dominant_id}`;
|
||||
} catch (error) {
|
||||
return `Error resolving duplicate: ${error}`;
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
// Semantic search operations
|
||||
|
||||
import { query, queryOne, getProjectKey } from '../db.js';
|
||||
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||
import type { SimilarTask } from '../types.js';
|
||||
|
||||
interface SessionNote {
|
||||
note_type: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface SessionTask {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
}
|
||||
|
||||
interface SessionCommit {
|
||||
commit_hash: string;
|
||||
commit_message: string;
|
||||
}
|
||||
|
||||
interface TaskSessionContextArgs {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session context for a task - retrieves notes, decisions, and related tasks
|
||||
* from the session where the task was created
|
||||
*/
|
||||
export async function taskSessionContext(args: TaskSessionContextArgs): Promise<string> {
|
||||
const { id } = args;
|
||||
|
||||
// Get task with session info
|
||||
const task = await queryOne<{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
session_id: string;
|
||||
}>(
|
||||
`SELECT t.id, t.title, t.description, t.session_id
|
||||
FROM tasks t
|
||||
WHERE t.id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!task) {
|
||||
return `Task not found: ${id}`;
|
||||
}
|
||||
|
||||
if (!task.session_id) {
|
||||
return `# Context for ${id}\n\n**Task:** ${task.title}\n\n⚠️ No session linked to this task. Task was created before session tracking was implemented or via direct database insert.\n\n${task.description ? `**Description:**\n${task.description}` : ''}`;
|
||||
}
|
||||
|
||||
// Get session info
|
||||
const session = await queryOne<{
|
||||
session_number: number;
|
||||
summary: string;
|
||||
started_at: string;
|
||||
}>(
|
||||
`SELECT session_number, summary, to_char(started_at, 'YYYY-MM-DD HH24:MI') as started_at
|
||||
FROM sessions
|
||||
WHERE id = $1`,
|
||||
[task.session_id]
|
||||
);
|
||||
|
||||
let output = `# Context for ${id}\n\n`;
|
||||
output += `**Task:** ${task.title}\n`;
|
||||
|
||||
if (session) {
|
||||
output += `**Created in Session:** #${session.session_number} (${session.started_at})\n`;
|
||||
|
||||
if (session.summary) {
|
||||
output += `\n## Session Summary\n${session.summary}\n`;
|
||||
}
|
||||
} else {
|
||||
output += `**Session ID:** ${task.session_id} (session record not found)\n`;
|
||||
}
|
||||
|
||||
if (task.description) {
|
||||
output += `\n## Task Description\n${task.description}\n`;
|
||||
}
|
||||
|
||||
// Get session notes
|
||||
const notes = await query<SessionNote>(
|
||||
`SELECT note_type, content, to_char(created_at, 'HH24:MI') as created_at
|
||||
FROM session_notes
|
||||
WHERE session_id = $1
|
||||
ORDER BY created_at`,
|
||||
[task.session_id]
|
||||
);
|
||||
|
||||
if (notes.length > 0) {
|
||||
output += `\n## Session Notes\n`;
|
||||
for (const note of notes) {
|
||||
output += `- **[${note.note_type}]** ${note.content}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Get related tasks from same session
|
||||
const relatedTasks = await query<SessionTask>(
|
||||
`SELECT id, title, status, priority
|
||||
FROM tasks
|
||||
WHERE session_id = $1 AND id != $2
|
||||
ORDER BY created_at`,
|
||||
[task.session_id, id]
|
||||
);
|
||||
|
||||
if (relatedTasks.length > 0) {
|
||||
output += `\n## Other Tasks from Same Session\n`;
|
||||
for (const t of relatedTasks) {
|
||||
const statusIcon = t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '▶' : '○';
|
||||
output += `- ${statusIcon} [${t.priority}] ${t.id}: ${t.title}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Get commits from session
|
||||
const commits = await query<SessionCommit>(
|
||||
`SELECT DISTINCT commit_hash, commit_message
|
||||
FROM task_commits
|
||||
WHERE task_id IN (SELECT id FROM tasks WHERE session_id = $1)
|
||||
ORDER BY committed_at DESC
|
||||
LIMIT 10`,
|
||||
[task.session_id]
|
||||
);
|
||||
|
||||
if (commits.length > 0) {
|
||||
output += `\n## Commits from Session\n`;
|
||||
for (const c of commits) {
|
||||
output += `- \`${c.commit_hash}\` ${c.commit_message}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
interface TaskSimilarArgs {
|
||||
query: string;
|
||||
project?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
interface TaskContextArgs {
|
||||
description: string;
|
||||
project?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find semantically similar tasks using pgvector
|
||||
*/
|
||||
export async function taskSimilar(args: TaskSimilarArgs): Promise<string> {
|
||||
const { query: searchQuery, project, limit = 5 } = args;
|
||||
|
||||
// Generate embedding for the query
|
||||
const embedding = await getEmbedding(searchQuery);
|
||||
if (!embedding) {
|
||||
return 'Error: Could not generate embedding for search query';
|
||||
}
|
||||
|
||||
const embeddingStr = formatEmbedding(embedding);
|
||||
|
||||
let whereClause = 'WHERE embedding IS NOT NULL';
|
||||
const params: unknown[] = [embeddingStr, limit];
|
||||
let paramIndex = 3;
|
||||
|
||||
if (project) {
|
||||
const projectKey = await getProjectKey(project);
|
||||
whereClause += ` AND project = $${paramIndex}`;
|
||||
params.push(projectKey);
|
||||
}
|
||||
|
||||
const results = await query<SimilarTask>(
|
||||
`SELECT id, title, type, status, priority,
|
||||
1 - (embedding <=> $1) as similarity
|
||||
FROM tasks
|
||||
${whereClause}
|
||||
ORDER BY embedding <=> $1
|
||||
LIMIT $2`,
|
||||
params
|
||||
);
|
||||
|
||||
if (results.length === 0) {
|
||||
return 'No similar tasks found';
|
||||
}
|
||||
|
||||
const lines = results.map(t => {
|
||||
const pct = Math.round(t.similarity * 100);
|
||||
const statusIcon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[>]' : '[ ]';
|
||||
return `${statusIcon} ${pct}% ${t.id}: ${t.title} [${t.type}] [${t.priority}]`;
|
||||
});
|
||||
|
||||
return `Similar tasks for "${searchQuery}":\n\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get related tasks for current work context
|
||||
* Returns markdown suitable for injection into delegations
|
||||
*/
|
||||
export async function taskContext(args: TaskContextArgs): Promise<string> {
|
||||
const { description, project, limit = 3 } = args;
|
||||
|
||||
// Generate embedding for the description
|
||||
const embedding = await getEmbedding(description);
|
||||
if (!embedding) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const embeddingStr = formatEmbedding(embedding);
|
||||
|
||||
let whereClause = 'WHERE embedding IS NOT NULL AND status != \'completed\'';
|
||||
const params: unknown[] = [embeddingStr, limit];
|
||||
let paramIndex = 3;
|
||||
|
||||
if (project) {
|
||||
const projectKey = await getProjectKey(project);
|
||||
whereClause += ` AND project = $${paramIndex}`;
|
||||
params.push(projectKey);
|
||||
}
|
||||
|
||||
const results = await query<SimilarTask>(
|
||||
`SELECT id, title, type, status, priority,
|
||||
1 - (embedding <=> $1) as similarity
|
||||
FROM tasks
|
||||
${whereClause}
|
||||
ORDER BY embedding <=> $1
|
||||
LIMIT $2`,
|
||||
params
|
||||
);
|
||||
|
||||
if (results.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Format as markdown for delegation context
|
||||
let output = '## Related Tasks\n\n';
|
||||
for (const t of results) {
|
||||
const pct = Math.round(t.similarity * 100);
|
||||
output += `- **${t.id}**: ${t.title} (${pct}% match, ${t.priority}, ${t.status})\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { query, queryOne, execute } from '../db.js';
|
||||
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||
import { getSessionId } from './crud.js';
|
||||
import { getSessionId } from './session-id.js';
|
||||
|
||||
// ============================================================================
|
||||
// SESSION NOTES
|
||||
|
||||
26
src/tools/session-id.ts
Normal file
26
src/tools/session-id.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Shared utility: get current session ID from environment or cache file.
|
||||
* Extracted from crud.ts during task-mcp → session-mcp fork (CF-762).
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
export function getSessionId(): string {
|
||||
if (process.env.CLAUDE_SESSION_ID) {
|
||||
return process.env.CLAUDE_SESSION_ID;
|
||||
}
|
||||
|
||||
const cacheFile = path.join(os.homedir(), '.cache', 'session-memory', 'current_session');
|
||||
try {
|
||||
const sessionId = fs.readFileSync(cacheFile, 'utf-8').trim();
|
||||
if (sessionId) return sessionId;
|
||||
} catch {
|
||||
// File doesn't exist or can't be read
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 15);
|
||||
return `session_${timestamp}`;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ interface SessionStartArgs {
|
||||
working_directory?: string;
|
||||
git_branch?: string;
|
||||
initial_prompt?: string;
|
||||
jira_issue_key?: string;
|
||||
}
|
||||
|
||||
interface SessionUpdateArgs {
|
||||
@@ -52,6 +53,7 @@ interface Session {
|
||||
token_count: number;
|
||||
tools_used: string[] | null;
|
||||
status: string;
|
||||
jira_issue_key: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -60,15 +62,15 @@ interface Session {
|
||||
* Returns session_id and session_number
|
||||
*/
|
||||
export async function sessionStart(args: SessionStartArgs): Promise<string> {
|
||||
const { session_id, project, working_directory, git_branch, initial_prompt } = args;
|
||||
const { session_id, project, working_directory, git_branch, initial_prompt, jira_issue_key } = args;
|
||||
|
||||
// Generate session ID if not provided (fallback, should come from session-memory)
|
||||
const id = session_id || `session_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
await execute(
|
||||
`INSERT INTO sessions (id, project, started_at, working_directory, git_branch, initial_prompt, status)
|
||||
VALUES ($1, $2, NOW(), $3, $4, $5, 'active')`,
|
||||
[id, project, working_directory || null, git_branch || null, initial_prompt || null]
|
||||
`INSERT INTO sessions (id, project, started_at, working_directory, git_branch, initial_prompt, jira_issue_key, status)
|
||||
VALUES ($1, $2, NOW(), $3, $4, $5, $6, 'active')`,
|
||||
[id, project, working_directory || null, git_branch || null, initial_prompt || null, jira_issue_key || null]
|
||||
);
|
||||
|
||||
// Get the assigned session_number
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
// Version management operations for task-mcp
|
||||
|
||||
import { query, queryOne, execute, getProjectKey } from '../db.js';
|
||||
import type { Version, Task } from '../types.js';
|
||||
|
||||
interface VersionAddArgs {
|
||||
project: string;
|
||||
version: string;
|
||||
build_number?: number;
|
||||
status?: string;
|
||||
release_notes?: string;
|
||||
}
|
||||
|
||||
interface VersionListArgs {
|
||||
project?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
interface VersionUpdateArgs {
|
||||
id: string;
|
||||
status?: string;
|
||||
git_tag?: string;
|
||||
git_sha?: string;
|
||||
release_notes?: string;
|
||||
release_date?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate version ID from project and version number
|
||||
*/
|
||||
function generateVersionId(projectKey: string, version: string): string {
|
||||
return `${projectKey}-v${version.replace(/^v/, '')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new version
|
||||
*/
|
||||
export async function versionAdd(args: VersionAddArgs): Promise<string> {
|
||||
const { project, version, build_number, status = 'planned', release_notes } = args;
|
||||
|
||||
// Get project key
|
||||
const projectKey = await getProjectKey(project);
|
||||
|
||||
// Generate version ID
|
||||
const versionId = generateVersionId(projectKey, version);
|
||||
|
||||
// Check if version already exists
|
||||
const existing = await queryOne<{ id: string }>(`SELECT id FROM versions WHERE id = $1`, [versionId]);
|
||||
if (existing) {
|
||||
return `Version already exists: ${versionId}`;
|
||||
}
|
||||
|
||||
// Insert version
|
||||
await execute(
|
||||
`INSERT INTO versions (id, project, version, build_number, status, release_notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[versionId, projectKey, version, build_number || null, status, release_notes || null]
|
||||
);
|
||||
|
||||
return `Created version: ${versionId}\n Version: ${version}\n Project: ${projectKey}\n Status: ${status}${build_number ? `\n Build: ${build_number}` : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* List versions with filters
|
||||
*/
|
||||
export async function versionList(args: VersionListArgs): Promise<string> {
|
||||
const { project, status, limit = 20 } = args;
|
||||
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (project) {
|
||||
const projectKey = await getProjectKey(project);
|
||||
whereClause += ` AND v.project = $${paramIndex++}`;
|
||||
params.push(projectKey);
|
||||
}
|
||||
if (status) {
|
||||
whereClause += ` AND v.status = $${paramIndex++}`;
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
params.push(limit);
|
||||
|
||||
const versions = await query<Version & { task_count: number; open_count: number }>(
|
||||
`SELECT v.id, v.version, v.status, v.project, v.build_number, v.git_tag,
|
||||
to_char(v.release_date, 'YYYY-MM-DD') as release_date,
|
||||
COUNT(t.id) as task_count,
|
||||
COUNT(t.id) FILTER (WHERE t.status != 'completed') as open_count
|
||||
FROM versions v
|
||||
LEFT JOIN tasks t ON t.version_id = v.id
|
||||
${whereClause}
|
||||
GROUP BY v.id, v.version, v.status, v.project, v.build_number, v.git_tag, v.release_date, v.created_at
|
||||
ORDER BY
|
||||
CASE v.status WHEN 'in_progress' THEN 0 WHEN 'planned' THEN 1 WHEN 'released' THEN 2 ELSE 3 END,
|
||||
v.created_at DESC
|
||||
LIMIT $${paramIndex}`,
|
||||
params
|
||||
);
|
||||
|
||||
if (versions.length === 0) {
|
||||
return `No versions found${project ? ` for project ${project}` : ''}`;
|
||||
}
|
||||
|
||||
const lines = versions.map(v => {
|
||||
const statusIcon = v.status === 'released' ? '[R]' : v.status === 'in_progress' ? '[>]' : v.status === 'archived' ? '[A]' : '[ ]';
|
||||
const progress = v.task_count > 0 ? ` (${v.task_count - v.open_count}/${v.task_count} tasks)` : '';
|
||||
const tag = v.git_tag ? ` [${v.git_tag}]` : '';
|
||||
const date = (v as unknown as { release_date: string }).release_date ? ` - ${(v as unknown as { release_date: string }).release_date}` : '';
|
||||
return `${statusIcon} ${v.id}: ${v.version}${tag}${progress}${date}`;
|
||||
});
|
||||
|
||||
return `Versions${project ? ` (${project})` : ''}:\n\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show version details with assigned tasks
|
||||
*/
|
||||
export async function versionShow(id: string): Promise<string> {
|
||||
const version = await queryOne<Version & { created: string; released: string }>(
|
||||
`SELECT id, project, version, build_number, status, release_notes, git_tag, git_sha,
|
||||
to_char(created_at, 'YYYY-MM-DD HH24:MI') as created,
|
||||
to_char(release_date, 'YYYY-MM-DD') as released
|
||||
FROM versions WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!version) {
|
||||
return `Version not found: ${id}`;
|
||||
}
|
||||
|
||||
let output = `# ${version.id}\n\n`;
|
||||
output += `**Version:** ${version.version}\n`;
|
||||
output += `**Project:** ${version.project}\n`;
|
||||
output += `**Status:** ${version.status}\n`;
|
||||
if (version.build_number) {
|
||||
output += `**Build:** ${version.build_number}\n`;
|
||||
}
|
||||
if ((version as unknown as { git_tag: string }).git_tag) {
|
||||
output += `**Git Tag:** ${(version as unknown as { git_tag: string }).git_tag}\n`;
|
||||
}
|
||||
if ((version as unknown as { git_sha: string }).git_sha) {
|
||||
output += `**Git SHA:** ${(version as unknown as { git_sha: string }).git_sha}\n`;
|
||||
}
|
||||
output += `**Created:** ${version.created}\n`;
|
||||
if (version.released) {
|
||||
output += `**Released:** ${version.released}\n`;
|
||||
}
|
||||
|
||||
if (version.release_notes) {
|
||||
output += `\n**Release Notes:**\n${version.release_notes}\n`;
|
||||
}
|
||||
|
||||
// Get tasks assigned to this version
|
||||
const tasks = await query<Task>(
|
||||
`SELECT id, title, status, priority, type
|
||||
FROM tasks
|
||||
WHERE version_id = $1
|
||||
ORDER BY
|
||||
CASE status WHEN 'in_progress' THEN 0 WHEN 'open' THEN 1 WHEN 'blocked' THEN 2 ELSE 3 END,
|
||||
CASE priority WHEN 'P0' THEN 0 WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 ELSE 3 END`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (tasks.length > 0) {
|
||||
const done = tasks.filter(t => t.status === 'completed').length;
|
||||
output += `\n**Tasks:** (${done}/${tasks.length} done)\n`;
|
||||
for (const t of tasks) {
|
||||
const statusIcon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[>]' : t.status === 'blocked' ? '[!]' : '[ ]';
|
||||
output += ` ${statusIcon} ${t.priority} ${t.id}: ${t.title}\n`;
|
||||
}
|
||||
} else {
|
||||
output += `\n**Tasks:** None assigned\n`;
|
||||
}
|
||||
|
||||
// Get epics targeting this version
|
||||
const epics = await query<{ id: string; title: string; status: string }>(
|
||||
`SELECT id, title, status FROM epics WHERE target_version_id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (epics.length > 0) {
|
||||
output += `\n**Epics:**\n`;
|
||||
for (const e of epics) {
|
||||
const statusIcon = e.status === 'completed' ? '[x]' : e.status === 'in_progress' ? '[>]' : '[ ]';
|
||||
output += ` ${statusIcon} ${e.id}: ${e.title}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a version
|
||||
*/
|
||||
export async function versionUpdate(args: VersionUpdateArgs): Promise<string> {
|
||||
const { id, status, git_tag, git_sha, release_notes, release_date } = args;
|
||||
|
||||
const updates: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (status) {
|
||||
updates.push(`status = $${paramIndex++}`);
|
||||
params.push(status);
|
||||
}
|
||||
if (git_tag !== undefined) {
|
||||
updates.push(`git_tag = $${paramIndex++}`);
|
||||
params.push(git_tag);
|
||||
}
|
||||
if (git_sha !== undefined) {
|
||||
updates.push(`git_sha = $${paramIndex++}`);
|
||||
params.push(git_sha);
|
||||
}
|
||||
if (release_notes !== undefined) {
|
||||
updates.push(`release_notes = $${paramIndex++}`);
|
||||
params.push(release_notes);
|
||||
}
|
||||
if (release_date) {
|
||||
updates.push(`release_date = $${paramIndex++}`);
|
||||
params.push(release_date);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return 'No updates specified';
|
||||
}
|
||||
|
||||
params.push(id);
|
||||
|
||||
const result = await execute(
|
||||
`UPDATE versions SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
|
||||
params
|
||||
);
|
||||
|
||||
if (result === 0) {
|
||||
return `Version not found: ${id}`;
|
||||
}
|
||||
|
||||
return `Updated: ${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a version as released
|
||||
*/
|
||||
export async function versionRelease(args: { id: string; git_tag?: string }): Promise<string> {
|
||||
const { id, git_tag } = args;
|
||||
|
||||
// Verify version exists
|
||||
const version = await queryOne<{ id: string; status: string; version: string }>(
|
||||
`SELECT id, status, version FROM versions WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!version) {
|
||||
return `Version not found: ${id}`;
|
||||
}
|
||||
|
||||
if (version.status === 'released') {
|
||||
return `Version already released: ${id}`;
|
||||
}
|
||||
|
||||
// Update version status
|
||||
const updates = ['status = $1', 'release_date = NOW()'];
|
||||
const params: unknown[] = ['released'];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (git_tag) {
|
||||
updates.push(`git_tag = $${paramIndex++}`);
|
||||
params.push(git_tag);
|
||||
}
|
||||
|
||||
params.push(id);
|
||||
|
||||
await execute(
|
||||
`UPDATE versions SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
|
||||
params
|
||||
);
|
||||
|
||||
return `Released: ${id} (${version.version})${git_tag ? ` tagged as ${git_tag}` : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a task to a version
|
||||
*/
|
||||
export async function versionAssignTask(args: { task_id: string; version_id: string }): Promise<string> {
|
||||
const { task_id, version_id } = args;
|
||||
|
||||
// Verify version exists
|
||||
const version = await queryOne<{ id: string }>(`SELECT id FROM versions WHERE id = $1`, [version_id]);
|
||||
if (!version) {
|
||||
return `Version not found: ${version_id}`;
|
||||
}
|
||||
|
||||
// Update task
|
||||
const result = await execute(
|
||||
`UPDATE tasks SET version_id = $1, updated_at = NOW() WHERE id = $2`,
|
||||
[version_id, task_id]
|
||||
);
|
||||
|
||||
if (result === 0) {
|
||||
return `Task not found: ${task_id}`;
|
||||
}
|
||||
|
||||
return `Assigned ${task_id} to version ${version_id}`;
|
||||
}
|
||||
Reference in New Issue
Block a user