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
This commit is contained in:
Roo Code 2025-07-29 06:18:22 +00:00
parent 00a3738d30
commit 6ff42434d3
7 changed files with 118 additions and 6 deletions

View file

@ -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:
<new_task>
<mode>your-mode-slug-here</mode>
<message>Your initial instructions here</message>
<todos>
[ ] First task to complete
[ ] Second task to complete
[ ] Third task to complete
</todos>
</new_task>
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement user authentication</message>
<todos>
[ ] Set up auth middleware
[ ] Create login endpoint
[ ] Add session management
[ ] Write tests
</todos>
</new_task>
`
}

View file

@ -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<ClineEvents> {
rootTask,
parentTask,
taskNumber = -1,
initialTodos,
onCreated,
}: TaskOptions) {
super()
@ -324,6 +326,11 @@ export class Task extends EventEmitter<ClineEvents> {
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

View file

@ -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
})

View file

@ -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

View file

@ -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/)

View file

@ -532,7 +532,12 @@ export class ClineProvider
options: Partial<
Pick<
TaskOptions,
"enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments"
| "enableDiff"
| "enableCheckpoints"
| "fuzzyMatchThreshold"
| "consecutiveMistakeLimit"
| "experiments"
| "initialTodos"
>
> = {},
) {

View file

@ -155,7 +155,7 @@ export interface SwitchModeToolUse extends ToolUse {
export interface NewTaskToolUse extends ToolUse {
name: "new_task"
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message">>
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message" | "todos">>
}
export interface SearchAndReplaceToolUse extends ToolUse {