Skip to content

Commit 6ff4243

Browse files
committed
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
1 parent 00a3738 commit 6ff4243

File tree

7 files changed

+118
-6
lines changed

7 files changed

+118
-6
lines changed

src/core/prompts/tools/new-task.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,34 @@ import { ToolArgs } from "./types"
22

33
export function getNewTaskDescription(_args: ToolArgs): string {
44
return `## new_task
5-
Description: This will let you create a new task instance in the chosen mode using your provided message.
5+
Description: This will let you create a new task instance in the chosen mode using your provided message and initial todo list.
66
77
Parameters:
88
- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
99
- message: (required) The initial user message or instructions for this new task.
10+
- todos: (required) A markdown checklist string defining the initial todo list for the task.
1011
1112
Usage:
1213
<new_task>
1314
<mode>your-mode-slug-here</mode>
1415
<message>Your initial instructions here</message>
16+
<todos>
17+
[ ] First task to complete
18+
[ ] Second task to complete
19+
[ ] Third task to complete
20+
</todos>
1521
</new_task>
1622
1723
Example:
1824
<new_task>
1925
<mode>code</mode>
20-
<message>Implement a new feature for the application.</message>
26+
<message>Implement user authentication</message>
27+
<todos>
28+
[ ] Set up auth middleware
29+
[ ] Create login endpoint
30+
[ ] Add session management
31+
[ ] Write tests
32+
</todos>
2133
</new_task>
2234
`
2335
}

src/core/task/Task.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export type TaskOptions = {
125125
rootTask?: Task
126126
parentTask?: Task
127127
taskNumber?: number
128+
initialTodos?: TodoItem[]
128129
onCreated?: (cline: Task) => void
129130
}
130131

@@ -270,6 +271,7 @@ export class Task extends EventEmitter<ClineEvents> {
270271
rootTask,
271272
parentTask,
272273
taskNumber = -1,
274+
initialTodos,
273275
onCreated,
274276
}: TaskOptions) {
275277
super()
@@ -324,6 +326,11 @@ export class Task extends EventEmitter<ClineEvents> {
324326
TelemetryService.instance.captureTaskCreated(this.taskId)
325327
}
326328

329+
// Initialize todo list if provided
330+
if (initialTodos) {
331+
this.todoList = initialTodos
332+
}
333+
327334
// Only set up diff strategy if diff is enabled
328335
if (this.diffEnabled) {
329336
// Default to old strategy, will be updated if experiment is enabled

src/core/tools/__tests__/newTaskTool.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ vi.mock("../../prompts/responses", () => ({
1414
},
1515
}))
1616

17+
vi.mock("../updateTodoListTool", () => ({
18+
parseMarkdownChecklist: vi.fn((md: string) => [
19+
{ id: "1", content: "Test todo 1", status: "pending" },
20+
{ id: "2", content: "Test todo 2", status: "pending" },
21+
]),
22+
}))
23+
1724
// Define a minimal type for the resolved value
1825
type MockClineInstance = { taskId: string }
1926

@@ -72,6 +79,7 @@ describe("newTaskTool", () => {
7279
params: {
7380
mode: "code",
7481
message: "Review this: \\\\@file1.txt and also \\\\\\\\@file2.txt", // Input with \\@ and \\\\@
82+
todos: "[ ] Test todo 1\n[ ] Test todo 2",
7583
},
7684
partial: false,
7785
}
@@ -93,6 +101,12 @@ describe("newTaskTool", () => {
93101
"Review this: \\@file1.txt and also \\\\\\@file2.txt", // Unit Test Expectation: \\@ -> \@, \\\\@ -> \\\\@
94102
undefined,
95103
mockCline,
104+
{
105+
initialTodos: [
106+
{ id: "1", content: "Test todo 1", status: "pending" },
107+
{ id: "2", content: "Test todo 2", status: "pending" },
108+
],
109+
},
96110
)
97111

98112
// Verify side effects
@@ -109,6 +123,7 @@ describe("newTaskTool", () => {
109123
params: {
110124
mode: "code",
111125
message: "This is already unescaped: \\@file1.txt",
126+
todos: "[ ] Test todo",
112127
},
113128
partial: false,
114129
}
@@ -126,6 +141,12 @@ describe("newTaskTool", () => {
126141
"This is already unescaped: \\@file1.txt", // Expected: \@ remains \@
127142
undefined,
128143
mockCline,
144+
{
145+
initialTodos: [
146+
{ id: "1", content: "Test todo 1", status: "pending" },
147+
{ id: "2", content: "Test todo 2", status: "pending" },
148+
],
149+
},
129150
)
130151
})
131152

@@ -136,6 +157,7 @@ describe("newTaskTool", () => {
136157
params: {
137158
mode: "code",
138159
message: "A normal mention @file1.txt",
160+
todos: "[ ] Test todo",
139161
},
140162
partial: false,
141163
}
@@ -153,6 +175,12 @@ describe("newTaskTool", () => {
153175
"A normal mention @file1.txt", // Expected: @ remains @
154176
undefined,
155177
mockCline,
178+
{
179+
initialTodos: [
180+
{ id: "1", content: "Test todo 1", status: "pending" },
181+
{ id: "2", content: "Test todo 2", status: "pending" },
182+
],
183+
},
156184
)
157185
})
158186

@@ -163,6 +191,7 @@ describe("newTaskTool", () => {
163191
params: {
164192
mode: "code",
165193
message: "Mix: @file0.txt, \\@file1.txt, \\\\@file2.txt, \\\\\\\\@file3.txt",
194+
todos: "[ ] Test todo",
166195
},
167196
partial: false,
168197
}
@@ -180,8 +209,41 @@ describe("newTaskTool", () => {
180209
"Mix: @file0.txt, \\@file1.txt, \\@file2.txt, \\\\\\@file3.txt", // Unit Test Expectation: @->@, \@->\@, \\@->\@, \\\\@->\\\\@
181210
undefined,
182211
mockCline,
212+
{
213+
initialTodos: [
214+
{ id: "1", content: "Test todo 1", status: "pending" },
215+
{ id: "2", content: "Test todo 2", status: "pending" },
216+
],
217+
},
183218
)
184219
})
185220

221+
it("should handle missing todos parameter", async () => {
222+
const block: ToolUse = {
223+
type: "tool_use",
224+
name: "new_task",
225+
params: {
226+
mode: "code",
227+
message: "Test message",
228+
// todos is missing
229+
},
230+
partial: false,
231+
}
232+
233+
await newTaskTool(
234+
mockCline as any,
235+
block,
236+
mockAskApproval,
237+
mockHandleError,
238+
mockPushToolResult,
239+
mockRemoveClosingTag,
240+
)
241+
242+
// Should call sayAndCreateMissingParamError for todos
243+
expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "todos")
244+
expect(mockCline.consecutiveMistakeCount).toBe(1)
245+
expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task")
246+
})
247+
186248
// Add more tests for error handling (missing params, invalid mode, approval denied) if needed
187249
})

src/core/tools/newTaskTool.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Task } from "../task/Task"
55
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
66
import { formatResponse } from "../prompts/responses"
77
import { t } from "../../i18n"
8+
import { parseMarkdownChecklist } from "./updateTodoListTool"
89

910
export async function newTaskTool(
1011
cline: Task,
@@ -16,13 +17,15 @@ export async function newTaskTool(
1617
) {
1718
const mode: string | undefined = block.params.mode
1819
const message: string | undefined = block.params.message
20+
const todos: string | undefined = block.params.todos
1921

2022
try {
2123
if (block.partial) {
2224
const partialMessage = JSON.stringify({
2325
tool: "newTask",
2426
mode: removeClosingTag("mode", mode),
2527
content: removeClosingTag("message", message),
28+
todos: removeClosingTag("todos", todos),
2629
})
2730

2831
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
@@ -42,6 +45,13 @@ export async function newTaskTool(
4245
return
4346
}
4447

48+
if (!todos) {
49+
cline.consecutiveMistakeCount++
50+
cline.recordToolError("new_task")
51+
pushToolResult(await cline.sayAndCreateMissingParamError("new_task", "todos"))
52+
return
53+
}
54+
4555
cline.consecutiveMistakeCount = 0
4656
// Un-escape one level of backslashes before '@' for hierarchical subtasks
4757
// Un-escape one level: \\@ -> \@ (removes one backslash for hierarchical subtasks)
@@ -55,10 +65,24 @@ export async function newTaskTool(
5565
return
5666
}
5767

68+
// Parse the todos markdown
69+
let parsedTodos
70+
try {
71+
parsedTodos = parseMarkdownChecklist(todos)
72+
} catch (error) {
73+
pushToolResult(
74+
formatResponse.toolError(
75+
`Invalid todos markdown format: ${error instanceof Error ? error.message : String(error)}`,
76+
),
77+
)
78+
return
79+
}
80+
5881
const toolMessage = JSON.stringify({
5982
tool: "newTask",
6083
mode: targetMode.name,
6184
content: message,
85+
todos: parsedTodos,
6286
})
6387

6488
const didApprove = await askApproval("tool", toolMessage)
@@ -81,7 +105,9 @@ export async function newTaskTool(
81105
cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug
82106

83107
// Create new task instance first (this preserves parent's current mode in its history)
84-
const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline)
108+
const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline, {
109+
initialTodos: parsedTodos,
110+
})
85111
if (!newCline) {
86112
pushToolResult(t("tools:newTask.errors.policy_restriction"))
87113
return

src/core/tools/updateTodoListTool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function normalizeStatus(status: string | undefined): TodoStatus {
100100
return "pending"
101101
}
102102

103-
function parseMarkdownChecklist(md: string): TodoItem[] {
103+
export function parseMarkdownChecklist(md: string): TodoItem[] {
104104
if (typeof md !== "string") return []
105105
const lines = md
106106
.split(/\r?\n/)

src/core/webview/ClineProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,12 @@ export class ClineProvider
532532
options: Partial<
533533
Pick<
534534
TaskOptions,
535-
"enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments"
535+
| "enableDiff"
536+
| "enableCheckpoints"
537+
| "fuzzyMatchThreshold"
538+
| "consecutiveMistakeLimit"
539+
| "experiments"
540+
| "initialTodos"
536541
>
537542
> = {},
538543
) {

src/shared/tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export interface SwitchModeToolUse extends ToolUse {
155155

156156
export interface NewTaskToolUse extends ToolUse {
157157
name: "new_task"
158-
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message">>
158+
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message" | "todos">>
159159
}
160160

161161
export interface SearchAndReplaceToolUse extends ToolUse {

0 commit comments

Comments
 (0)