From 32065697723bf30b5e99f3cb5e2aea2c2c6db059 Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Wed, 25 Feb 2026 10:15:58 +0000 Subject: [PATCH] fix(jira): resolve missing stories status, storiesListId, and progress model context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The splitting agent on JIRA projects failed because: 1. The PM wizard's JIRA_STATUS_SLOTS omitted 'stories', so users couldn't configure the stories status mapping. 2. promptContext.ts only read storiesListId from Trello config, leaving it undefined for JIRA projects — the agent hallucinated a container ID. 3. The progress model had no agent-role context, generating misleading messages like "split main.py" instead of referencing work item breakdown. Changes: - Add 'stories' to JIRA_STATUS_SLOTS in pm-wizard - Fall back to JIRA project key for storiesListId in promptContext - Auto-transition new JIRA issues to stories status after creation - Extract AGENT_ROLE_HINTS to shared agentMessages.ts, wire into both ackMessageGenerator and progressModel - Add tests for all new behavior Co-Authored-By: Claude Opus 4.6 --- src/agents/shared/promptContext.ts | 4 +- src/backends/progressModel.ts | 3 +- src/config/agentMessages.ts | 19 ++++++++ src/pm/jira/adapter.ts | 15 ++++++ src/router/ackMessageGenerator.ts | 5 +- .../unit/agents/shared/promptContext.test.ts | 14 ++++++ tests/unit/backends/progressModel.test.ts | 22 +++++++++ tests/unit/pm/jira/adapter.test.ts | 47 +++++++++++++++++++ tests/unit/router/ackMessageGenerator.test.ts | 5 ++ web/src/components/projects/pm-wizard.tsx | 1 + 10 files changed, 130 insertions(+), 5 deletions(-) diff --git a/src/agents/shared/promptContext.ts b/src/agents/shared/promptContext.ts index c75252a6..b769d530 100644 --- a/src/agents/shared/promptContext.ts +++ b/src/agents/shared/promptContext.ts @@ -1,4 +1,4 @@ -import { getTrelloConfig } from '../../pm/config.js'; +import { getJiraConfig, getTrelloConfig } from '../../pm/config.js'; import { getPMProvider } from '../../pm/index.js'; import type { ProjectConfig } from '../../types/index.js'; import type { PromptContext } from '../prompts/index.js'; @@ -30,7 +30,7 @@ export function buildPromptContext( cardUrl: cardId ? pmProvider.getWorkItemUrl(cardId) : undefined, projectId: project.id, baseBranch: project.baseBranch, - storiesListId: getTrelloConfig(project)?.lists?.stories, + storiesListId: getTrelloConfig(project)?.lists?.stories ?? getJiraConfig(project)?.projectKey, processedLabelId: getTrelloConfig(project)?.labels?.processed, pmType: pmProvider.type, workItemNoun: isJira ? 'issue' : 'card', diff --git a/src/backends/progressModel.ts b/src/backends/progressModel.ts index 22bd7241..5fa7ed33 100644 --- a/src/backends/progressModel.ts +++ b/src/backends/progressModel.ts @@ -7,7 +7,7 @@ import { LLMist, type ModelSpec } from 'llmist'; -import { getAgentLabel } from '../config/agentMessages.js'; +import { AGENT_ROLE_HINTS, getAgentLabel } from '../config/agentMessages.js'; import type { Todo } from '../gadgets/todo/storage.js'; export interface ProgressContext { @@ -40,6 +40,7 @@ function formatProgressUserPrompt(context: ProgressContext): string { const sections: string[] = [ `Agent: ${agentType}`, + `Agent role: ${AGENT_ROLE_HINTS[agentType] ?? 'Processes the request'}`, `Progress header: **${emoji} ${label}** (${Math.round(elapsedMinutes)} min)`, `Task: ${taskDescription.slice(0, 500)}`, `Time elapsed: ${Math.round(elapsedMinutes)} minutes`, diff --git a/src/config/agentMessages.ts b/src/config/agentMessages.ts index 00bde289..127e8e41 100644 --- a/src/config/agentMessages.ts +++ b/src/config/agentMessages.ts @@ -25,6 +25,25 @@ export function getAgentLabel(agentType: string): { emoji: string; label: string return AGENT_LABELS[agentType] ?? { emoji: '⚙️', label: 'Progress Update' }; } +/** + * Agent role hints — give LLMs context about what each agent type does. + * + * Used by: + * - ackMessageGenerator.ts — contextual acknowledgment messages + * - progressModel.ts — progress update generation + */ +export const AGENT_ROLE_HINTS: Record = { + splitting: 'Breaks down a feature plan into smaller, ordered work items (subtasks)', + planning: 'Studies the codebase and designs a step-by-step implementation plan', + implementation: 'Writes code, runs tests, and prepares a pull request', + review: 'Reviews pull request changes for quality and correctness', + 'respond-to-planning-comment': 'Reads user feedback and updates the plan accordingly', + 'respond-to-review': 'Addresses code review feedback by making requested changes', + 'respond-to-pr-comment': 'Reads a PR comment and takes action', + 'respond-to-ci': 'Analyzes failed CI checks and works on a fix', + debug: 'Analyzes session logs to identify what went wrong', +}; + /** * Human-readable initial messages per agent type. * diff --git a/src/pm/jira/adapter.ts b/src/pm/jira/adapter.ts index 34786c4d..ef906daf 100644 --- a/src/pm/jira/adapter.ts +++ b/src/pm/jira/adapter.ts @@ -149,6 +149,21 @@ export class JiraPMProvider implements PMProvider { ...(config.labels?.length ? { labels: config.labels } : {}), }); const key = result.key ?? ''; + + // Transition to stories status if configured (mirrors Trello's stories list) + const storiesStatus = this.config.statuses?.stories; + if (storiesStatus) { + try { + await this.moveWorkItem(key, storiesStatus); + } catch (err) { + logger.warn('[JIRA] Failed to transition new issue to stories status', { + issueKey: key, + targetStatus: storiesStatus, + error: String(err), + }); + } + } + return { id: key, title: config.title, diff --git a/src/router/ackMessageGenerator.ts b/src/router/ackMessageGenerator.ts index b856f266..7e400d1a 100644 --- a/src/router/ackMessageGenerator.ts +++ b/src/router/ackMessageGenerator.ts @@ -8,7 +8,7 @@ import { LLMist, type ModelSpec } from 'llmist'; -import { INITIAL_MESSAGES } from '../config/agentMessages.js'; +import { AGENT_ROLE_HINTS, INITIAL_MESSAGES } from '../config/agentMessages.js'; import { CUSTOM_MODELS } from '../config/customModels.js'; import { getOrgCredential, loadConfig } from '../config/provider.js'; import { logger } from '../utils/logging.js'; @@ -240,7 +240,8 @@ async function callAckModel( contextSnippet: string, ): Promise { const client = new LLMist({ customModels: CUSTOM_MODELS as ModelSpec[] }); - const userPrompt = `Agent type: ${agentType}\n\nRequest context:\n${contextSnippet}`; + const roleHint = AGENT_ROLE_HINTS[agentType] ?? 'Processes the request'; + const userPrompt = `Agent type: ${agentType}\nAgent role: ${roleHint}\n\nRequest context:\n${contextSnippet}`; const result = await client.text.complete(userPrompt, { model, diff --git a/tests/unit/agents/shared/promptContext.test.ts b/tests/unit/agents/shared/promptContext.test.ts index 34f7f535..e14a6d46 100644 --- a/tests/unit/agents/shared/promptContext.test.ts +++ b/tests/unit/agents/shared/promptContext.test.ts @@ -133,6 +133,20 @@ describe('buildPromptContext', () => { const ctx = buildPromptContext('PROJ-123', makeProject() as never); expect(ctx.pmType).toBe('jira'); }); + + it('sets storiesListId to JIRA project key when no Trello config', () => { + const jiraProject = makeProject({ + trello: undefined, + pm: { type: 'jira' }, + jira: { + projectKey: 'BTS', + baseUrl: 'https://company.atlassian.net', + statuses: { todo: 'To Do' }, + }, + }); + const ctx = buildPromptContext('BTS-148', jiraProject as never); + expect(ctx.storiesListId).toBe('BTS'); + }); }); describe('with prContext', () => { diff --git a/tests/unit/backends/progressModel.test.ts b/tests/unit/backends/progressModel.test.ts index b77a3033..d3724db8 100644 --- a/tests/unit/backends/progressModel.test.ts +++ b/tests/unit/backends/progressModel.test.ts @@ -103,4 +103,26 @@ describe('callProgressModel', () => { expect(mockTextComplete).toHaveBeenCalledTimes(1); expect(MockLLMist).toHaveBeenCalledTimes(1); }); + + it('includes agent role hint in the user prompt', async () => { + mockTextComplete.mockResolvedValue('Progress update.'); + + await callProgressModel('test-model', makeContext({ agentType: 'splitting' }), []); + + const userPrompt = mockTextComplete.mock.calls[0][0] as string; + expect(userPrompt).toContain('Agent: splitting'); + expect(userPrompt).toContain( + 'Agent role: Breaks down a feature plan into smaller, ordered work items (subtasks)', + ); + }); + + it('uses fallback role hint for unknown agent types', async () => { + mockTextComplete.mockResolvedValue('Progress update.'); + + await callProgressModel('test-model', makeContext({ agentType: 'unknown-agent' }), []); + + const userPrompt = mockTextComplete.mock.calls[0][0] as string; + expect(userPrompt).toContain('Agent: unknown-agent'); + expect(userPrompt).toContain('Agent role: Processes the request'); + }); }); diff --git a/tests/unit/pm/jira/adapter.test.ts b/tests/unit/pm/jira/adapter.test.ts index 7276baab..e3f52ebb 100644 --- a/tests/unit/pm/jira/adapter.test.ts +++ b/tests/unit/pm/jira/adapter.test.ts @@ -257,6 +257,53 @@ describe('JiraPMProvider', () => { expect.not.objectContaining({ labels: expect.anything() }), ); }); + + it('transitions new issue to stories status when configured', async () => { + const storiesProvider = new JiraPMProvider({ + ...mockConfig, + statuses: { ...mockConfig.statuses, stories: 'Stories' }, + }); + mockJiraClient.createIssue.mockResolvedValue({ key: 'PROJ-100' }); + mockJiraClient.getTransitions.mockResolvedValue([ + { id: '31', name: 'Stories', to: { name: 'Stories' } }, + ]); + mockJiraClient.transitionIssue.mockResolvedValue(undefined); + + await storiesProvider.createWorkItem({ + containerId: 'PROJ', + title: 'Story task', + }); + + expect(mockJiraClient.getTransitions).toHaveBeenCalledWith('PROJ-100'); + expect(mockJiraClient.transitionIssue).toHaveBeenCalledWith('PROJ-100', '31'); + }); + + it('does not transition when stories status is not configured', async () => { + mockJiraClient.createIssue.mockResolvedValue({ key: 'PROJ-101' }); + + await provider.createWorkItem({ + containerId: 'PROJ', + title: 'Regular task', + }); + + expect(mockJiraClient.getTransitions).not.toHaveBeenCalled(); + }); + + it('logs warning and continues when stories transition fails', async () => { + const storiesProvider = new JiraPMProvider({ + ...mockConfig, + statuses: { ...mockConfig.statuses, stories: 'Stories' }, + }); + mockJiraClient.createIssue.mockResolvedValue({ key: 'PROJ-102' }); + mockJiraClient.getTransitions.mockRejectedValue(new Error('API error')); + + const result = await storiesProvider.createWorkItem({ + containerId: 'PROJ', + title: 'Task with failing transition', + }); + + expect(result.id).toBe('PROJ-102'); + }); }); describe('listWorkItems', () => { diff --git a/tests/unit/router/ackMessageGenerator.test.ts b/tests/unit/router/ackMessageGenerator.test.ts index 4797398e..da10d716 100644 --- a/tests/unit/router/ackMessageGenerator.test.ts +++ b/tests/unit/router/ackMessageGenerator.test.ts @@ -36,6 +36,11 @@ vi.mock('../../../src/config/agentMessages.js', () => ({ '**📋 Splitting plan** — Reading the plan and splitting it into ordered work items...', review: '**🔍 Reviewing code** — Examining the PR changes for quality and correctness...', }, + AGENT_ROLE_HINTS: { + splitting: 'Breaks down a feature plan into smaller, ordered work items (subtasks)', + implementation: 'Writes code, runs tests, and prepares a pull request', + review: 'Reviews pull request changes for quality and correctness', + }, })); import { getOrgCredential, loadConfig } from '../../../src/config/provider.js'; diff --git a/web/src/components/projects/pm-wizard.tsx b/web/src/components/projects/pm-wizard.tsx index ff47a626..e03ca409 100644 --- a/web/src/components/projects/pm-wizard.tsx +++ b/web/src/components/projects/pm-wizard.tsx @@ -590,6 +590,7 @@ const TRELLO_LABEL_SLOTS = ['readyToProcess', 'processing', 'processed', 'error' const JIRA_STATUS_SLOTS = [ 'splitting', + 'stories', 'planning', 'todo', 'inProgress',