diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f615564533..b763efd63e 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -76,6 +76,7 @@ import { TrackerListTasksTool, TrackerAddDependencyTool, TrackerVisualizeTool, + TrackerDeleteTaskTool, } from '../tools/trackerTools.js'; import { logRipgrepFallback, @@ -2907,6 +2908,11 @@ export class Config implements McpContext, AgentLoopContext { maybeRegister(TrackerVisualizeTool, () => registry.registerTool(new TrackerVisualizeTool(this, this._messageBus)), ); + maybeRegister(TrackerDeleteTaskTool, () => + registry.registerTool( + new TrackerDeleteTaskTool(this, this._messageBus), + ), + ); } // Register Subagents as Tools diff --git a/packages/core/src/core/__snapshots__/prompts.test.ts.snap b/packages/core/src/core/__snapshots__/prompts.test.ts.snap index e345fc3882..cd0ff92daf 100644 --- a/packages/core/src/core/__snapshots__/prompts.test.ts.snap +++ b/packages/core/src/core/__snapshots__/prompts.test.ts.snap @@ -2877,7 +2877,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi # TASK MANAGEMENT PROTOCOL You are operating with a persistent file-based task tracking system located at \`.tracker/tasks/\`. You must adhere to the following rules: -1. **NO IN-MEMORY LISTS**: Do not maintain a mental list of tasks or write markdown checkboxes in the chat. Use the provided tools (\`tracker_create_task\`, \`tracker_list_tasks\`, \`tracker_update_task\`) for all state management. +1. **NO IN-MEMORY LISTS**: Do not maintain a mental list of tasks or write markdown checkboxes in the chat. Use the provided tools (\`tracker_create_task\`, \`tracker_list_tasks\`, \`tracker_update_task\`, \`tracker_delete_task\`) for all state management. 2. **IMMEDIATE DECOMPOSITION**: Upon receiving a task, evaluate its functional complexity and scope. If the request involves more than a single atomic modification, or necessitates research before execution, you MUST immediately decompose it into discrete entries using \`tracker_create_task\`. 3. **IGNORE FORMATTING BIAS**: Trigger the protocol based on the **objective complexity** of the goal, regardless of whether the user provided a structured list or a single block of text/paragraph. "Paragraph-style" goals that imply multiple actions are multi-step projects and MUST be tracked. 4. **PLAN MODE INTEGRATION**: If an approved plan exists, you MUST use the \`tracker_create_task\` tool to decompose it into discrete tasks before writing any code. Maintain a bidirectional understanding between the plan document and the task graph. diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 3b3334f96b..ae76048a4e 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -30,6 +30,7 @@ import { TRACKER_CREATE_TASK_TOOL_NAME, TRACKER_LIST_TASKS_TOOL_NAME, TRACKER_UPDATE_TASK_TOOL_NAME, + TRACKER_DELETE_TASK_TOOL_NAME, } from '../tools/tool-names.js'; import type { HierarchicalMemory } from '../config/memory.js'; import { DEFAULT_CONTEXT_FILENAME } from '../tools/memoryTool.js'; @@ -478,12 +479,13 @@ export function renderTaskTracker(): string { const trackerCreate = formatToolName(TRACKER_CREATE_TASK_TOOL_NAME); const trackerList = formatToolName(TRACKER_LIST_TASKS_TOOL_NAME); const trackerUpdate = formatToolName(TRACKER_UPDATE_TASK_TOOL_NAME); + const trackerDelete = formatToolName(TRACKER_DELETE_TASK_TOOL_NAME); return ` # TASK MANAGEMENT PROTOCOL You are operating with a persistent file-based task tracking system located at \`.tracker/tasks/\`. You must adhere to the following rules: -1. **NO IN-MEMORY LISTS**: Do not maintain a mental list of tasks or write markdown checkboxes in the chat. Use the provided tools (${trackerCreate}, ${trackerList}, ${trackerUpdate}) for all state management. +1. **NO IN-MEMORY LISTS**: Do not maintain a mental list of tasks or write markdown checkboxes in the chat. Use the provided tools (${trackerCreate}, ${trackerList}, ${trackerUpdate}, ${trackerDelete}) for all state management. 2. **IMMEDIATE DECOMPOSITION**: Upon receiving a task, evaluate its functional complexity and scope. If the request involves more than a single atomic modification, or necessitates research before execution, you MUST immediately decompose it into discrete entries using ${trackerCreate}. 3. **IGNORE FORMATTING BIAS**: Trigger the protocol based on the **objective complexity** of the goal, regardless of whether the user provided a structured list or a single block of text/paragraph. "Paragraph-style" goals that imply multiple actions are multi-step projects and MUST be tracked. 4. **PLAN MODE INTEGRATION**: If an approved plan exists, you MUST use the ${trackerCreate} tool to decompose it into discrete tasks before writing any code. Maintain a bidirectional understanding between the plan document and the task graph. diff --git a/packages/core/src/services/trackerService.test.ts b/packages/core/src/services/trackerService.test.ts index 70a29d25af..348ae2a08d 100644 --- a/packages/core/src/services/trackerService.test.ts +++ b/packages/core/src/services/trackerService.test.ts @@ -139,4 +139,73 @@ describe('TrackerService', () => { service.updateTask(taskA.id, { dependencies: [taskB.id] }), ).rejects.toThrow(/Circular dependency detected/); }); + + it('should delete a task if no other tasks depend on it', async () => { + const task = await service.createTask({ + title: 'Task to be deleted', + description: 'Will be deleted', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }); + + await service.deleteTask(task.id); + const retrieved = await service.getTask(task.id); + expect(retrieved).toBeNull(); + }); + + it('should throw when deleting a non-existent task', async () => { + await expect(service.deleteTask('nonexistent')).rejects.toThrow( + /Task with ID nonexistent not found/, + ); + }); + + it('should prevent deleting a task that is a dependency for another task', async () => { + const dep = await service.createTask({ + title: 'Dependency', + description: 'Used by main', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }); + + const main = await service.createTask({ + title: 'Main Task', + description: 'Depends on dep', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [dep.id], + }); + + await expect(service.deleteTask(dep.id)).rejects.toThrow( + new RegExp( + `Cannot delete task ${dep.id} because it is a dependency of other tasks: ${main.id}`, + ), + ); + }); + + it('should prevent deleting a task that has children', async () => { + const parent = await service.createTask({ + title: 'Parent', + description: 'Has child', + type: TaskType.EPIC, + status: TaskStatus.OPEN, + dependencies: [], + }); + + const child = await service.createTask({ + title: 'Child Task', + description: 'Has parent', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + parentId: parent.id, + }); + + await expect(service.deleteTask(parent.id)).rejects.toThrow( + new RegExp( + `Cannot delete task ${parent.id} because it has child tasks: ${child.id}`, + ), + ); + }); }); diff --git a/packages/core/src/services/trackerService.ts b/packages/core/src/services/trackerService.ts index 06e890175f..376c1fdbb0 100644 --- a/packages/core/src/services/trackerService.ts +++ b/packages/core/src/services/trackerService.ts @@ -186,6 +186,35 @@ export class TrackerService { return updatedTask; } + /** + * Deletes a task by ID. + */ + async deleteTask(id: string): Promise { + await this.ensureInitialized(); + const task = await this.getTask(id); + if (!task) { + throw new Error(`Task with ID ${id} not found.`); + } + + // Prevent deletion if other tasks depend on this one or have it as parent + const allTasks = await this.listTasks(); + const dependents = allTasks.filter((t) => t.dependencies.includes(id)); + if (dependents.length > 0) { + throw new Error( + `Cannot delete task ${id} because it is a dependency of other tasks: ${dependents.map((t) => t.id).join(', ')}.`, + ); + } + const children = allTasks.filter((t) => t.parentId === id); + if (children.length > 0) { + throw new Error( + `Cannot delete task ${id} because it has child tasks: ${children.map((t) => t.id).join(', ')}.`, + ); + } + + const taskPath = path.join(this.tasksDir, `${id}.json`); + await fs.unlink(taskPath); + } + /** * Saves a task to disk. */ diff --git a/packages/core/src/tools/definitions/trackerTools.ts b/packages/core/src/tools/definitions/trackerTools.ts index e136d90d04..87d5996c67 100644 --- a/packages/core/src/tools/definitions/trackerTools.ts +++ b/packages/core/src/tools/definitions/trackerTools.ts @@ -12,6 +12,7 @@ import { TRACKER_LIST_TASKS_TOOL_NAME, TRACKER_ADD_DEPENDENCY_TOOL_NAME, TRACKER_VISUALIZE_TOOL_NAME, + TRACKER_DELETE_TASK_TOOL_NAME, } from '../tool-names.js'; export const TRACKER_CREATE_TASK_DEFINITION: ToolDefinition = { @@ -159,3 +160,20 @@ export const TRACKER_VISUALIZE_DEFINITION: ToolDefinition = { }, }, }; + +export const TRACKER_DELETE_TASK_DEFINITION: ToolDefinition = { + base: { + name: TRACKER_DELETE_TASK_TOOL_NAME, + description: 'Deletes a task from the tracker.', + parametersJsonSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'The 6-character hex ID of the task to delete.', + }, + }, + required: ['id'], + }, + }, +}; diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index fcdcbd6df6..6115fedd89 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -166,6 +166,7 @@ export const TRACKER_GET_TASK_TOOL_NAME = 'tracker_get_task'; export const TRACKER_LIST_TASKS_TOOL_NAME = 'tracker_list_tasks'; export const TRACKER_ADD_DEPENDENCY_TOOL_NAME = 'tracker_add_dependency'; export const TRACKER_VISUALIZE_TOOL_NAME = 'tracker_visualize'; +export const TRACKER_DELETE_TASK_TOOL_NAME = 'tracker_delete_task'; /** * Mapping of legacy tool names to their current names. @@ -225,6 +226,7 @@ export const ALL_BUILTIN_TOOL_NAMES = [ TRACKER_LIST_TASKS_TOOL_NAME, TRACKER_ADD_DEPENDENCY_TOOL_NAME, TRACKER_VISUALIZE_TOOL_NAME, + TRACKER_DELETE_TASK_TOOL_NAME, GET_INTERNAL_DOCS_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, diff --git a/packages/core/src/tools/trackerTools.test.ts b/packages/core/src/tools/trackerTools.test.ts index ec0bd0e889..025b402dce 100644 --- a/packages/core/src/tools/trackerTools.test.ts +++ b/packages/core/src/tools/trackerTools.test.ts @@ -14,6 +14,7 @@ import { TrackerUpdateTaskTool, TrackerVisualizeTool, TrackerAddDependencyTool, + TrackerDeleteTaskTool, } from './trackerTools.js'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; @@ -142,4 +143,35 @@ describe('Tracker Tools Integration', () => { expect(vizResult.llmContent).toContain('Child Task'); expect(vizResult.llmContent).toContain(childId); }); + + it('deletes a task', async () => { + const createTool = new TrackerCreateTaskTool(config, messageBus); + const deleteTool = new TrackerDeleteTaskTool(config, messageBus); + + // Create Task to delete + await createTool.buildAndExecute( + { + title: 'Delete Me', + description: '...', + type: TaskType.TASK, + }, + getSignal(), + ); + + const tasks = await config.getTrackerService().listTasks(); + const taskId = tasks.find((t) => t.title === 'Delete Me')!.id; + + // Delete Task + const deleteResult = await deleteTool.buildAndExecute( + { + id: taskId, + }, + getSignal(), + ); + + expect(deleteResult.llmContent).toContain(`Deleted task ${taskId}`); + + const task = await config.getTrackerService().getTask(taskId); + expect(task).toBeNull(); + }); }); diff --git a/packages/core/src/tools/trackerTools.ts b/packages/core/src/tools/trackerTools.ts index 03ee3c3a97..b5e07909c3 100644 --- a/packages/core/src/tools/trackerTools.ts +++ b/packages/core/src/tools/trackerTools.ts @@ -13,6 +13,7 @@ import { TRACKER_LIST_TASKS_DEFINITION, TRACKER_UPDATE_TASK_DEFINITION, TRACKER_VISUALIZE_DEFINITION, + TRACKER_DELETE_TASK_DEFINITION, } from './definitions/trackerTools.js'; import { resolveToolDeclaration } from './definitions/resolver.js'; import { @@ -22,6 +23,7 @@ import { TRACKER_LIST_TASKS_TOOL_NAME, TRACKER_UPDATE_TASK_TOOL_NAME, TRACKER_VISUALIZE_TOOL_NAME, + TRACKER_DELETE_TASK_TOOL_NAME, } from './tool-names.js'; import type { ToolResult } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; @@ -604,3 +606,82 @@ export class TrackerVisualizeTool extends BaseDeclarativeTool< return resolveToolDeclaration(TRACKER_VISUALIZE_DEFINITION, modelId); } } + +// --- tracker_delete_task --- + +interface DeleteTaskParams { + id: string; +} + +class TrackerDeleteTaskInvocation extends BaseToolInvocation< + DeleteTaskParams, + ToolResult +> { + constructor( + private readonly config: Config, + params: DeleteTaskParams, + messageBus: MessageBus, + toolName: string, + ) { + super(params, messageBus, toolName); + } + + private get service() { + return this.config.getTrackerService(); + } + getDescription(): string { + return `Deleting task ${this.params.id}`; + } + + override async execute(_signal: AbortSignal): Promise { + try { + await this.service.deleteTask(this.params.id); + return { + llmContent: `Deleted task ${this.params.id}.`, + returnDisplay: `Deleted task ${this.params.id}.`, + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + llmContent: `Error deleting task: ${errorMessage}`, + returnDisplay: 'Failed to delete task.', + error: { + message: errorMessage, + type: ToolErrorType.EXECUTION_FAILED, + }, + }; + } + } +} + +export class TrackerDeleteTaskTool extends BaseDeclarativeTool< + DeleteTaskParams, + ToolResult +> { + static readonly Name = TRACKER_DELETE_TASK_TOOL_NAME; + constructor( + private config: Config, + messageBus: MessageBus, + ) { + super( + TrackerDeleteTaskTool.Name, + 'Delete Task', + TRACKER_DELETE_TASK_DEFINITION.base.description!, + Kind.Edit, + TRACKER_DELETE_TASK_DEFINITION.base.parametersJsonSchema, + messageBus, + ); + } + protected createInvocation(params: DeleteTaskParams, messageBus: MessageBus) { + return new TrackerDeleteTaskInvocation( + this.config, + params, + messageBus, + this.name, + ); + } + override getSchema(modelId?: string) { + return resolveToolDeclaration(TRACKER_DELETE_TASK_DEFINITION, modelId); + } +}