From 6ff42434d3a3dcf276ec870c7474d855fb7a48fd Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 29 Jul 2025 06:18:22 +0000 Subject: [PATCH] feat: add required todos parameter to new_task tool for hierarchical task planning - Export parseMarkdownChecklist function from updateTodoListTool.ts - Update NewTaskToolUse interface to include todos parameter - Add initialTodos to TaskOptions and Task constructor - Update ClineProvider.initClineWithTask to pass initialTodos - Implement todos parameter handling in newTaskTool.ts with markdown parsing - Update tool documentation to include todos parameter and examples - Add comprehensive tests for the new functionality Fixes #6329 BREAKING CHANGE: The new_task tool now requires a todos parameter containing a markdown checklist string --- src/core/prompts/tools/new-task.ts | 16 ++++- src/core/task/Task.ts | 7 +++ src/core/tools/__tests__/newTaskTool.spec.ts | 62 ++++++++++++++++++++ src/core/tools/newTaskTool.ts | 28 ++++++++- src/core/tools/updateTodoListTool.ts | 2 +- src/core/webview/ClineProvider.ts | 7 ++- src/shared/tools.ts | 2 +- 7 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/core/prompts/tools/new-task.ts b/src/core/prompts/tools/new-task.ts index 7301b7b422..d40b6b44b6 100644 --- a/src/core/prompts/tools/new-task.ts +++ b/src/core/prompts/tools/new-task.ts @@ -2,22 +2,34 @@ import { ToolArgs } from "./types" export function getNewTaskDescription(_args: ToolArgs): string { return `## new_task -Description: This will let you create a new task instance in the chosen mode using your provided message. +Description: This will let you create a new task instance in the chosen mode using your provided message and initial todo list. Parameters: - mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect"). - message: (required) The initial user message or instructions for this new task. +- todos: (required) A markdown checklist string defining the initial todo list for the task. Usage: your-mode-slug-here Your initial instructions here + +[ ] First task to complete +[ ] Second task to complete +[ ] Third task to complete + Example: code -Implement a new feature for the application. +Implement user authentication + +[ ] Set up auth middleware +[ ] Create login endpoint +[ ] Add session management +[ ] Write tests + ` } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index edbde32ea7..71b86c22af 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -125,6 +125,7 @@ export type TaskOptions = { rootTask?: Task parentTask?: Task taskNumber?: number + initialTodos?: TodoItem[] onCreated?: (cline: Task) => void } @@ -270,6 +271,7 @@ export class Task extends EventEmitter { rootTask, parentTask, taskNumber = -1, + initialTodos, onCreated, }: TaskOptions) { super() @@ -324,6 +326,11 @@ export class Task extends EventEmitter { TelemetryService.instance.captureTaskCreated(this.taskId) } + // Initialize todo list if provided + if (initialTodos) { + this.todoList = initialTodos + } + // Only set up diff strategy if diff is enabled if (this.diffEnabled) { // Default to old strategy, will be updated if experiment is enabled diff --git a/src/core/tools/__tests__/newTaskTool.spec.ts b/src/core/tools/__tests__/newTaskTool.spec.ts index 1dd79d6e98..97f0f37792 100644 --- a/src/core/tools/__tests__/newTaskTool.spec.ts +++ b/src/core/tools/__tests__/newTaskTool.spec.ts @@ -14,6 +14,13 @@ vi.mock("../../prompts/responses", () => ({ }, })) +vi.mock("../updateTodoListTool", () => ({ + parseMarkdownChecklist: vi.fn((md: string) => [ + { id: "1", content: "Test todo 1", status: "pending" }, + { id: "2", content: "Test todo 2", status: "pending" }, + ]), +})) + // Define a minimal type for the resolved value type MockClineInstance = { taskId: string } @@ -72,6 +79,7 @@ describe("newTaskTool", () => { params: { mode: "code", message: "Review this: \\\\@file1.txt and also \\\\\\\\@file2.txt", // Input with \\@ and \\\\@ + todos: "[ ] Test todo 1\n[ ] Test todo 2", }, partial: false, } @@ -93,6 +101,12 @@ describe("newTaskTool", () => { "Review this: \\@file1.txt and also \\\\\\@file2.txt", // Unit Test Expectation: \\@ -> \@, \\\\@ -> \\\\@ undefined, mockCline, + { + initialTodos: [ + { id: "1", content: "Test todo 1", status: "pending" }, + { id: "2", content: "Test todo 2", status: "pending" }, + ], + }, ) // Verify side effects @@ -109,6 +123,7 @@ describe("newTaskTool", () => { params: { mode: "code", message: "This is already unescaped: \\@file1.txt", + todos: "[ ] Test todo", }, partial: false, } @@ -126,6 +141,12 @@ describe("newTaskTool", () => { "This is already unescaped: \\@file1.txt", // Expected: \@ remains \@ undefined, mockCline, + { + initialTodos: [ + { id: "1", content: "Test todo 1", status: "pending" }, + { id: "2", content: "Test todo 2", status: "pending" }, + ], + }, ) }) @@ -136,6 +157,7 @@ describe("newTaskTool", () => { params: { mode: "code", message: "A normal mention @file1.txt", + todos: "[ ] Test todo", }, partial: false, } @@ -153,6 +175,12 @@ describe("newTaskTool", () => { "A normal mention @file1.txt", // Expected: @ remains @ undefined, mockCline, + { + initialTodos: [ + { id: "1", content: "Test todo 1", status: "pending" }, + { id: "2", content: "Test todo 2", status: "pending" }, + ], + }, ) }) @@ -163,6 +191,7 @@ describe("newTaskTool", () => { params: { mode: "code", message: "Mix: @file0.txt, \\@file1.txt, \\\\@file2.txt, \\\\\\\\@file3.txt", + todos: "[ ] Test todo", }, partial: false, } @@ -180,8 +209,41 @@ describe("newTaskTool", () => { "Mix: @file0.txt, \\@file1.txt, \\@file2.txt, \\\\\\@file3.txt", // Unit Test Expectation: @->@, \@->\@, \\@->\@, \\\\@->\\\\@ undefined, mockCline, + { + initialTodos: [ + { id: "1", content: "Test todo 1", status: "pending" }, + { id: "2", content: "Test todo 2", status: "pending" }, + ], + }, ) }) + it("should handle missing todos parameter", async () => { + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "code", + message: "Test message", + // todos is missing + }, + partial: false, + } + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Should call sayAndCreateMissingParamError for todos + expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "todos") + expect(mockCline.consecutiveMistakeCount).toBe(1) + expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task") + }) + // Add more tests for error handling (missing params, invalid mode, approval denied) if needed }) diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index cc56659d02..2540eb3584 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -5,6 +5,7 @@ import { Task } from "../task/Task" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import { formatResponse } from "../prompts/responses" import { t } from "../../i18n" +import { parseMarkdownChecklist } from "./updateTodoListTool" export async function newTaskTool( cline: Task, @@ -16,6 +17,7 @@ export async function newTaskTool( ) { const mode: string | undefined = block.params.mode const message: string | undefined = block.params.message + const todos: string | undefined = block.params.todos try { if (block.partial) { @@ -23,6 +25,7 @@ export async function newTaskTool( tool: "newTask", mode: removeClosingTag("mode", mode), content: removeClosingTag("message", message), + todos: removeClosingTag("todos", todos), }) await cline.ask("tool", partialMessage, block.partial).catch(() => {}) @@ -42,6 +45,13 @@ export async function newTaskTool( return } + if (!todos) { + cline.consecutiveMistakeCount++ + cline.recordToolError("new_task") + pushToolResult(await cline.sayAndCreateMissingParamError("new_task", "todos")) + return + } + cline.consecutiveMistakeCount = 0 // Un-escape one level of backslashes before '@' for hierarchical subtasks // Un-escape one level: \\@ -> \@ (removes one backslash for hierarchical subtasks) @@ -55,10 +65,24 @@ export async function newTaskTool( return } + // Parse the todos markdown + let parsedTodos + try { + parsedTodos = parseMarkdownChecklist(todos) + } catch (error) { + pushToolResult( + formatResponse.toolError( + `Invalid todos markdown format: ${error instanceof Error ? error.message : String(error)}`, + ), + ) + return + } + const toolMessage = JSON.stringify({ tool: "newTask", mode: targetMode.name, content: message, + todos: parsedTodos, }) const didApprove = await askApproval("tool", toolMessage) @@ -81,7 +105,9 @@ export async function newTaskTool( cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug // Create new task instance first (this preserves parent's current mode in its history) - const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline) + const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline, { + initialTodos: parsedTodos, + }) if (!newCline) { pushToolResult(t("tools:newTask.errors.policy_restriction")) return diff --git a/src/core/tools/updateTodoListTool.ts b/src/core/tools/updateTodoListTool.ts index cbb90338d3..de96c3cc76 100644 --- a/src/core/tools/updateTodoListTool.ts +++ b/src/core/tools/updateTodoListTool.ts @@ -100,7 +100,7 @@ function normalizeStatus(status: string | undefined): TodoStatus { return "pending" } -function parseMarkdownChecklist(md: string): TodoItem[] { +export function parseMarkdownChecklist(md: string): TodoItem[] { if (typeof md !== "string") return [] const lines = md .split(/\r?\n/) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1218999a9a..a40a159906 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -532,7 +532,12 @@ export class ClineProvider options: Partial< Pick< TaskOptions, - "enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments" + | "enableDiff" + | "enableCheckpoints" + | "fuzzyMatchThreshold" + | "consecutiveMistakeLimit" + | "experiments" + | "initialTodos" > > = {}, ) { diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 67972243fe..047c2fe351 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -155,7 +155,7 @@ export interface SwitchModeToolUse extends ToolUse { export interface NewTaskToolUse extends ToolUse { name: "new_task" - params: Partial, "mode" | "message">> + params: Partial, "mode" | "message" | "todos">> } export interface SearchAndReplaceToolUse extends ToolUse {