feat(CF-301): Implement task_move_project function
Added task_move_project tool to move tasks between projects while preserving all relationships and history: Changes: - db.ts: Added getClient() for transaction support - crud.ts: Implemented taskMoveProject() with full transaction - index.ts: Wired up tool handler and import - tools/index.ts: Registered tool definition Preserves: - Task metadata (title, description, type, status, priority) - Checklist items - Linked commits - Delegations - Activity history - Task links (relates_to, blocks, duplicates) - Epic and version assignments - Embeddings Process: 1. Validates source task and target project exist 2. Generates new ID in target project 3. Transfers all related data in transaction 4. Marks old task as completed with reference 5. Creates duplicate link between tasks Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,13 @@ export async function testConnection(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a client from the pool for transactions
|
||||||
|
*/
|
||||||
|
export async function getClient() {
|
||||||
|
return await pool.connect();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the connection pool
|
* Close the connection pool
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
|
|
||||||
import { testConnection, close } from './db.js';
|
import { testConnection, close } from './db.js';
|
||||||
import { toolDefinitions } from './tools/index.js';
|
import { toolDefinitions } from './tools/index.js';
|
||||||
import { taskAdd, taskList, taskShow, taskClose, taskUpdate, taskInvestigate } from './tools/crud.js';
|
import { taskAdd, taskList, taskShow, taskClose, taskUpdate, taskInvestigate, taskMoveProject } from './tools/crud.js';
|
||||||
import { taskSimilar, taskContext } from './tools/search.js';
|
import { taskSimilar, taskContext } from './tools/search.js';
|
||||||
import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js';
|
import { taskLink, checklistAdd, checklistToggle, taskResolveDuplicate } from './tools/relations.js';
|
||||||
import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js';
|
import { epicAdd, epicList, epicShow, epicAssign, epicClose } from './tools/epics.js';
|
||||||
@@ -147,6 +147,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
description: a.description,
|
description: a.description,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'task_move_project':
|
||||||
|
result = await taskMoveProject({
|
||||||
|
id: a.id,
|
||||||
|
target_project: a.target_project,
|
||||||
|
reason: a.reason,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
case 'task_similar':
|
case 'task_similar':
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// CRUD operations for tasks
|
// CRUD operations for tasks
|
||||||
|
|
||||||
import { query, queryOne, execute, getNextTaskId, getProjectKey, detectProjectFromCwd } from '../db.js';
|
import { query, queryOne, execute, getNextTaskId, getProjectKey, detectProjectFromCwd, getClient } from '../db.js';
|
||||||
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
import { getEmbedding, formatEmbedding } from '../embeddings.js';
|
||||||
import type { Task, ChecklistItem, TaskLink } from '../types.js';
|
import type { Task, ChecklistItem, TaskLink } from '../types.js';
|
||||||
import { getRecentDelegations } from './delegations.js';
|
import { getRecentDelegations } from './delegations.js';
|
||||||
@@ -551,3 +551,109 @@ export async function taskInvestigate(args: TaskAddArgs): Promise<string> {
|
|||||||
|
|
||||||
return taskResult + '\n\n🔍 Investigation started! All new tasks will auto-link to this investigation.';
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,6 +82,19 @@ export const toolDefinitions = [
|
|||||||
required: ['title'],
|
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
|
// Semantic Search Tools
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user