From 73db206c9adc771c1e06c96b79b9e1c822382658 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:04:24 +0900 Subject: [PATCH] takt: fix-pr-issue-number --- src/__tests__/github-pr.test.ts | 32 +++++++++++++++++-- src/app/cli/routing.ts | 30 +++++++++++++---- src/features/pipeline/execute.ts | 2 +- .../tasks/execute/selectAndExecute.ts | 2 +- src/features/tasks/execute/types.ts | 3 ++ src/infra/github/pr.ts | 14 ++++---- 6 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/__tests__/github-pr.test.ts b/src/__tests__/github-pr.test.ts index 2a128cd..2e640d4 100644 --- a/src/__tests__/github-pr.test.ts +++ b/src/__tests__/github-pr.test.ts @@ -10,7 +10,7 @@ import { buildPrBody } from '../infra/github/pr.js'; import type { GitHubIssue } from '../infra/github/types.js'; describe('buildPrBody', () => { - it('should build body with issue and report', () => { + it('should build body with single issue and report', () => { const issue: GitHubIssue = { number: 99, title: 'Add login feature', @@ -19,7 +19,7 @@ describe('buildPrBody', () => { comments: [], }; - const result = buildPrBody(issue, 'Piece `default` completed.'); + const result = buildPrBody([issue], 'Piece `default` completed.'); expect(result).toContain('## Summary'); expect(result).toContain('Implement username/password authentication.'); @@ -37,7 +37,7 @@ describe('buildPrBody', () => { comments: [], }; - const result = buildPrBody(issue, 'Done.'); + const result = buildPrBody([issue], 'Done.'); expect(result).toContain('Fix bug'); expect(result).toContain('Closes #10'); @@ -51,4 +51,30 @@ describe('buildPrBody', () => { expect(result).toContain('Task completed.'); expect(result).not.toContain('Closes'); }); + + it('should support multiple issues', () => { + const issues: GitHubIssue[] = [ + { + number: 1, + title: 'First issue', + body: 'First issue body.', + labels: [], + comments: [], + }, + { + number: 2, + title: 'Second issue', + body: 'Second issue body.', + labels: [], + comments: [], + }, + ]; + + const result = buildPrBody(issues, 'Done.'); + + expect(result).toContain('## Summary'); + expect(result).toContain('First issue body.'); + expect(result).toContain('Closes #1'); + expect(result).toContain('Closes #2'); + }); }); diff --git a/src/app/cli/routing.ts b/src/app/cli/routing.ts index 4731de5..1a463ac 100644 --- a/src/app/cli/routing.ts +++ b/src/app/cli/routing.ts @@ -7,7 +7,7 @@ import { info, error } from '../../shared/ui/index.js'; import { getErrorMessage } from '../../shared/utils/index.js'; -import { resolveIssueTask } from '../../infra/github/index.js'; +import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers } from '../../infra/github/index.js'; import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js'; import { executePipeline } from '../../features/pipeline/index.js'; import { interactiveMode } from '../../features/interactive/index.js'; @@ -65,7 +65,13 @@ export async function executeDefaultAction(task?: string): Promise { const issueFromOption = opts.issue as number | undefined; if (issueFromOption) { try { - const resolvedTask = resolveIssueTask(`#${issueFromOption}`); + const ghStatus = checkGhCli(); + if (!ghStatus.available) { + throw new Error(ghStatus.error); + } + const issue = fetchIssue(issueFromOption); + const resolvedTask = formatIssueAsTask(issue); + selectOptions.issues = [issue]; await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); } catch (e) { error(getErrorMessage(e)); @@ -75,17 +81,27 @@ export async function executeDefaultAction(task?: string): Promise { } if (task && isDirectTask(task)) { - // isDirectTask() returns true only for issue references - let resolvedTask: string; + // isDirectTask() returns true only for issue references (e.g., "#6" or "#1 #2") try { info('Fetching GitHub Issue...'); - resolvedTask = resolveIssueTask(task); + const ghStatus = checkGhCli(); + if (!ghStatus.available) { + throw new Error(ghStatus.error); + } + // Parse all issue numbers from task (supports "#6" and "#1 #2") + const tokens = task.trim().split(/\s+/); + const issueNumbers = parseIssueNumbers(tokens); + if (issueNumbers.length === 0) { + throw new Error(`Invalid issue reference: ${task}`); + } + const issues = issueNumbers.map((n) => fetchIssue(n)); + const resolvedTask = issues.map(formatIssueAsTask).join('\n\n---\n\n'); + selectOptions.issues = issues; + await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); } catch (e) { error(getErrorMessage(e)); process.exit(1); } - - await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); return; } diff --git a/src/features/pipeline/execute.ts b/src/features/pipeline/execute.ts index e9262fe..b274a70 100644 --- a/src/features/pipeline/execute.ts +++ b/src/features/pipeline/execute.ts @@ -96,7 +96,7 @@ function buildPipelinePrBody( report, }); } - return buildPrBody(issue, report); + return buildPrBody(issue ? [issue] : undefined, report); } /** diff --git a/src/features/tasks/execute/selectAndExecute.ts b/src/features/tasks/execute/selectAndExecute.ts index d39923a..ca0d811 100644 --- a/src/features/tasks/execute/selectAndExecute.ts +++ b/src/features/tasks/execute/selectAndExecute.ts @@ -168,7 +168,7 @@ export async function selectAndExecuteTask( const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false); if (shouldCreatePr) { info('Creating pull request...'); - const prBody = buildPrBody(undefined, `Piece \`${pieceIdentifier}\` completed successfully.`); + const prBody = buildPrBody(options?.issues, `Piece \`${pieceIdentifier}\` completed successfully.`); const prResult = createPullRequest(execCwd, { branch, title: task.length > 100 ? `${task.slice(0, 97)}...` : task, diff --git a/src/features/tasks/execute/types.ts b/src/features/tasks/execute/types.ts index 63ab06b..206ca94 100644 --- a/src/features/tasks/execute/types.ts +++ b/src/features/tasks/execute/types.ts @@ -4,6 +4,7 @@ import type { Language } from '../../../core/models/index.js'; import type { ProviderType } from '../../../infra/providers/index.js'; +import type { GitHubIssue } from '../../../infra/github/index.js'; /** Result of piece execution */ export interface PieceExecutionResult { @@ -93,4 +94,6 @@ export interface SelectAndExecuteOptions { interactiveUserInput?: boolean; /** Interactive mode result metadata for NDJSON logging */ interactiveMetadata?: InteractiveMetadata; + /** GitHub Issues to associate with the PR (adds "Closes #N" for each issue) */ + issues?: GitHubIssue[]; } diff --git a/src/infra/github/pr.ts b/src/infra/github/pr.ts index d0233bc..3b366bd 100644 --- a/src/infra/github/pr.ts +++ b/src/infra/github/pr.ts @@ -70,15 +70,17 @@ export function createPullRequest(cwd: string, options: CreatePrOptions): Create } /** - * Build PR body from issue and execution report. + * Build PR body from issues and execution report. + * Supports multiple issues (adds "Closes #N" for each). */ -export function buildPrBody(issue: GitHubIssue | undefined, report: string): string { +export function buildPrBody(issues: GitHubIssue[] | undefined, report: string): string { const parts: string[] = []; parts.push('## Summary'); - if (issue) { + if (issues && issues.length > 0) { parts.push(''); - parts.push(issue.body || issue.title); + // Use the first issue's body/title for summary + parts.push(issues[0]!.body || issues[0]!.title); } parts.push(''); @@ -86,9 +88,9 @@ export function buildPrBody(issue: GitHubIssue | undefined, report: string): str parts.push(''); parts.push(report); - if (issue) { + if (issues && issues.length > 0) { parts.push(''); - parts.push(`Closes #${issue.number}`); + parts.push(issues.map((issue) => `Closes #${issue.number}`).join('\n')); } return parts.join('\n');