From cd4c194143c70f71c7c0ab5b6c0247c7badcffe6 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Sat, 14 Feb 2026 18:33:53 -0500 Subject: [PATCH 1/5] fixes #596 --- src/lib/GitHubService.test.ts | 10 ++++++++++ src/lib/GitHubService.ts | 5 +++++ src/lib/IssueTracker.ts | 3 +++ src/lib/IssueTrackerFactory.test.ts | 26 ++++++++++++++++++++++++++ src/lib/IssueTrackerFactory.ts | 22 +++++++++++++++++++++- src/lib/LinearService.test.ts | 22 ++++++++++++++++++++++ src/lib/LinearService.ts | 8 ++++++++ src/utils/mcp.ts | 3 ++- 8 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/lib/IssueTrackerFactory.test.ts diff --git a/src/lib/GitHubService.test.ts b/src/lib/GitHubService.test.ts index f6babf10..eac97ed5 100644 --- a/src/lib/GitHubService.test.ts +++ b/src/lib/GitHubService.test.ts @@ -671,6 +671,16 @@ describe('GitHubService', () => { }) }) + describe('formatIssueId', () => { + it('should prefix number with #', () => { + expect(service.formatIssueId(123)).toBe('#123') + }) + + it('should prefix string with #', () => { + expect(service.formatIssueId('456')).toBe('#456') + }) + }) + describe('extractContext', () => { it('should extract context from issue', () => { const issue = { diff --git a/src/lib/GitHubService.ts b/src/lib/GitHubService.ts index 32c08828..73afadda 100644 --- a/src/lib/GitHubService.ts +++ b/src/lib/GitHubService.ts @@ -319,6 +319,11 @@ export class GitHubService implements IssueTracker { } } + // Formatting - provider-aware issue ID display + public formatIssueId(identifier: string | number): string { + return `#${identifier}` + } + // Utility methods public extractContext(entity: Issue | PullRequest): string { if ('branch' in entity) { diff --git a/src/lib/IssueTracker.ts b/src/lib/IssueTracker.ts index a562bf9d..64726e9a 100644 --- a/src/lib/IssueTracker.ts +++ b/src/lib/IssueTracker.ts @@ -41,6 +41,9 @@ export interface IssueTracker { // Status management - optional, check provider capabilities before calling moveIssueToInProgress?(identifier: string | number): Promise + // Formatting - provider-aware issue ID display + formatIssueId(identifier: string | number): string + // Context extraction - formats issue/PR for AI prompts extractContext(entity: Issue | PullRequest): string } diff --git a/src/lib/IssueTrackerFactory.test.ts b/src/lib/IssueTrackerFactory.test.ts new file mode 100644 index 00000000..456cc314 --- /dev/null +++ b/src/lib/IssueTrackerFactory.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest' +import { IssueTrackerFactory } from './IssueTrackerFactory.js' + +describe('IssueTrackerFactory', () => { + describe('formatIssueId', () => { + it('should prefix GitHub identifiers with #', () => { + expect(IssueTrackerFactory.formatIssueId('github', 123)).toBe('#123') + expect(IssueTrackerFactory.formatIssueId('github', '456')).toBe('#456') + }) + + it('should uppercase Linear identifiers', () => { + expect(IssueTrackerFactory.formatIssueId('linear', 'eng-123')).toBe('ENG-123') + expect(IssueTrackerFactory.formatIssueId('linear', 'ENG-456')).toBe('ENG-456') + }) + + it('should uppercase Jira identifiers', () => { + expect(IssueTrackerFactory.formatIssueId('jira', 'qlh-4404')).toBe('QLH-4404') + expect(IssueTrackerFactory.formatIssueId('jira', 'PROJ-100')).toBe('PROJ-100') + }) + + it('should default to # prefix for unknown provider types', () => { + // Cast to bypass type checking for the default case + expect(IssueTrackerFactory.formatIssueId('unknown' as 'github', 99)).toBe('#99') + }) + }) +}) diff --git a/src/lib/IssueTrackerFactory.ts b/src/lib/IssueTrackerFactory.ts index 8307667d..416ae148 100644 --- a/src/lib/IssueTrackerFactory.ts +++ b/src/lib/IssueTrackerFactory.ts @@ -7,7 +7,7 @@ import { LinearService, type LinearServiceConfig } from './LinearService.js' import type { IloomSettings } from './SettingsManager.js' import { getLogger } from '../utils/logger-context.js' -export type IssueTrackerProviderType = 'github' | 'linear' +export type IssueTrackerProviderType = 'github' | 'linear' | 'jira' /** * Factory for creating IssueTracker instances based on settings @@ -58,6 +58,26 @@ export class IssueTrackerFactory { } } + /** + * Format an issue identifier for display without needing a provider instance. + * GitHub issues get a "#" prefix, Linear/Jira identifiers are uppercased. + * + * @param providerType - The issue tracker provider type + * @param identifier - The issue identifier (number or string) + * @returns Formatted issue ID string + */ + static formatIssueId(providerType: IssueTrackerProviderType, identifier: string | number): string { + switch (providerType) { + case 'github': + return `#${identifier}` + case 'linear': + case 'jira': + return String(identifier).toUpperCase() + default: + return `#${identifier}` + } + } + /** * Get the configured provider name from settings * Defaults to 'github' if not configured diff --git a/src/lib/LinearService.test.ts b/src/lib/LinearService.test.ts index df80fded..c2dd1e4b 100644 --- a/src/lib/LinearService.test.ts +++ b/src/lib/LinearService.test.ts @@ -9,6 +9,28 @@ vi.mock('../utils/linear.js', () => ({ })) describe('LinearService', () => { + describe('formatIssueId', () => { + it('should uppercase a lowercase identifier', () => { + const service = new LinearService() + expect(service.formatIssueId('eng-123')).toBe('ENG-123') + }) + + it('should keep already-uppercase identifiers unchanged', () => { + const service = new LinearService() + expect(service.formatIssueId('ENG-456')).toBe('ENG-456') + }) + + it('should handle mixed case', () => { + const service = new LinearService() + expect(service.formatIssueId('Eng-789')).toBe('ENG-789') + }) + + it('should convert numeric identifier to uppercase string', () => { + const service = new LinearService() + expect(service.formatIssueId(123)).toBe('123') + }) + }) + describe('constructor', () => { let originalToken: string | undefined diff --git a/src/lib/LinearService.ts b/src/lib/LinearService.ts index f26d0456..441a298a 100644 --- a/src/lib/LinearService.ts +++ b/src/lib/LinearService.ts @@ -192,6 +192,14 @@ export class LinearService implements IssueTracker { await updateLinearIssueState(String(identifier), 'In Progress') } + /** + * Format a Linear issue identifier for display + * Uppercases to match Linear convention (e.g., "eng-123" -> "ENG-123") + */ + public formatIssueId(identifier: string | number): string { + return String(identifier).toUpperCase() + } + /** * Extract issue context for AI prompts * @param entity - Issue (Linear doesn't have PRs) diff --git a/src/utils/mcp.ts b/src/utils/mcp.ts index 72084725..10399d9e 100644 --- a/src/utils/mcp.ts +++ b/src/utils/mcp.ts @@ -4,6 +4,7 @@ import { getRepoInfo } from './github.js' import { logger } from './logger.js' import type { IloomSettings } from '../lib/SettingsManager.js' import type { LoomMetadata } from '../lib/MetadataManager.js' +import type { IssueTrackerProviderType } from '../lib/IssueTrackerFactory.js' /** * Generate MCP configuration for issue management @@ -18,7 +19,7 @@ import type { LoomMetadata } from '../lib/MetadataManager.js' export async function generateIssueManagementMcpConfig( contextType?: 'issue' | 'pr', repo?: string, - provider: 'github' | 'linear' = 'github', + provider: IssueTrackerProviderType = 'github', settings?: IloomSettings, draftPrNumber?: number ): Promise[]> { From cf184c94c9685832078bc6fcbe4bce5f25611c72 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Sat, 14 Feb 2026 18:42:33 -0500 Subject: [PATCH 2/5] fixes #600 --- src/commands/cleanup.test.ts | 26 +++++++++++++------------- src/commands/cleanup.ts | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/commands/cleanup.test.ts b/src/commands/cleanup.test.ts index e1572c73..6c986f69 100644 --- a/src/commands/cleanup.test.ts +++ b/src/commands/cleanup.test.ts @@ -129,7 +129,7 @@ describe('CleanupCommand', () => { }) expect(logger.info).toHaveBeenCalledWith('Cleanup mode: issue') - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #42...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #42...') }) it('should handle issue mode with number 1', async () => { @@ -137,7 +137,7 @@ describe('CleanupCommand', () => { options: { issue: 1 } }) - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #1...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #1...') }) it('should handle issue mode with large number', async () => { @@ -145,7 +145,7 @@ describe('CleanupCommand', () => { options: { issue: 999 } }) - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #999...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #999...') }) }) @@ -157,7 +157,7 @@ describe('CleanupCommand', () => { }) expect(logger.info).toHaveBeenCalledWith('Cleanup mode: issue') - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #42...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #42...') }) it('should detect "123" as issue number', async () => { @@ -166,7 +166,7 @@ describe('CleanupCommand', () => { options: {} }) - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #123...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #123...') }) it('should detect "1" as issue number', async () => { @@ -175,7 +175,7 @@ describe('CleanupCommand', () => { options: {} }) - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #1...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #1...') }) it('should detect "0" as issue number (edge case)', async () => { @@ -184,7 +184,7 @@ describe('CleanupCommand', () => { options: {} }) - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #0...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #0...') }) it('should parse numeric string to integer correctly', async () => { @@ -194,7 +194,7 @@ describe('CleanupCommand', () => { }) // Should parse as integer 7, not string "007" - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #7...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #7...') }) }) @@ -223,7 +223,7 @@ describe('CleanupCommand', () => { }) // Should use explicit issue flag (99), not auto-detected (42) - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #99...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #99...') }) }) @@ -420,7 +420,7 @@ describe('CleanupCommand', () => { }) // Should parse to integer 7 - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #7...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #7...') }) @@ -1029,7 +1029,7 @@ describe('CleanupCommand', () => { options: { issue: 25 } }) - expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to GitHub issue/PR #25...') + expect(logger.info).toHaveBeenCalledWith('Finding worktrees related to issue/PR #25...') expect(logger.info).toHaveBeenCalledWith('Found 2 worktree(s) related to issue/PR #25:') }) @@ -1057,7 +1057,7 @@ describe('CleanupCommand', () => { options: { issue: 25 } }) - expect(logger.warn).toHaveBeenCalledWith('No worktrees found for GitHub issue/PR #25') + expect(logger.warn).toHaveBeenCalledWith('No worktrees found for issue/PR #25') }) it('should handle no matching worktrees found', async () => { @@ -1068,7 +1068,7 @@ describe('CleanupCommand', () => { options: { issue: 99999 } }) - expect(logger.warn).toHaveBeenCalledWith('No worktrees found for GitHub issue/PR #99999') + expect(logger.warn).toHaveBeenCalledWith('No worktrees found for issue/PR #99999') expect(logger.info).toHaveBeenCalledWith('Searched for worktree paths containing: 99999, _pr_99999, issue-99999, etc.') }) diff --git a/src/commands/cleanup.ts b/src/commands/cleanup.ts index e176144b..58097a95 100644 --- a/src/commands/cleanup.ts +++ b/src/commands/cleanup.ts @@ -6,6 +6,7 @@ import { DatabaseManager } from '../lib/DatabaseManager.js' import { EnvironmentManager } from '../lib/EnvironmentManager.js' import { CLIIsolationManager } from '../lib/CLIIsolationManager.js' import { SettingsManager } from '../lib/SettingsManager.js' +import { IssueTrackerFactory } from '../lib/IssueTrackerFactory.js' import { promptConfirmation } from '../utils/prompt.js' import { IdentifierParser } from '../utils/IdentifierParser.js' import { loadEnvIntoProcess } from '../utils/env.js' @@ -47,6 +48,7 @@ export class CleanupCommand { private readonly gitWorktreeManager: GitWorktreeManager private resourceCleanup?: ResourceCleanup private loomManager?: import('../lib/LoomManager.js').LoomManager + private settings?: import('../lib/SettingsManager.js').IloomSettings private readonly identifierParser: IdentifierParser constructor( @@ -84,6 +86,7 @@ export class CleanupCommand { const settingsManager = new SettingsManager() const settings = await settingsManager.loadSettings() + this.settings = settings const databaseUrlEnvVarName = settings.capabilities?.database?.databaseUrlEnvVarName ?? 'DATABASE_URL' const environmentManager = new EnvironmentManager() @@ -462,7 +465,12 @@ export class CleanupCommand { const { force, dryRun } = parsed.options - getLogger().info(`Finding worktrees related to GitHub issue/PR #${issueNumber}...`) + // Use settings from ensureResourceCleanup (called via checkForChildLooms in execute()) + // to get issue tracker provider type for formatting + const providerType = this.settings ? IssueTrackerFactory.getProviderName(this.settings) : 'github' + const formattedId = IssueTrackerFactory.formatIssueId(providerType, issueNumber) + + getLogger().info(`Finding worktrees related to issue/PR ${formattedId}...`) // Step 1: Get all worktrees and filter by path pattern const worktrees = await this.gitWorktreeManager.listWorktrees() @@ -479,7 +487,7 @@ export class CleanupCommand { }) if (matchingWorktrees.length === 0) { - getLogger().warn(`No worktrees found for GitHub issue/PR #${issueNumber}`) + getLogger().warn(`No worktrees found for issue/PR ${formattedId}`) getLogger().info(`Searched for worktree paths containing: ${issueNumber}, _pr_${issueNumber}, issue-${issueNumber}, etc.`) return { identifier: String(issueNumber), @@ -500,7 +508,7 @@ export class CleanupCommand { })) // Step 3: Display preview - getLogger().info(`Found ${targets.length} worktree(s) related to issue/PR #${issueNumber}:`) + getLogger().info(`Found ${targets.length} worktree(s) related to issue/PR ${formattedId}:`) for (const target of targets) { getLogger().info(` Branch: ${target.branchName} (${target.worktreePath})`) } @@ -592,7 +600,7 @@ export class CleanupCommand { } // Step 7: Report statistics - getLogger().success(`Completed cleanup for issue/PR #${issueNumber}:`) + getLogger().success(`Completed cleanup for issue/PR ${formattedId}:`) getLogger().info(` Worktrees removed: ${worktreesRemoved}`) getLogger().info(` Branches deleted: ${branchesDeleted}`) if (databaseBranchesDeletedList.length > 0) { From 7ac7d00f3b78078935d6d545c845fb19f44733a5 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Sat, 14 Feb 2026 18:43:38 -0500 Subject: [PATCH 3/5] fixes #601 --- src/commands/ignite.ts | 2 ++ src/lib/ClaudeService.test.ts | 5 ++++ src/lib/ClaudeService.ts | 3 +++ src/lib/IssueTrackerFactory.ts | 19 +++++++++++++++ src/lib/PromptTemplateManager.ts | 1 + .../agents/iloom-issue-analyze-and-plan.md | 4 ++-- templates/agents/iloom-issue-analyzer.md | 4 ++-- .../iloom-issue-complexity-evaluator.md | 4 ++-- templates/agents/iloom-issue-enhancer.md | 4 ++-- templates/agents/iloom-issue-implementer.md | 4 ++-- templates/agents/iloom-issue-planner.md | 4 ++-- templates/prompts/issue-prompt.txt | 24 +++++++++---------- templates/prompts/session-summary-prompt.txt | 2 +- 13 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/commands/ignite.ts b/src/commands/ignite.ts index f1deff7f..e53385d0 100644 --- a/src/commands/ignite.ts +++ b/src/commands/ignite.ts @@ -476,8 +476,10 @@ export class IgniteCommand { draftPrNumber?: number, draftPrUrl?: string ): TemplateVariables { + const providerType = this.settings ? IssueTrackerFactory.getProviderName(this.settings) : 'github' const variables: TemplateVariables = { WORKSPACE_PATH: context.workspacePath, + ISSUE_PREFIX: IssueTrackerFactory.getIssuePrefix(providerType), } if (context.issueNumber !== undefined) { diff --git a/src/lib/ClaudeService.test.ts b/src/lib/ClaudeService.test.ts index 0cf92328..0f10e947 100644 --- a/src/lib/ClaudeService.test.ts +++ b/src/lib/ClaudeService.test.ts @@ -86,6 +86,7 @@ describe('ClaudeService', () => { expect(mockTemplateManager.getPrompt).toHaveBeenCalledWith('issue', { ISSUE_NUMBER: 123, + ISSUE_PREFIX: '#', ISSUE_TITLE: 'Add authentication', WORKSPACE_PATH: '/workspace/issue-123', PORT: 3123, @@ -117,6 +118,7 @@ describe('ClaudeService', () => { expect(mockTemplateManager.getPrompt).toHaveBeenCalledWith('issue', { ISSUE_NUMBER: 123, + ISSUE_PREFIX: '#', WORKSPACE_PATH: '/workspace', PORT: 3123, }) @@ -137,6 +139,7 @@ describe('ClaudeService', () => { expect(mockTemplateManager.getPrompt).toHaveBeenCalledWith('issue', { ISSUE_NUMBER: 123, + ISSUE_PREFIX: '#', WORKSPACE_PATH: '/workspace', }) }) @@ -160,6 +163,7 @@ describe('ClaudeService', () => { await service.launchForWorkflow(options) expect(mockTemplateManager.getPrompt).toHaveBeenCalledWith('pr', { + ISSUE_PREFIX: '#', PR_NUMBER: 456, PR_TITLE: 'Fix bug', WORKSPACE_PATH: '/workspace/pr-456', @@ -193,6 +197,7 @@ describe('ClaudeService', () => { await service.launchForWorkflow(options) expect(mockTemplateManager.getPrompt).toHaveBeenCalledWith('regular', { + ISSUE_PREFIX: '#', WORKSPACE_PATH: '/workspace/feature', }) diff --git a/src/lib/ClaudeService.ts b/src/lib/ClaudeService.ts index f635e236..d0048757 100644 --- a/src/lib/ClaudeService.ts +++ b/src/lib/ClaudeService.ts @@ -1,6 +1,7 @@ import { detectClaudeCli, launchClaude, launchClaudeInNewTerminalWindow, ClaudeCliOptions } from '../utils/claude.js' import { PromptTemplateManager, TemplateVariables } from './PromptTemplateManager.js' import { SettingsManager, IloomSettings } from './SettingsManager.js' +import { IssueTrackerFactory } from './IssueTrackerFactory.js' import { logger } from '../utils/logger.js' export interface ClaudeWorkflowOptions { @@ -70,8 +71,10 @@ export class ClaudeService { this.settings ??= await this.settingsManager.loadSettings() // Build template variables + const providerType = this.settings ? IssueTrackerFactory.getProviderName(this.settings) : 'github' const variables: TemplateVariables = { WORKSPACE_PATH: workspacePath, + ISSUE_PREFIX: IssueTrackerFactory.getIssuePrefix(providerType), } if (issueNumber !== undefined) { diff --git a/src/lib/IssueTrackerFactory.ts b/src/lib/IssueTrackerFactory.ts index 416ae148..96019bd3 100644 --- a/src/lib/IssueTrackerFactory.ts +++ b/src/lib/IssueTrackerFactory.ts @@ -88,4 +88,23 @@ export class IssueTrackerFactory { static getProviderName(settings: IloomSettings): IssueTrackerProviderType { return (settings.issueManagement?.provider ?? 'github') as IssueTrackerProviderType } + + /** + * Get the issue ID prefix for a given provider type. + * GitHub uses '#' (e.g., #123), Linear and Jira use '' (identifiers are self-contained). + * + * @param providerType - The issue tracker provider type + * @returns Prefix string to prepend before issue identifiers in display text + */ + static getIssuePrefix(providerType: IssueTrackerProviderType): string { + switch (providerType) { + case 'github': + return '#' + case 'linear': + case 'jira': + return '' + default: + return '#' + } + } } diff --git a/src/lib/PromptTemplateManager.ts b/src/lib/PromptTemplateManager.ts index bfc40fdf..5f64b2f1 100644 --- a/src/lib/PromptTemplateManager.ts +++ b/src/lib/PromptTemplateManager.ts @@ -15,6 +15,7 @@ Handlebars.registerHelper('raw', function (this: unknown, options: Handlebars.He export interface TemplateVariables { ISSUE_NUMBER?: string | number + ISSUE_PREFIX?: string PR_NUMBER?: number ISSUE_TITLE?: string PR_TITLE?: string diff --git a/templates/agents/iloom-issue-analyze-and-plan.md b/templates/agents/iloom-issue-analyze-and-plan.md index a722dd34..33a51654 100644 --- a/templates/agents/iloom-issue-analyze-and-plan.md +++ b/templates/agents/iloom-issue-analyze-and-plan.md @@ -11,14 +11,14 @@ model: opus **IMPORTANT: This loom is using draft PR mode.** -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}}{{#unless DRAFT_PR_NUMBER}}[PR NUMBER MISSING]{{/unless}} using `type: "pr"` Do NOT write comments to the issue - only to the draft PR. {{else}} ## Comment Routing: Standard Issue Mode -- **Read and write** to Issue #{{ISSUE_NUMBER}} using `type: "issue"` +- **Read and write** to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `type: "issue"` {{/if}} You are Claude, an AI assistant specialized in combined analysis and planning for simple issues. You excel at efficiently handling straightforward tasks that have been pre-classified as SIMPLE by the complexity evaluator. diff --git a/templates/agents/iloom-issue-analyzer.md b/templates/agents/iloom-issue-analyzer.md index 70bda089..04c88403 100644 --- a/templates/agents/iloom-issue-analyzer.md +++ b/templates/agents/iloom-issue-analyzer.md @@ -11,14 +11,14 @@ model: opus **IMPORTANT: This loom is using draft PR mode.** -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}}{{#unless DRAFT_PR_NUMBER}}[PR NUMBER MISSING]{{/unless}} using `type: "pr"` Do NOT write comments to the issue - only to the draft PR. {{else}} ## Comment Routing: Standard Issue Mode -- **Read and write** to Issue #{{ISSUE_NUMBER}} using `type: "issue"` +- **Read and write** to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `type: "issue"` {{/if}} You are Claude, an elite issue analyst specializing in deep technical investigation and root cause analysis. Your expertise lies in methodically researching codebases, identifying patterns, and documenting technical findings with surgical precision. diff --git a/templates/agents/iloom-issue-complexity-evaluator.md b/templates/agents/iloom-issue-complexity-evaluator.md index f3346782..fd13eee0 100644 --- a/templates/agents/iloom-issue-complexity-evaluator.md +++ b/templates/agents/iloom-issue-complexity-evaluator.md @@ -11,14 +11,14 @@ model: haiku **IMPORTANT: This loom is using draft PR mode.** -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}}{{#unless DRAFT_PR_NUMBER}}[PR NUMBER MISSING]{{/unless}} using `type: "pr"` Do NOT write comments to the issue - only to the draft PR. {{else}} ## Comment Routing: Standard Issue Mode -- **Read and write** to Issue #{{ISSUE_NUMBER}} using `type: "issue"` +- **Read and write** to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `type: "issue"` {{/if}} You are Claude, an AI assistant specialized in rapid complexity assessment for issues. Your role is to perform a quick evaluation to determine whether an issue should follow a TRIVIAL, SIMPLE, or COMPLEX workflow. diff --git a/templates/agents/iloom-issue-enhancer.md b/templates/agents/iloom-issue-enhancer.md index d50f12a8..a16cb030 100644 --- a/templates/agents/iloom-issue-enhancer.md +++ b/templates/agents/iloom-issue-enhancer.md @@ -11,14 +11,14 @@ model: opus **IMPORTANT: This loom is using draft PR mode.** -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}}{{#unless DRAFT_PR_NUMBER}}[PR NUMBER MISSING]{{/unless}} using `type: "pr"` Do NOT write comments to the issue - only to the draft PR. {{else}} ## Comment Routing: Standard Issue Mode -- **Read and write** to Issue #{{ISSUE_NUMBER}} using `type: "issue"` +- **Read and write** to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `type: "issue"` {{/if}} You are Claude, an elite Product Manager specializing in bug and enhancement report analysis. Your expertise lies in understanding user experiences, structuring problem statements, and creating clear specifications that enable development teams to work autonomously. diff --git a/templates/agents/iloom-issue-implementer.md b/templates/agents/iloom-issue-implementer.md index c045ed11..b9d41d26 100644 --- a/templates/agents/iloom-issue-implementer.md +++ b/templates/agents/iloom-issue-implementer.md @@ -11,14 +11,14 @@ color: green **IMPORTANT: This loom is using draft PR mode.** -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}}{{#unless DRAFT_PR_NUMBER}}[PR NUMBER MISSING]{{/unless}} using `type: "pr"` Do NOT write comments to the issue - only to the draft PR. {{else}} ## Comment Routing: Standard Issue Mode -- **Read and write** to Issue #{{ISSUE_NUMBER}} using `type: "issue"` +- **Read and write** to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `type: "issue"` {{/if}} You are Claude, an AI assistant specialized in implementing issues with absolute precision and adherence to specifications. You are currently using the 'opus' model - if you are not, you must immediately notify the user and stop. Ultrathink to perform as described below. diff --git a/templates/agents/iloom-issue-planner.md b/templates/agents/iloom-issue-planner.md index f847a085..cf9e5dd3 100644 --- a/templates/agents/iloom-issue-planner.md +++ b/templates/agents/iloom-issue-planner.md @@ -11,14 +11,14 @@ model: opus **IMPORTANT: This loom is using draft PR mode.** -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}}{{#unless DRAFT_PR_NUMBER}}[PR NUMBER MISSING]{{/unless}} using `type: "pr"` Do NOT write comments to the issue - only to the draft PR. {{else}} ## Comment Routing: Standard Issue Mode -- **Read and write** to Issue #{{ISSUE_NUMBER}} using `type: "issue"` +- **Read and write** to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `type: "issue"` {{/if}} You are Claude, an AI assistant designed to excel at analyzing issues and creating detailed implementation plans. Analyze the context and respond with precision and thoroughness. Think harder as you execute your tasks. diff --git a/templates/prompts/issue-prompt.txt b/templates/prompts/issue-prompt.txt index 0b2a642d..a3dca9fa 100644 --- a/templates/prompts/issue-prompt.txt +++ b/templates/prompts/issue-prompt.txt @@ -59,7 +59,7 @@ Use these Recap MCP tools: **IMPORTANT: This loom is using draft PR mode.** -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` - **Read draft PR details** from PR #{{DRAFT_PR_NUMBER}} using `mcp__issue_management__get_pr` - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}} using `type: "pr"` @@ -91,7 +91,7 @@ Call `mcp__recap__add_artifact` to register the draft PR: { type: "pr", primaryUrl: "{{DRAFT_PR_URL}}", - description: "Draft PR for issue #{{ISSUE_NUMBER}}" + description: "Draft PR for issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}}" } ``` @@ -100,8 +100,8 @@ This ensures the draft PR appears in the recap panel for easy access throughout {{#if STANDARD_ISSUE_MODE}} ## Comment Routing: Standard Issue Mode -- **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` -- **Write ALL workflow comments** to Issue #{{ISSUE_NUMBER}} using `type: "issue"` +- **Read issue details** from Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue` +- **Write ALL workflow comments** to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} using `type: "issue"` When calling `mcp__issue_management__create_comment`: ``` @@ -350,7 +350,7 @@ Only execute if workflow plan determined NEEDS_ENHANCEMENT: {{#if ARTIFACT_REVIEW_ENABLED}} 2.6. Artifact Review (Enhancement): - The enhancer output should be reviewed before posting - - Invoke: @agent-iloom-artifact-reviewer with context: "Review this ENHANCEMENT artifact for issue #{{ISSUE_NUMBER}}: [ENHANCER_OUTPUT]" + - Invoke: @agent-iloom-artifact-reviewer with context: "Review this ENHANCEMENT artifact for issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}}: [ENHANCER_OUTPUT]" - Wait for review results - If review suggests improvements: {{#if ONE_SHOT_MODE}} @@ -426,7 +426,7 @@ Only execute if workflow plan determined NEEDS_COMPLEXITY_EVAL: {{#if ARTIFACT_REVIEW_ENABLED}} 2.6. Artifact Review (Complexity Evaluation): - The complexity evaluator output should be reviewed before posting - - Invoke: @agent-iloom-artifact-reviewer with context: "Review this COMPLEXITY EVALUATION artifact for issue #{{ISSUE_NUMBER}}: [COMPLEXITY_EVALUATOR_OUTPUT]" + - Invoke: @agent-iloom-artifact-reviewer with context: "Review this COMPLEXITY EVALUATION artifact for issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}}: [COMPLEXITY_EVALUATOR_OUTPUT]" - Wait for review results - If review suggests improvements: {{#if ONE_SHOT_MODE}} @@ -520,7 +520,7 @@ Only execute if workflow plan determined NEEDS_ANALYSIS AND complexity is COMPLE {{#if ARTIFACT_REVIEW_ENABLED}} 2.6. Artifact Review (Analysis): - The analyzer output should be reviewed before posting - - Invoke: @agent-iloom-artifact-reviewer with context: "Review this ANALYSIS artifact for issue #{{ISSUE_NUMBER}}: [ANALYZER_OUTPUT]" + - Invoke: @agent-iloom-artifact-reviewer with context: "Review this ANALYSIS artifact for issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}}: [ANALYZER_OUTPUT]" - Wait for review results - If review suggests improvements: {{#if ONE_SHOT_MODE}} @@ -625,7 +625,7 @@ Execute combined analyze-and-plan agent: {{#if ARTIFACT_REVIEW_ENABLED}} 3.6. Artifact Review (Analysis and Plan): - The analyze-and-plan output should be reviewed before posting - - Invoke: @agent-iloom-artifact-reviewer with context: "Review this ANALYSIS AND PLAN artifact for issue #{{ISSUE_NUMBER}}: [ANALYZE_AND_PLAN_OUTPUT]" + - Invoke: @agent-iloom-artifact-reviewer with context: "Review this ANALYSIS AND PLAN artifact for issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}}: [ANALYZE_AND_PLAN_OUTPUT]" - Wait for review results - If review suggests improvements: {{#if ONE_SHOT_MODE}} @@ -692,7 +692,7 @@ Only execute if workflow plan determined NEEDS_PLANNING AND complexity is COMPLE {{#if ARTIFACT_REVIEW_ENABLED}} 2.6. Artifact Review (Plan): - The planner output should be reviewed before posting - - Invoke: @agent-iloom-artifact-reviewer with context: "Review this PLAN artifact for issue #{{ISSUE_NUMBER}}: [PLANNER_OUTPUT]" + - Invoke: @agent-iloom-artifact-reviewer with context: "Review this PLAN artifact for issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}}: [PLANNER_OUTPUT]" - Wait for review results - If review suggests improvements: {{#if ONE_SHOT_MODE}} @@ -817,7 +817,7 @@ Only execute if workflow plan determined NEEDS_IMPLEMENTATION: {{#if ARTIFACT_REVIEW_ENABLED}} 5.5. Artifact Review (Implementation): - The implementer output should be reviewed for plan alignment before code review - - Invoke: @agent-iloom-artifact-reviewer with context: "Review this IMPLEMENTATION artifact for issue #{{ISSUE_NUMBER}}: [IMPLEMENTER_OUTPUT]. The plan it was executing: [PLAN_CONTENT]" + - Invoke: @agent-iloom-artifact-reviewer with context: "Review this IMPLEMENTATION artifact for issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}}: [IMPLEMENTER_OUTPUT]. The plan it was executing: [PLAN_CONTENT]" - Wait for review results - If review suggests improvements: {{#if ONE_SHOT_MODE}} @@ -926,7 +926,7 @@ This is NOT optional - if the reviewer requests Claude Local Review, it must be c. **Create the real implementation commit:** - Generate a commit message summarizing the implementation work - - Include reference to Issue #{{ISSUE_NUMBER}} + - Include reference to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} - Format: `feat(issue-{{ISSUE_NUMBER}}): [summary of changes]` ```bash git commit -m "feat(issue-{{ISSUE_NUMBER}}): [generated summary]" @@ -947,7 +947,7 @@ This is NOT optional - if the reviewer requests Claude Local Review, it must be b. **Create commit with descriptive message:** - Generate a commit message summarizing the implementation work - - Include reference to Issue #{{ISSUE_NUMBER}} + - Include reference to Issue {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} - Format: `feat(issue-{{ISSUE_NUMBER}}): [summary of changes]` ```bash git commit -m "feat(issue-{{ISSUE_NUMBER}}): [generated summary]" diff --git a/templates/prompts/session-summary-prompt.txt b/templates/prompts/session-summary-prompt.txt index 61a5fbd5..6b15177a 100644 --- a/templates/prompts/session-summary-prompt.txt +++ b/templates/prompts/session-summary-prompt.txt @@ -1,7 +1,7 @@ You are generating a summary of THIS conversation - the development session you just completed. ## Context -- Issue/PR: #{{ISSUE_NUMBER}} +- Issue/PR: {{ISSUE_PREFIX}}{{ISSUE_NUMBER}} - Branch: {{BRANCH_NAME}} - Loom Type: {{LOOM_TYPE}} From f01dbde853f0ae74a1813a5d75ecbc89ccf2c509 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Sat, 14 Feb 2026 18:46:03 -0500 Subject: [PATCH 4/5] fixes #598 --- src/commands/enhance.test.ts | 1 + src/commands/enhance.ts | 4 +- src/commands/finish.pr-workflow.test.ts | 7 +++ src/commands/finish.test.ts | 2 + src/commands/finish.ts | 4 +- src/commands/start.test.ts | 4 ++ src/commands/start.ts | 8 ++-- src/lib/IssueEnhancementService.test.ts | 3 ++ src/lib/IssueEnhancementService.ts | 4 +- src/lib/LoomLauncher.test.ts | 60 +++++++++++++++++++++++++ src/lib/LoomLauncher.ts | 13 +++--- src/lib/LoomManager.ts | 2 + 12 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/commands/enhance.test.ts b/src/commands/enhance.test.ts index 724e0fa6..adcf2a0e 100644 --- a/src/commands/enhance.test.ts +++ b/src/commands/enhance.test.ts @@ -50,6 +50,7 @@ describe('EnhanceCommand', () => { fetchIssue: vi.fn(), getIssueUrl: vi.fn(), providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService // Create mock IssueEnhancementService diff --git a/src/commands/enhance.ts b/src/commands/enhance.ts index 69d21a3a..f5565baf 100644 --- a/src/commands/enhance.ts +++ b/src/commands/enhance.ts @@ -75,7 +75,7 @@ export class EnhanceCommand { // Step 2: Fetch issue to verify it exists if (!isJsonMode) { - getLogger().info(`Fetching issue #${issueNumber}...`) + getLogger().info(`Fetching issue ${this.issueTracker.formatIssueId(issueNumber)}...`) } const issue = await this.issueTracker.fetchIssue(issueNumber, repo) getLogger().debug('Issue fetched successfully', { number: issue.number, title: issue.title }) @@ -109,7 +109,7 @@ export class EnhanceCommand { return } - getLogger().success(`Issue #${issueNumber} enhanced successfully!`) + getLogger().success(`Issue ${this.issueTracker.formatIssueId(issueNumber)} enhanced successfully!`) getLogger().info(`Enhanced specification available at: ${result.url}`) // Prompt to open browser (unless --no-browser flag is set) diff --git a/src/commands/finish.pr-workflow.test.ts b/src/commands/finish.pr-workflow.test.ts index 3cbd9bc8..234dfa05 100644 --- a/src/commands/finish.pr-workflow.test.ts +++ b/src/commands/finish.pr-workflow.test.ts @@ -68,6 +68,7 @@ describe('FinishCommand - PR State Detection', () => { fetchIssue: vi.fn(), supportsPullRequests: true, providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService mockGitWorktreeManager = { @@ -247,6 +248,7 @@ describe('FinishCommand - Open PR Workflow', () => { fetchIssue: vi.fn(), supportsPullRequests: true, providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService mockGitWorktreeManager = { @@ -455,6 +457,7 @@ describe('FinishCommand - Child Loom GitHub PR Workflow', () => { fetchIssue: vi.fn().mockResolvedValue(mockIssue), supportsPullRequests: true, providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService mockGitWorktreeManager = { @@ -586,6 +589,7 @@ describe('FinishCommand - Closed PR Workflow', () => { fetchIssue: vi.fn(), supportsPullRequests: true, providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService mockGitWorktreeManager = { @@ -789,6 +793,7 @@ describe('FinishCommand - Merged PR Workflow', () => { fetchIssue: vi.fn(), supportsPullRequests: true, providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService mockGitWorktreeManager = { @@ -883,6 +888,7 @@ describe('FinishCommand - Dry-Run Mode for PRs', () => { fetchIssue: vi.fn(), supportsPullRequests: true, providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService mockGitWorktreeManager = { @@ -1037,6 +1043,7 @@ describe('FinishCommand - Issue Workflow (Regression Tests)', () => { fetchIssue: vi.fn().mockResolvedValue(mockIssue), supportsPullRequests: true, providerName: 'github', + formatIssueId: vi.fn((id: string | number) => `#${id}`), } as unknown as GitHubService mockGitWorktreeManager = { diff --git a/src/commands/finish.test.ts b/src/commands/finish.test.ts index 0a3d0c4a..4bbbc9fc 100644 --- a/src/commands/finish.test.ts +++ b/src/commands/finish.test.ts @@ -112,6 +112,8 @@ describe('FinishCommand', () => { // Set IssueTracker interface properties mockGitHubService.supportsPullRequests = true mockGitHubService.providerName = 'github' + // Mock formatIssueId to return GitHub-style formatting + vi.mocked(mockGitHubService.formatIssueId).mockImplementation((id) => `#${id}`) mockGitWorktreeManager = new GitWorktreeManager() mockValidationRunner = new ValidationRunner() mockCommitManager = new CommitManager() diff --git a/src/commands/finish.ts b/src/commands/finish.ts index ba3c9f9f..7c89c9cc 100644 --- a/src/commands/finish.ts +++ b/src/commands/finish.ts @@ -461,7 +461,7 @@ export class FinishCommand { // Validate issue state (warn if closed unless --force) if (issue.state === 'closed' && !options.force) { throw new Error( - `Issue #${parsed.number} is closed. Use --force to finish anyway.` + `Issue ${this.issueTracker.formatIssueId(parsed.number ?? 0)} is closed. Use --force to finish anyway.` ) } @@ -581,7 +581,7 @@ export class FinishCommand { case 'pr': return `PR #${parsed.number}${autoLabel}` case 'issue': - return `Issue #${parsed.number}${autoLabel}` + return `Issue ${this.issueTracker.formatIssueId(parsed.number ?? 0)}${autoLabel}` case 'branch': return `Branch '${parsed.branchName}'${autoLabel}` default: diff --git a/src/commands/start.test.ts b/src/commands/start.test.ts index 6738623e..8316aa36 100644 --- a/src/commands/start.test.ts +++ b/src/commands/start.test.ts @@ -105,6 +105,8 @@ describe('StartCommand', () => { // Set IssueTracker interface properties mockGitHubService.supportsPullRequests = true mockGitHubService.providerName = 'github' + // Mock formatIssueId to return GitHub-style formatting + vi.mocked(mockGitHubService.formatIssueId).mockImplementation((id) => `#${id}`) command = new StartCommand(mockGitHubService) }) @@ -1563,6 +1565,7 @@ describe('StartCommand', () => { detectInputType: ReturnType fetchIssue: ReturnType validateIssueState: ReturnType + formatIssueId: ReturnType fetchPR?: ReturnType validatePRState?: ReturnType } @@ -1576,6 +1579,7 @@ describe('StartCommand', () => { detectInputType: vi.fn(), fetchIssue: vi.fn(), validateIssueState: vi.fn(), + formatIssueId: vi.fn((id: string | number) => String(id).toUpperCase()), // Linear does NOT have fetchPR or validatePRState } linearCommand = new StartCommand(mockLinearService as unknown as GitHubService) diff --git a/src/commands/start.ts b/src/commands/start.ts index 8763257e..6671d916 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -166,7 +166,7 @@ export class StartCommand { // Format display message based on parent type const parentDisplay = parentLoom.type === 'issue' - ? `issue #${parentLoom.identifier}` + ? `issue ${this.issueTracker.formatIssueId(parentLoom.identifier)}` : parentLoom.type === 'pr' ? `PR #${parentLoom.identifier}` : `branch ${parentLoom.identifier}` @@ -217,7 +217,7 @@ export class StartCommand { title, // Use capitalized description as title body // Use capitalized body or empty ) - getLogger().success(`Created issue #${result.number}: ${result.url}`) + getLogger().success(`Created issue ${this.issueTracker.formatIssueId(result.number)}: ${result.url}`) // Update parsed to be an issue type with the new number parsed.type = 'issue' parsed.number = result.number @@ -408,7 +408,7 @@ export class StartCommand { originalInput: trimmedIdentifier, } } else { - throw new Error(`Could not find issue or PR #${number}`) + throw new Error(`Could not find issue or PR ${this.issueTracker.formatIssueId(number)}`) } } else { // Issue tracker doesn't support PRs (e.g., Linear) @@ -523,7 +523,7 @@ export class StartCommand { case 'pr': return `PR #${parsed.number}` case 'issue': - return `Issue #${parsed.number}` + return `Issue ${this.issueTracker.formatIssueId(parsed.number ?? 0)}` case 'branch': return `Branch '${parsed.branchName}'` case 'description': diff --git a/src/lib/IssueEnhancementService.test.ts b/src/lib/IssueEnhancementService.test.ts index 7541a1f2..0c339f37 100644 --- a/src/lib/IssueEnhancementService.test.ts +++ b/src/lib/IssueEnhancementService.test.ts @@ -51,6 +51,9 @@ describe('IssueEnhancementService', () => { mockAgentManager = new AgentManager() mockSettingsManager = new SettingsManager() + // Mock formatIssueId to return GitHub-style formatting + vi.mocked(mockGitHubService.formatIssueId).mockImplementation((id) => `#${id}`) + service = new IssueEnhancementService( mockGitHubService, mockAgentManager, diff --git a/src/lib/IssueEnhancementService.ts b/src/lib/IssueEnhancementService.ts index 76a8648a..2eb8d911 100644 --- a/src/lib/IssueEnhancementService.ts +++ b/src/lib/IssueEnhancementService.ts @@ -160,7 +160,7 @@ Your response should be the raw markdown that will become the GitHub issue body. if (isNonInteractive) { // In non-interactive environment: Skip all interactive operations - getLogger().info(`Running in non-interactive environment - skipping interactive prompts for issue #${issueNumber}`) + getLogger().info(`Running in non-interactive environment - skipping interactive prompts for issue ${this.issueTrackerService.formatIssueId(issueNumber)}`) return } @@ -168,7 +168,7 @@ Your response should be the raw markdown that will become the GitHub issue body. const issueUrl = await this.issueTrackerService.getIssueUrl(issueNumber, repository) // Display message and wait for first keypress - const message = `Created issue #${issueNumber}. + const message = `Created issue ${this.issueTrackerService.formatIssueId(issueNumber)}. Review and edit the issue in your browser if needed. Press any key to open issue for editing...` await waitForKeypress(message) diff --git a/src/lib/LoomLauncher.test.ts b/src/lib/LoomLauncher.test.ts index 3fc7c26e..a05a7910 100644 --- a/src/lib/LoomLauncher.test.ts +++ b/src/lib/LoomLauncher.test.ts @@ -612,4 +612,64 @@ describe('LoomLauncher', () => { }) }) }) + + describe('provider-aware issue formatting', () => { + it('should format Linear issue identifiers without # prefix in terminal titles', async () => { + await launcher.launchLoom({ + ...baseOptions, + enableClaude: true, + enableCode: false, + enableDevServer: false, + enableTerminal: false, + identifier: 'ENG-123', + issueTrackerProvider: 'linear', + }) + + // Claude terminal should use Linear formatting (no # prefix) + expect(mockClaudeContext.launchWithContext).toHaveBeenCalledWith( + expect.objectContaining({ + identifier: 'ENG-123', + }) + ) + }) + + it('should format GitHub issue identifiers with # prefix in multi-terminal titles', async () => { + await launcher.launchLoom({ + ...baseOptions, + enableClaude: false, + enableCode: false, + enableDevServer: true, + enableTerminal: true, + issueTrackerProvider: 'github', + }) + + // Multi-terminal mode should use tab titles with GitHub formatting + expect(terminal.openMultipleTerminalWindows).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ title: 'Dev Server - Issue #42' }), + expect.objectContaining({ title: 'Terminal - Issue #42' }), + ]) + ) + }) + + it('should format Linear identifiers in multi-terminal titles', async () => { + await launcher.launchLoom({ + ...baseOptions, + enableClaude: false, + enableCode: false, + enableDevServer: true, + enableTerminal: true, + identifier: 'ENG-123', + issueTrackerProvider: 'linear', + }) + + // Multi-terminal mode should use Linear formatting (no # prefix) + expect(terminal.openMultipleTerminalWindows).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ title: 'Dev Server - Issue ENG-123' }), + expect.objectContaining({ title: 'Terminal - Issue ENG-123' }), + ]) + ) + }) + }) }) diff --git a/src/lib/LoomLauncher.ts b/src/lib/LoomLauncher.ts index a9e823c1..65ba2d28 100644 --- a/src/lib/LoomLauncher.ts +++ b/src/lib/LoomLauncher.ts @@ -6,6 +6,7 @@ import { openIdeWindow } from '../utils/ide.js' import { generateColorFromBranchName, hexToRgb } from '../utils/color.js' import { getLogger } from '../utils/logger-context.js' import { ClaudeContextManager } from './ClaudeContextManager.js' +import { IssueTrackerFactory, type IssueTrackerProviderType } from './IssueTrackerFactory.js' import type { SettingsManager } from './SettingsManager.js' import type { Capability } from '../types/loom.js' import { getDotenvFlowFiles } from '../utils/env.js' @@ -29,6 +30,7 @@ export interface LaunchLoomOptions { sourceEnvOnStart?: boolean // defaults to false if undefined colorTerminal?: boolean // defaults to true if undefined colorHex?: string // Pre-calculated hex color from metadata, avoids recalculation + issueTrackerProvider?: IssueTrackerProviderType // Provider type for formatting issue IDs } /** @@ -208,7 +210,7 @@ export class LoomLauncher { options: LaunchLoomOptions ): Promise { const hasEnvFile = this.hasAnyEnvFiles(options.worktreePath) - const claudeTitle = `Claude - ${this.formatIdentifier(options.workflowType, options.identifier)}` + const claudeTitle = `Claude - ${this.formatIdentifier(options.workflowType, options.identifier, options.issueTrackerProvider)}` const executable = options.executablePath ?? 'iloom' let claudeCommand = `${executable} spin` @@ -250,7 +252,7 @@ export class LoomLauncher { const devServerIdentifier = String(options.identifier) const devServerCommand = `${executable} dev-server ${devServerIdentifier}` - const devServerTitle = `Dev Server - ${this.formatIdentifier(options.workflowType, options.identifier)}` + const devServerTitle = `Dev Server - ${this.formatIdentifier(options.workflowType, options.identifier, options.issueTrackerProvider)}` // Only generate color if terminal coloring is enabled (default: true) const backgroundColor = (options.colorTerminal ?? true) @@ -278,7 +280,7 @@ export class LoomLauncher { private buildStandaloneTerminalOptions( options: LaunchLoomOptions ): TerminalWindowOptions { - const terminalTitle = `Terminal - ${this.formatIdentifier(options.workflowType, options.identifier)}` + const terminalTitle = `Terminal - ${this.formatIdentifier(options.workflowType, options.identifier, options.issueTrackerProvider)}` // Build shell command with identifier // Use the same executable path pattern as buildClaudeTerminalOptions @@ -332,9 +334,10 @@ export class LoomLauncher { /** * Format identifier for terminal tab titles */ - private formatIdentifier(workflowType: 'issue' | 'pr' | 'regular', identifier: string | number): string { + private formatIdentifier(workflowType: 'issue' | 'pr' | 'regular', identifier: string | number, issueTrackerProvider?: IssueTrackerProviderType): string { if (workflowType === 'issue') { - return `Issue #${identifier}` + const formattedId = IssueTrackerFactory.formatIssueId(issueTrackerProvider ?? 'github', identifier) + return `Issue ${formattedId}` } else if (workflowType === 'pr') { return `PR #${identifier}` } else { diff --git a/src/lib/LoomManager.ts b/src/lib/LoomManager.ts index ad6cbfac..94344e1a 100644 --- a/src/lib/LoomManager.ts +++ b/src/lib/LoomManager.ts @@ -381,6 +381,7 @@ export class LoomManager { sourceEnvOnStart: settingsData.sourceEnvOnStart ?? false, colorTerminal: input.options?.colorTerminal ?? settingsData.colors?.terminal ?? true, colorHex: colorData.hex, + issueTrackerProvider: this.issueTracker.providerName as import('./IssueTrackerFactory.js').IssueTrackerProviderType, }) } @@ -1320,6 +1321,7 @@ export class LoomManager { sourceEnvOnStart: settingsData.sourceEnvOnStart ?? false, colorTerminal: input.options?.colorTerminal ?? settingsData.colors?.terminal ?? true, colorHex, + issueTrackerProvider: this.issueTracker.providerName as import('./IssueTrackerFactory.js').IssueTrackerProviderType, }) } From d40cd1d19486e8dff7d9fd444d4b9131893584a2 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Sat, 14 Feb 2026 18:47:02 -0500 Subject: [PATCH 5/5] fixes #599 --- src/commands/commit.test.ts | 13 +++++++++---- src/commands/commit.ts | 12 +++++++----- src/commands/plan.test.ts | 4 ++++ src/commands/plan.ts | 17 ++++++++--------- src/lib/LoomManager.test.ts | 23 ++++++++++++++++++----- src/lib/LoomManager.ts | 3 ++- src/lib/PRManager.test.ts | 12 ++++++++++++ src/lib/PRManager.ts | 32 +++++++++++++++++++------------- 8 files changed, 79 insertions(+), 37 deletions(-) diff --git a/src/commands/commit.test.ts b/src/commands/commit.test.ts index 968e7183..edca6265 100644 --- a/src/commands/commit.test.ts +++ b/src/commands/commit.test.ts @@ -14,7 +14,14 @@ vi.mock('../lib/GitWorktreeManager.js') vi.mock('../lib/SettingsManager.js') vi.mock('../lib/MetadataManager.js') vi.mock('../lib/ValidationRunner.js') -vi.mock('../mcp/IssueManagementProviderFactory.js') +vi.mock('../lib/IssueTrackerFactory.js', () => ({ + IssueTrackerFactory: { + formatIssueId: vi.fn((provider: string, id: string | number) => { + if (provider === 'github') return `#${id}` + return String(id).toUpperCase() + }), + }, +})) vi.mock('../utils/git.js', () => ({ isValidGitRepo: vi.fn(), getWorktreeRoot: vi.fn(), @@ -23,7 +30,6 @@ vi.mock('../utils/git.js', () => ({ // Import mocked functions import { isValidGitRepo, getWorktreeRoot, extractIssueNumber } from '../utils/git.js' -import { IssueManagementProviderFactory } from '../mcp/IssueManagementProviderFactory.js' describe('CommitCommand', () => { let command: CommitCommand @@ -63,8 +69,7 @@ describe('CommitCommand', () => { originalIloomEnv = process.env.ILOOM delete process.env.ILOOM - // Mock IssueManagementProviderFactory - vi.mocked(IssueManagementProviderFactory.create).mockReturnValue({ issuePrefix: '#' } as ReturnType) + // IssueTrackerFactory.formatIssueId is already mocked via vi.mock above // Create mock CommitManager mockCommitManager = { diff --git a/src/commands/commit.ts b/src/commands/commit.ts index ced2db2b..b6eb23e8 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -4,7 +4,7 @@ import { CommitManager } from '../lib/CommitManager.js' import { SettingsManager } from '../lib/SettingsManager.js' import { MetadataManager } from '../lib/MetadataManager.js' import { ValidationRunner } from '../lib/ValidationRunner.js' -import { IssueManagementProviderFactory } from '../mcp/IssueManagementProviderFactory.js' +import { IssueTrackerFactory, type IssueTrackerProviderType } from '../lib/IssueTrackerFactory.js' import { getLogger } from '../utils/logger-context.js' import { extractIssueNumber, isValidGitRepo, getWorktreeRoot } from '../utils/git.js' import type { CommitOptions } from '../types/index.js' @@ -131,10 +131,9 @@ export class CommitCommand { validationPassed = true } - // Step 6: Load settings to get issue prefix + // Step 6: Load settings to get provider type for issue formatting const settings = await this.settingsManager.loadSettings(worktreePath) - const providerType = settings.issueManagement?.provider ?? 'github' - const issuePrefix = IssueManagementProviderFactory.create(providerType).issuePrefix + const providerType = (settings.issueManagement?.provider ?? 'github') as IssueTrackerProviderType // Determine whether to skip pre-commit hooks: // - With --wip-commit: always skip hooks (quick WIP commit) @@ -146,7 +145,7 @@ export class CommitCommand { let commitMessage: string | undefined = input.message if (input.wipCommit && !input.message) { if (detected.issueNumber !== undefined) { - commitMessage = `WIP commit for Issue ${issuePrefix}${detected.issueNumber}` + commitMessage = `WIP commit for Issue ${IssueTrackerFactory.formatIssueId(providerType, detected.issueNumber)}` } else { commitMessage = 'WIP commit' } @@ -154,6 +153,9 @@ export class CommitCommand { } // Step 8: Build commit options + // Derive issuePrefix for CommitManager's downstream contract + // formatIssueId with empty string yields just the prefix (e.g., '#' for GitHub, '' for Linear) + const issuePrefix = IssueTrackerFactory.formatIssueId(providerType, '') const commitOptions: CommitOptions = { issuePrefix, skipVerify: shouldSkipVerify, diff --git a/src/commands/plan.test.ts b/src/commands/plan.test.ts index a77d0d43..d1ff6294 100644 --- a/src/commands/plan.test.ts +++ b/src/commands/plan.test.ts @@ -20,6 +20,10 @@ vi.mock('../lib/SettingsManager.js', () => ({ vi.mock('../lib/IssueTrackerFactory.js', () => ({ IssueTrackerFactory: { getProviderName: vi.fn().mockReturnValue('github'), + formatIssueId: vi.fn((provider: string, id: string | number) => { + if (provider === 'github') return `#${id}` + return String(id).toUpperCase() + }), }, })) vi.mock('../utils/logger.js', () => ({ diff --git a/src/commands/plan.ts b/src/commands/plan.ts index 365964ec..25b15344 100644 --- a/src/commands/plan.ts +++ b/src/commands/plan.ts @@ -5,7 +5,7 @@ import { detectClaudeCli, launchClaude } from '../utils/claude.js' import { PromptTemplateManager, type TemplateVariables } from '../lib/PromptTemplateManager.js' import { generateIssueManagementMcpConfig } from '../utils/mcp.js' import { SettingsManager, PlanCommandSettingsSchema } from '../lib/SettingsManager.js' -import { IssueTrackerFactory } from '../lib/IssueTrackerFactory.js' +import { IssueTrackerFactory, type IssueTrackerProviderType } from '../lib/IssueTrackerFactory.js' import { matchIssueIdentifier } from '../utils/IdentifierParser.js' import { IssueManagementProviderFactory } from '../mcp/IssueManagementProviderFactory.js' import { needsFirstRunSetup, launchFirstRunSetup } from '../utils/first-run-setup.js' @@ -22,23 +22,23 @@ type ReviewerProvider = (typeof REVIEWER_PROVIDERS)[number] /** * Format child issues as a markdown list for inclusion in the prompt */ -function formatChildIssues(children: ChildIssueResult[], issuePrefix: string): string { +function formatChildIssues(children: ChildIssueResult[], providerType: IssueTrackerProviderType): string { if (children.length === 0) return 'None' return children - .map(child => `- ${issuePrefix}${child.id}: ${child.title} (${child.state})`) + .map(child => `- ${IssueTrackerFactory.formatIssueId(providerType, child.id)}: ${child.title} (${child.state})`) .join('\n') } /** * Format dependencies as a markdown list for inclusion in the prompt */ -function formatDependencies(dependencies: DependenciesResult, issuePrefix: string): string { +function formatDependencies(dependencies: DependenciesResult, providerType: IssueTrackerProviderType): string { const lines: string[] = [] if (dependencies.blockedBy.length > 0) { lines.push('**Blocked by:**') for (const dep of dependencies.blockedBy) { - lines.push(`- ${issuePrefix}${dep.id}: ${dep.title} (${dep.state})`) + lines.push(`- ${IssueTrackerFactory.formatIssueId(providerType, dep.id)}: ${dep.title} (${dep.state})`) } } @@ -46,7 +46,7 @@ function formatDependencies(dependencies: DependenciesResult, issuePrefix: strin if (lines.length > 0) lines.push('') lines.push('**Blocking:**') for (const dep of dependencies.blocking) { - lines.push(`- ${issuePrefix}${dep.id}: ${dep.title} (${dep.state})`) + lines.push(`- ${IssueTrackerFactory.formatIssueId(providerType, dep.id)}: ${dep.title} (${dep.state})`) } } @@ -187,7 +187,6 @@ export class PlanCommand { } | null = null const provider = settings ? IssueTrackerFactory.getProviderName(settings) : 'github' - const issuePrefix = provider === 'github' ? '#' : '' if (prompt && looksLikeIssueIdentifier) { // Validate and fetch issue using issueTracker.detectInputType() pattern from StartCommand @@ -380,10 +379,10 @@ export class PlanCommand { PARENT_ISSUE_TITLE: decompositionContext?.title, PARENT_ISSUE_BODY: decompositionContext?.body, PARENT_ISSUE_CHILDREN: decompositionContext?.children - ? formatChildIssues(decompositionContext.children, issuePrefix) + ? formatChildIssues(decompositionContext.children, provider) : undefined, PARENT_ISSUE_DEPENDENCIES: decompositionContext?.dependencies - ? formatDependencies(decompositionContext.dependencies, issuePrefix) + ? formatDependencies(decompositionContext.dependencies, provider) : undefined, PLANNER: effectivePlanner, REVIEWER: effectiveReviewer, diff --git a/src/lib/LoomManager.test.ts b/src/lib/LoomManager.test.ts index 698549bb..cbf363ae 100644 --- a/src/lib/LoomManager.test.ts +++ b/src/lib/LoomManager.test.ts @@ -24,6 +24,20 @@ vi.mock('./ProjectCapabilityDetector.js') vi.mock('./CLIIsolationManager.js') vi.mock('./SettingsManager.js') +// Mock IssueTrackerFactory for formatIssueId +vi.mock('./IssueTrackerFactory.js', () => ({ + IssueTrackerFactory: { + formatIssueId: vi.fn((provider: string, id: string | number) => { + if (provider === 'github') return `#${id}` + return String(id).toUpperCase() + }), + getProviderName: vi.fn((settings: Record) => { + const issueManagement = settings.issueManagement as Record | undefined + return (issueManagement?.provider as string) ?? 'github' + }), + }, +})) + // Mock fs-extra vi.mock('fs-extra', () => ({ default: { @@ -118,7 +132,6 @@ vi.mock('./LoomLauncher.js', () => ({ // Shared mock functions for verification in tests const mockCreateDraftPR = vi.fn() const mockCheckForExistingPR = vi.fn() -let mockIssuePrefix = '#' vi.mock('./PRManager.js', () => { // Use a class-like factory that creates fresh instances // This avoids issues with mockReset clearing the constructor implementation @@ -126,7 +139,6 @@ vi.mock('./PRManager.js', () => { PRManager: class MockPRManager { createDraftPR = mockCreateDraftPR checkForExistingPR = mockCheckForExistingPR - get issuePrefix() { return mockIssuePrefix } }, } }) @@ -148,7 +160,6 @@ describe('LoomManager', () => { let mockSettings: vi.Mocked beforeEach(() => { - mockIssuePrefix = '#' // Reset to GitHub default mockGitWorktree = new GitWorktreeManager() as vi.Mocked mockGitHub = new GitHubService() as vi.Mocked mockBranchNaming = new DefaultBranchNamingService() as vi.Mocked @@ -495,9 +506,8 @@ describe('LoomManager', () => { // Configure Linear provider (doesn't support PRs natively) mockGitHub.supportsPullRequests = false mockGitHub.providerName = 'linear' - mockIssuePrefix = '' // Linear issues use empty prefix (identifier already includes team key) - // Mock settings with github-draft-pr mode + // Mock settings with github-draft-pr mode and Linear provider // (Issue #464: Linear + github-draft-pr should work since PRs go through GitHub CLI) vi.mocked(mockSettings.loadSettings).mockResolvedValue({ mainBranch: 'main', @@ -505,6 +515,9 @@ describe('LoomManager', () => { mergeBehavior: { mode: 'github-draft-pr', }, + issueManagement: { + provider: 'linear', + }, }) // Mock issue fetch (Linear issues work like GitHub issues) diff --git a/src/lib/LoomManager.ts b/src/lib/LoomManager.ts index ad6cbfac..e57028fa 100644 --- a/src/lib/LoomManager.ts +++ b/src/lib/LoomManager.ts @@ -24,6 +24,7 @@ import type { GitWorktree } from '../types/worktree.js' import type { Issue, PullRequest } from '../types/index.js' import { getLogger } from '../utils/logger-context.js' import { PRManager } from './PRManager.js' +import { IssueTrackerFactory } from './IssueTrackerFactory.js' /** * LoomManager orchestrates the creation and management of looms (isolated workspaces) @@ -279,7 +280,7 @@ export class LoomManager { let prBody: string if (input.type === 'issue') { const issueBody = issueData?.body ? `\n\n## ${issueData.title}\n\n${issueData.body}` : '' - prBody = `Fixes ${prManager.issuePrefix}${input.identifier}${issueBody}\n\n---\n*This PR was created automatically by iloom.*` + prBody = `Fixes ${IssueTrackerFactory.formatIssueId(IssueTrackerFactory.getProviderName(settingsData), input.identifier)}${issueBody}\n\n---\n*This PR was created automatically by iloom.*` } else { prBody = `Branch: ${branchName}\n\n---\n*This PR was created automatically by iloom.*` } diff --git a/src/lib/PRManager.test.ts b/src/lib/PRManager.test.ts index 48eb90b0..b58f8ecf 100644 --- a/src/lib/PRManager.test.ts +++ b/src/lib/PRManager.test.ts @@ -10,6 +10,18 @@ vi.mock('../utils/github.js') vi.mock('../utils/claude.js') vi.mock('../utils/remote.js') vi.mock('../utils/browser.js') +vi.mock('./IssueTrackerFactory.js', () => ({ + IssueTrackerFactory: { + formatIssueId: vi.fn((provider: string, id: string | number) => { + if (provider === 'github') return `#${id}` + return String(id).toUpperCase() + }), + getProviderName: vi.fn((settings: Record) => { + const issueManagement = settings.issueManagement as Record | undefined + return (issueManagement?.provider as string) ?? 'github' + }), + }, +})) describe('PRManager', () => { let prManager: PRManager diff --git a/src/lib/PRManager.ts b/src/lib/PRManager.ts index 86102c57..ee651b55 100644 --- a/src/lib/PRManager.ts +++ b/src/lib/PRManager.ts @@ -4,7 +4,7 @@ import { getEffectivePRTargetRemote, getConfiguredRepoFromSettings, parseGitRemo import { openBrowser } from '../utils/browser.js' import { getLogger } from '../utils/logger-context.js' import type { IloomSettings } from './SettingsManager.js' -import { IssueManagementProviderFactory } from '../mcp/IssueManagementProviderFactory.js' +import { IssueTrackerFactory, type IssueTrackerProviderType } from './IssueTrackerFactory.js' interface ExistingPR { number: number @@ -23,12 +23,17 @@ export class PRManager { } /** - * Get the issue prefix from the configured provider + * Get the configured provider type */ - public get issuePrefix(): string { - const providerType = this.settings.issueManagement?.provider ?? 'github' - const provider = IssueManagementProviderFactory.create(providerType) - return provider.issuePrefix + private get providerType(): IssueTrackerProviderType { + return IssueTrackerFactory.getProviderName(this.settings) + } + + /** + * Format an issue identifier for display using IssueTrackerFactory + */ + private formatIssueId(identifier: string | number): string { + return IssueTrackerFactory.formatIssueId(this.providerType, identifier) } /** @@ -91,7 +96,7 @@ export class PRManager { let body = 'This PR contains changes from the iloom workflow.\n\n' if (issueNumber) { - body += `Fixes ${this.issuePrefix}${issueNumber}` + body += `Fixes ${this.formatIssueId(issueNumber)}` } return body @@ -102,28 +107,29 @@ export class PRManager { * Uses XML format for clear task definition and output expectations */ private buildPRBodyPrompt(issueNumber?: string | number): string { - const issueContext = issueNumber + const formattedIssueId = issueNumber !== undefined ? this.formatIssueId(issueNumber) : undefined + const issueContext = formattedIssueId ? `\n -This PR is associated with issue ${this.issuePrefix}${issueNumber}. -Include "Fixes ${this.issuePrefix}${issueNumber}" at the end of the body on its own line. +This PR is associated with issue ${formattedIssueId}. +Include "Fixes ${formattedIssueId}" at the end of the body on its own line. ` : '' - const examplePrefix = this.issuePrefix || '' // Use empty string for Linear examples + const exampleIssueId = this.formatIssueId(42) return ` You are a software engineer writing a pull request body for this repository. Examine the changes in the git repository and generate a concise, professional PR description. -Write 2-3 sentences summarizing what was changed and why.${issueNumber ? `\n\nEnd with "Fixes ${this.issuePrefix}${issueNumber}" on its own line.` : ''} +Write 2-3 sentences summarizing what was changed and why.${formattedIssueId ? `\n\nEnd with "Fixes ${formattedIssueId}" on its own line.` : ''} Professional and concise Summarize the changes and their purpose CRITICAL: Do NOT include ANY explanatory text, analysis, or meta-commentary. Output ONLY the raw PR body text. Good: "Add user authentication with JWT tokens to secure the API endpoints. This includes login and registration endpoints with proper password hashing. -Fixes ${examplePrefix}42" +Fixes ${exampleIssueId}" Good: "Fix navigation bug in sidebar menu that caused incorrect highlighting on nested routes." Bad: "Here's the PR body:\n\n---\n\nAdd user authentication..." Bad: "Based on the changes, I'll write: Fix navigation bug..."