From edec9759fc84ec56de16a4cc631eb76f4e4bbeb3 Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Sun, 22 Feb 2026 11:29:12 +0000 Subject: [PATCH] feat(jira): add description support for JIRA subtasks created via AddChecklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The planning agent writes detailed step descriptions (files to modify, specific changes, testing strategy) in the card body, but this context was lost when creating bare JIRA subtasks with only a summary. This threads an optional description through the AddChecklist gadget → PMProvider.addChecklistItem() → JIRA adapter, where it is converted to ADF and set on the subtask. Trello checklist items don't support descriptions, so the Trello adapter ignores the field. The schema change is backward-compatible: items accept string | {name, description?}. Co-Authored-By: Claude Opus 4.6 --- src/agents/prompts/templates/planning.eta | 5 +- src/gadgets/pm/AddChecklist.ts | 37 +++++++-- src/gadgets/pm/core/addChecklist.ts | 8 +- src/pm/jira/adapter.ts | 8 +- src/pm/trello/adapter.ts | 7 +- src/pm/types.ts | 7 +- .../unit/gadgets/pm/core/addChecklist.test.ts | 80 ++++++++++++++++++- tests/unit/pm/jira/adapter.test.ts | 34 ++++++++ 8 files changed, 170 insertions(+), 16 deletions(-) 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,