diff --git a/src/agents/prompts/templates/planning.eta b/src/agents/prompts/templates/planning.eta index 2931e8a9..40cfcf73 100644 --- a/src/agents/prompts/templates/planning.eta +++ b/src/agents/prompts/templates/planning.eta @@ -101,7 +101,8 @@ Update the <%= it.workItemNoun || 'card' %> description with **emoji section hea **IMPORTANT:** - After updating the <%= it.workItemNoun || 'card' %>, ALWAYS call `AddChecklist` to create an interactive "📋 Implementation Steps" checklist with each step as an item. -- When referencing other <%= it.workItemNounPlural || 'cards' %> (related stories, dependencies), ALWAYS use markdown links: `[<%= it.workItemNounCap || 'Card' %> Title](URL)` +<% if (it.pmType === 'jira') { %>- When calling `AddChecklist`, pass items as objects with `name` and `description`. The description should include the files to modify, specific changes, and testing notes from the corresponding Implementation Step section. This becomes the JIRA subtask description. +<% } %>- When referencing other <%= it.workItemNounPlural || 'cards' %> (related stories, dependencies), ALWAYS use markdown links: `[<%= it.workItemNounCap || 'Card' %> Title](URL)` ## Comment Format @@ -124,7 +125,7 @@ Review the updated description and move to TODO when ready to implement! **<%= it.pmName || 'Trello' %> (for outputting your plan):** - `ReadWorkItem` - Read <%= it.workItemNoun || 'card' %> details (title, description, comments, labels) - `UpdateWorkItem` - Update <%= it.workItemNoun || 'card' %> title/description -- `AddChecklist` - Add an interactive checklist to a <%= it.workItemNoun || 'card' %> (use for implementation steps) +- `AddChecklist` - Add an interactive checklist to a <%= it.workItemNoun || 'card' %> (use for implementation steps)<% if (it.pmType === 'jira') { %>. Each item can include a `description` with files-to-modify, specific changes, and testing notes — these become subtask descriptions.<% } %> - `PostComment` - Post a comment on a <%= it.workItemNoun || 'card' %> **Codebase exploration (READ-ONLY):** diff --git a/src/gadgets/pm/AddChecklist.ts b/src/gadgets/pm/AddChecklist.ts index b1eb94c7..25cf3d6f 100644 --- a/src/gadgets/pm/AddChecklist.ts +++ b/src/gadgets/pm/AddChecklist.ts @@ -11,20 +11,45 @@ export class AddChecklist extends Gadget({ checklistName: z .string() .describe('Name of the checklist (e.g., "Acceptance Criteria" or "Implementation Steps")'), - items: z.array(z.string()).min(1).describe('List of checklist items to add'), + items: z + .array( + z.union([ + z.string(), + z.object({ + name: z.string().describe('Checklist item name / subtask title'), + description: z + .string() + .optional() + .describe( + 'Detailed description (used as JIRA subtask description, ignored for Trello)', + ), + }), + ]), + ) + .min(1) + .describe( + 'List of checklist items to add. Use objects with name+description for richer subtasks.', + ), }), examples: [ { params: { - workItemId: 'abc123', + workItemId: 'PROJ-42', checklistName: 'Implementation Steps', items: [ - 'Add reset password endpoint to API', - 'Create email template for reset link', - 'Add password validation logic', + { + name: 'Add reset password endpoint to API', + description: + '**Files:** `src/api/auth.ts`\n- Add POST /auth/reset-password route\n- Validate email format and lookup user\n- Generate time-limited reset token', + }, + { + name: 'Create email template for reset link', + description: + '**Files:** `src/templates/reset-password.html`\n- Create responsive HTML email template\n- Include reset link with token parameter', + }, ], }, - comment: 'Add implementation steps checklist to a work item', + comment: 'Add implementation steps with descriptions to a JIRA issue', }, ], }) { diff --git a/src/gadgets/pm/core/addChecklist.ts b/src/gadgets/pm/core/addChecklist.ts index bf4148be..eb47f781 100644 --- a/src/gadgets/pm/core/addChecklist.ts +++ b/src/gadgets/pm/core/addChecklist.ts @@ -1,9 +1,11 @@ import { getPMProvider } from '../../../pm/index.js'; +export type ChecklistItemInput = string | { name: string; description?: string }; + export interface AddChecklistParams { workItemId: string; checklistName: string; - items: string[]; + items: ChecklistItemInput[]; } export async function addChecklist(params: AddChecklistParams): Promise { @@ -11,7 +13,9 @@ export async function addChecklist(params: AddChecklistParams): Promise const checklist = await provider.createChecklist(params.workItemId, params.checklistName); for (const item of params.items) { - await provider.addChecklistItem(checklist.id, item); + const name = typeof item === 'string' ? item : item.name; + const description = typeof item === 'string' ? undefined : item.description; + await provider.addChecklistItem(checklist.id, name, false, description); } return `Checklist "${params.checklistName}" created with ${params.items.length} items on work item ${params.workItemId}`; diff --git a/src/pm/jira/adapter.ts b/src/pm/jira/adapter.ts index 70151ba5..1673da5b 100644 --- a/src/pm/jira/adapter.ts +++ b/src/pm/jira/adapter.ts @@ -242,7 +242,12 @@ export class JiraPMProvider implements PMProvider { }; } - async addChecklistItem(_checklistId: string, name: string, _checked = false): Promise { + async addChecklistItem( + _checklistId: string, + name: string, + _checked = false, + description?: string, + ): Promise { // Extract parent issue key from checklistId format: "checklist-PROJ-123-timestamp" // or "subtasks-PROJ-123" // Use \d{10,} to only strip timestamps (10+ digits), not issue numbers like PROJ-5 @@ -258,6 +263,7 @@ export class JiraPMProvider implements PMProvider { parent: { key: parentKey }, summary: name, issuetype: { name: issueType }, + ...(description ? { description: markdownToAdf(description) } : {}), }); } diff --git a/src/pm/trello/adapter.ts b/src/pm/trello/adapter.ts index c558f90f..d2f392cf 100644 --- a/src/pm/trello/adapter.ts +++ b/src/pm/trello/adapter.ts @@ -146,7 +146,12 @@ export class TrelloPMProvider implements PMProvider { }; } - async addChecklistItem(checklistId: string, name: string, checked = false): Promise { + async addChecklistItem( + checklistId: string, + name: string, + checked = false, + _description?: string, + ): Promise { await trelloClient.addChecklistItem(checklistId, name, checked); } diff --git a/src/pm/types.ts b/src/pm/types.ts index a5d91e92..6654a784 100644 --- a/src/pm/types.ts +++ b/src/pm/types.ts @@ -80,7 +80,12 @@ export interface PMProvider { // Checklists getChecklists(workItemId: string): Promise; createChecklist(workItemId: string, name: string): Promise; - addChecklistItem(checklistId: string, name: string, checked?: boolean): Promise; + addChecklistItem( + checklistId: string, + name: string, + checked?: boolean, + description?: string, + ): Promise; updateChecklistItem(workItemId: string, checkItemId: string, complete: boolean): Promise; // Attachments & custom fields diff --git a/tests/unit/gadgets/pm/core/addChecklist.test.ts b/tests/unit/gadgets/pm/core/addChecklist.test.ts index 1f5132c4..8e1131d5 100644 --- a/tests/unit/gadgets/pm/core/addChecklist.test.ts +++ b/tests/unit/gadgets/pm/core/addChecklist.test.ts @@ -15,7 +15,7 @@ beforeEach(() => { }); describe('addChecklist', () => { - it('creates checklist and adds items', async () => { + it('creates checklist and adds string items', async () => { mockProvider.createChecklist.mockResolvedValue({ id: 'cl1', name: 'My Tasks', @@ -32,11 +32,85 @@ describe('addChecklist', () => { expect(mockProvider.createChecklist).toHaveBeenCalledWith('item1', 'My Tasks'); expect(mockProvider.addChecklistItem).toHaveBeenCalledTimes(2); - expect(mockProvider.addChecklistItem).toHaveBeenCalledWith('cl1', 'Task A'); - expect(mockProvider.addChecklistItem).toHaveBeenCalledWith('cl1', 'Task B'); + expect(mockProvider.addChecklistItem).toHaveBeenCalledWith('cl1', 'Task A', false, undefined); + expect(mockProvider.addChecklistItem).toHaveBeenCalledWith('cl1', 'Task B', false, undefined); expect(result).toBe('Checklist "My Tasks" created with 2 items on work item item1'); }); + it('creates checklist and adds object items with descriptions', async () => { + mockProvider.createChecklist.mockResolvedValue({ + id: 'cl1', + name: 'Steps', + workItemId: 'PROJ-42', + items: [], + }); + mockProvider.addChecklistItem.mockResolvedValue(undefined); + + const result = await addChecklist({ + workItemId: 'PROJ-42', + checklistName: 'Steps', + items: [ + { name: 'Add endpoint', description: '**Files:** `src/api.ts`\n- Add POST route' }, + { name: 'Write tests' }, + ], + }); + + expect(mockProvider.addChecklistItem).toHaveBeenCalledTimes(2); + expect(mockProvider.addChecklistItem).toHaveBeenCalledWith( + 'cl1', + 'Add endpoint', + false, + '**Files:** `src/api.ts`\n- Add POST route', + ); + expect(mockProvider.addChecklistItem).toHaveBeenCalledWith( + 'cl1', + 'Write tests', + false, + undefined, + ); + expect(result).toBe('Checklist "Steps" created with 2 items on work item PROJ-42'); + }); + + it('handles mixed string and object items', async () => { + mockProvider.createChecklist.mockResolvedValue({ + id: 'cl1', + name: 'Mixed', + workItemId: 'item1', + items: [], + }); + mockProvider.addChecklistItem.mockResolvedValue(undefined); + + await addChecklist({ + workItemId: 'item1', + checklistName: 'Mixed', + items: [ + 'Simple string item', + { name: 'Object item', description: 'Detailed description' }, + 'Another string', + ], + }); + + expect(mockProvider.addChecklistItem).toHaveBeenCalledTimes(3); + expect(mockProvider.addChecklistItem).toHaveBeenCalledWith( + 'cl1', + 'Simple string item', + false, + undefined, + ); + expect(mockProvider.addChecklistItem).toHaveBeenCalledWith( + 'cl1', + 'Object item', + false, + 'Detailed description', + ); + expect(mockProvider.addChecklistItem).toHaveBeenCalledWith( + 'cl1', + 'Another string', + false, + undefined, + ); + }); + it('creates checklist with no items', async () => { mockProvider.createChecklist.mockResolvedValue({ id: 'cl1', diff --git a/tests/unit/pm/jira/adapter.test.ts b/tests/unit/pm/jira/adapter.test.ts index 82cfd111..e1d6339e 100644 --- a/tests/unit/pm/jira/adapter.test.ts +++ b/tests/unit/pm/jira/adapter.test.ts @@ -495,6 +495,40 @@ describe('JiraPMProvider', () => { expect(mockJiraClient.getIssueTypesForProject).toHaveBeenCalledOnce(); }); + it('passes description as ADF to createIssue when provided', async () => { + const adfDoc = { type: 'doc', version: 1, content: [{ type: 'paragraph' }] }; + mockMarkdownToAdf.mockReturnValue(adfDoc); + mockJiraClient.createIssue.mockResolvedValue({ key: 'PROJ-105' }); + + await provider.addChecklistItem( + 'checklist-PROJ-1-1234567890', + 'Subtask with description', + false, + '**Files:** `src/api.ts`\n- Add POST route', + ); + + expect(mockMarkdownToAdf).toHaveBeenCalledWith('**Files:** `src/api.ts`\n- Add POST route'); + expect(mockJiraClient.createIssue).toHaveBeenCalledWith( + expect.objectContaining({ + project: { key: 'PROJ' }, + parent: { key: 'PROJ-1' }, + summary: 'Subtask with description', + issuetype: { name: 'Sub-task' }, + description: adfDoc, + }), + ); + }); + + it('omits description from createIssue when not provided', async () => { + mockJiraClient.createIssue.mockResolvedValue({ key: 'PROJ-106' }); + + await provider.addChecklistItem('checklist-PROJ-1-1234567890', 'No description subtask'); + + expect(mockJiraClient.createIssue).toHaveBeenCalledWith( + expect.not.objectContaining({ description: expect.anything() }), + ); + }); + it('falls back to "Subtask" when no subtask type found', async () => { const providerNoConfig = new JiraPMProvider({ ...mockConfig,