delete task tool

This commit is contained in:
Anjali Sridhar
2026-03-10 15:06:39 -07:00
parent b68d7bc0f9
commit 94dfb44e16
9 changed files with 241 additions and 2 deletions
+6
View File
@@ -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
@@ -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.
+3 -1
View File
@@ -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.
@@ -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}`,
),
);
});
});
@@ -186,6 +186,35 @@ export class TrackerService {
return updatedTask;
}
/**
* Deletes a task by ID.
*/
async deleteTask(id: string): Promise<void> {
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.
*/
@@ -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'],
},
},
};
+2
View File
@@ -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,
@@ -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();
});
});
+81
View File
@@ -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<ToolResult> {
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);
}
}