diff --git a/README.md b/README.md index cdb7bc0..c9066d0 100644 --- a/README.md +++ b/README.md @@ -57,20 +57,40 @@ The extension renders a persistent widget above the editor: ### `TaskCreate` -Create a structured task. Used proactively for complex multi-step work. +Create one structured task, or create multiple tasks in one call with `tasks`. Used proactively for complex multi-step work. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `subject` | string | yes | Brief imperative title | -| `description` | string | yes | Detailed context and acceptance criteria | +| `subject` | string | yes* | Brief imperative title | +| `description` | string | yes* | Detailed context and acceptance criteria | | `activeForm` | string | no | Present continuous form for spinner (e.g., "Running tests") | | `agentType` | string | no | Agent type for subagent execution (e.g., `"general-purpose"`, `"Explore"`) | | `metadata` | object | no | Arbitrary key-value pairs | +| `tasks` | array | no* | Bulk task list. Each item accepts `subject`, `description`, `activeForm`, `agentType`, and `metadata`. | + +\* Provide either top-level `subject` + `description`, or a non-empty `tasks` array. ``` → Task #1 created successfully: Fix authentication bug ``` +Bulk example: + +```json +{ + "tasks": [ + { "subject": "Design API", "description": "Define endpoints and data contracts" }, + { "subject": "Implement API", "description": "Build endpoints after design is approved" } + ] +} +``` + +``` +→ Created 2 tasks successfully: + Task #1: Design API + Task #2: Implement API +``` + ### `TaskList` List all tasks with status, owner, and blocked-by info. diff --git a/src/index.ts b/src/index.ts index 5bdd4de..578cbc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -426,9 +426,12 @@ NOTE that you should not use this tool if there is only one trivial task to do. ## Task Fields +TaskCreate supports either a single task using top-level fields, or multiple tasks using the \`tasks\` array. + - **subject**: A brief, actionable title in imperative form (e.g., "Fix authentication bug in login flow") - **description**: Detailed description of what needs to be done, including context and acceptance criteria - **activeForm** (optional): Present continuous form shown in the spinner when the task is in_progress (e.g., "Fixing authentication bug"). If omitted, the spinner shows the subject instead. +- **tasks** (optional): Array of task objects with the same fields for bulk creation. Use this when creating multiple related tasks at once. All tasks are created with status \`pending\`. @@ -445,20 +448,52 @@ All tasks are created with status \`pending\`. "Use TaskList to check for available work after completing a task.", ], parameters: Type.Object({ - subject: Type.String({ description: "A brief title for the task" }), - description: Type.String({ description: "A detailed description of what needs to be done" }), + subject: Type.Optional(Type.String({ description: "A brief title for the task" })), + description: Type.Optional(Type.String({ description: "A detailed description of what needs to be done" })), activeForm: Type.Optional(Type.String({ description: "Present continuous form shown in spinner when in_progress (e.g., 'Running tests')" })), agentType: Type.Optional(Type.String({ description: "Agent type for subagent execution (e.g., 'general-purpose', 'Explore'). Tasks with agentType can be started via TaskExecute." })), metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Arbitrary metadata to attach to the task" })), + tasks: Type.Optional(Type.Array(Type.Object({ + subject: Type.String({ description: "A brief title for the task" }), + description: Type.String({ description: "A detailed description of what needs to be done" }), + activeForm: Type.Optional(Type.String({ description: "Present continuous form shown in spinner when in_progress (e.g., 'Running tests')" })), + agentType: Type.Optional(Type.String({ description: "Agent type for subagent execution (e.g., 'general-purpose', 'Explore'). Tasks with agentType can be started via TaskExecute." })), + metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Arbitrary metadata to attach to the task" })), + }), { description: "Create multiple tasks in one call. Each task accepts subject, description, activeForm, agentType, and metadata." })), }), execute(_toolCallId, params, _signal, _onUpdate, _ctx) { autoClear.resetBatchCountdown(); - const meta = params.metadata ?? {}; - if (params.agentType) meta.agentType = params.agentType; - const task = store.create(params.subject, params.description, params.activeForm, Object.keys(meta).length > 0 ? meta : undefined); + const inputTasks = params.tasks ?? ( + params.subject && params.description + ? [{ + subject: params.subject, + description: params.description, + activeForm: params.activeForm, + agentType: params.agentType, + metadata: params.metadata, + }] + : [] + ); + + if (inputTasks.length === 0) { + throw new Error("TaskCreate requires either subject + description, or a non-empty tasks array"); + } + + const created = inputTasks.map((input) => { + const meta = { ...(input.metadata ?? {}) }; + if (input.agentType) meta.agentType = input.agentType; + return store.create(input.subject, input.description, input.activeForm, Object.keys(meta).length > 0 ? meta : undefined); + }); + widget.update(); - return Promise.resolve(textResult(`Task #${task.id} created successfully: ${task.subject}`)); + if (created.length === 1) { + const task = created[0]; + return Promise.resolve(textResult(`Task #${task.id} created successfully: ${task.subject}`)); + } + return Promise.resolve(textResult( + `Created ${created.length} tasks successfully:\n${created.map(task => `Task #${task.id}: ${task.subject}`).join("\n")}`, + )); }, }); diff --git a/test/subagent-integration.test.ts b/test/subagent-integration.test.ts index 2bdb36c..e59969a 100644 --- a/test/subagent-integration.test.ts +++ b/test/subagent-integration.test.ts @@ -481,6 +481,32 @@ describe("Standalone operation (no subagents extension)", () => { expect(result.content[0].text).toContain("Write tests"); }); + it("TaskCreate can create multiple tasks in one call", async () => { + const result = await mock.executeTool("TaskCreate", { + tasks: [ + { subject: "Design API", description: "Define endpoints", agentType: "Explore" }, + { subject: "Implement API", description: "Build endpoints", activeForm: "Building endpoints" }, + ], + }); + + expect(result.content[0].text).toContain("Created 2 tasks successfully"); + expect(result.content[0].text).toContain("Task #1: Design API"); + expect(result.content[0].text).toContain("Task #2: Implement API"); + + const first = await mock.executeTool("TaskGet", { taskId: "1" }); + expect(first.content[0].text).toContain("Metadata: {\"agentType\":\"Explore\"}"); + + const list = await mock.executeTool("TaskList", {}); + expect(list.content[0].text).toContain("#1 [pending] Design API"); + expect(list.content[0].text).toContain("#2 [pending] Implement API"); + }); + + it("TaskCreate rejects calls without single-task fields or bulk tasks", async () => { + await expect(mock.executeTool("TaskCreate", {})).rejects.toThrow( + "TaskCreate requires either subject + description, or a non-empty tasks array", + ); + }); + it("TaskList works without subagents", async () => { await mock.executeTool("TaskCreate", { subject: "A", description: "desc" }); await mock.executeTool("TaskCreate", { subject: "B", description: "desc" });