takt: github-issue-142-intarakuteibu (#147)
This commit is contained in:
parent
e48c267562
commit
f3b8c772cb
258
src/__tests__/cli-routing-issue-resolve.test.ts
Normal file
258
src/__tests__/cli-routing-issue-resolve.test.ts
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* Tests for issue resolution in routing module.
|
||||||
|
*
|
||||||
|
* Verifies that issue references (--issue N or #N positional arg)
|
||||||
|
* are resolved before interactive mode and passed to selectAndExecuteTask
|
||||||
|
* via selectOptions.issues.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
|
createLogger: () => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/github/issue.js', () => ({
|
||||||
|
parseIssueNumbers: vi.fn(() => []),
|
||||||
|
checkGhCli: vi.fn(),
|
||||||
|
fetchIssue: vi.fn(),
|
||||||
|
formatIssueAsTask: vi.fn(),
|
||||||
|
isIssueReference: vi.fn(),
|
||||||
|
resolveIssueTask: vi.fn(),
|
||||||
|
createIssue: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/tasks/index.js', () => ({
|
||||||
|
selectAndExecuteTask: vi.fn(),
|
||||||
|
determinePiece: vi.fn(),
|
||||||
|
saveTaskFromInteractive: vi.fn(),
|
||||||
|
createIssueFromTask: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/pipeline/index.js', () => ({
|
||||||
|
executePipeline: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../features/interactive/index.js', () => ({
|
||||||
|
interactiveMode: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
|
getPieceDescription: vi.fn(() => ({ name: 'default', description: 'test piece', pieceStructure: '' })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../shared/constants.js', () => ({
|
||||||
|
DEFAULT_PIECE_NAME: 'default',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockOpts: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
vi.mock('../app/cli/program.js', () => {
|
||||||
|
const chainable = {
|
||||||
|
opts: vi.fn(() => mockOpts),
|
||||||
|
argument: vi.fn().mockReturnThis(),
|
||||||
|
action: vi.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
program: chainable,
|
||||||
|
resolvedCwd: '/test/cwd',
|
||||||
|
pipelineMode: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../app/cli/helpers.js', () => ({
|
||||||
|
resolveAgentOverrides: vi.fn(),
|
||||||
|
parseCreateWorktreeOption: vi.fn(),
|
||||||
|
isDirectTask: vi.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
||||||
|
import { selectAndExecuteTask, determinePiece } from '../features/tasks/index.js';
|
||||||
|
import { interactiveMode } from '../features/interactive/index.js';
|
||||||
|
import { isDirectTask } from '../app/cli/helpers.js';
|
||||||
|
import { executeDefaultAction } from '../app/cli/routing.js';
|
||||||
|
import type { GitHubIssue } from '../infra/github/types.js';
|
||||||
|
|
||||||
|
const mockCheckGhCli = vi.mocked(checkGhCli);
|
||||||
|
const mockFetchIssue = vi.mocked(fetchIssue);
|
||||||
|
const mockFormatIssueAsTask = vi.mocked(formatIssueAsTask);
|
||||||
|
const mockParseIssueNumbers = vi.mocked(parseIssueNumbers);
|
||||||
|
const mockSelectAndExecuteTask = vi.mocked(selectAndExecuteTask);
|
||||||
|
const mockDeterminePiece = vi.mocked(determinePiece);
|
||||||
|
const mockInteractiveMode = vi.mocked(interactiveMode);
|
||||||
|
const mockIsDirectTask = vi.mocked(isDirectTask);
|
||||||
|
|
||||||
|
function createMockIssue(number: number): GitHubIssue {
|
||||||
|
return {
|
||||||
|
number,
|
||||||
|
title: `Issue #${number}`,
|
||||||
|
body: `Body of issue #${number}`,
|
||||||
|
labels: [],
|
||||||
|
comments: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
// Reset opts
|
||||||
|
for (const key of Object.keys(mockOpts)) {
|
||||||
|
delete mockOpts[key];
|
||||||
|
}
|
||||||
|
// Default setup
|
||||||
|
mockDeterminePiece.mockResolvedValue('default');
|
||||||
|
mockInteractiveMode.mockResolvedValue({ action: 'execute', task: 'summarized task' });
|
||||||
|
mockIsDirectTask.mockReturnValue(false);
|
||||||
|
mockParseIssueNumbers.mockReturnValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Issue resolution in routing', () => {
|
||||||
|
describe('--issue option', () => {
|
||||||
|
it('should resolve issue and pass to interactive mode when --issue is specified', async () => {
|
||||||
|
// Given
|
||||||
|
mockOpts.issue = 131;
|
||||||
|
const issue131 = createMockIssue(131);
|
||||||
|
mockCheckGhCli.mockReturnValue({ available: true });
|
||||||
|
mockFetchIssue.mockReturnValue(issue131);
|
||||||
|
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');
|
||||||
|
|
||||||
|
// When
|
||||||
|
await executeDefaultAction();
|
||||||
|
|
||||||
|
// Then: issue should be fetched
|
||||||
|
expect(mockFetchIssue).toHaveBeenCalledWith(131);
|
||||||
|
|
||||||
|
// Then: interactive mode should receive the formatted issue as initial input
|
||||||
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
|
'/test/cwd',
|
||||||
|
'## GitHub Issue #131: Issue #131',
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then: selectAndExecuteTask should receive issues in options
|
||||||
|
expect(mockSelectAndExecuteTask).toHaveBeenCalledWith(
|
||||||
|
'/test/cwd',
|
||||||
|
'summarized task',
|
||||||
|
expect.objectContaining({
|
||||||
|
issues: [issue131],
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exit with error when gh CLI is unavailable for --issue', async () => {
|
||||||
|
// Given
|
||||||
|
mockOpts.issue = 131;
|
||||||
|
mockCheckGhCli.mockReturnValue({
|
||||||
|
available: false,
|
||||||
|
error: 'gh CLI is not installed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||||
|
throw new Error('process.exit called');
|
||||||
|
});
|
||||||
|
|
||||||
|
// When / Then
|
||||||
|
await expect(executeDefaultAction()).rejects.toThrow('process.exit called');
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
expect(mockInteractiveMode).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
mockExit.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#N positional argument', () => {
|
||||||
|
it('should resolve issue reference and pass to interactive mode', async () => {
|
||||||
|
// Given
|
||||||
|
const issue131 = createMockIssue(131);
|
||||||
|
mockIsDirectTask.mockReturnValue(true);
|
||||||
|
mockCheckGhCli.mockReturnValue({ available: true });
|
||||||
|
mockFetchIssue.mockReturnValue(issue131);
|
||||||
|
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131: Issue #131');
|
||||||
|
mockParseIssueNumbers.mockReturnValue([131]);
|
||||||
|
|
||||||
|
// When
|
||||||
|
await executeDefaultAction('#131');
|
||||||
|
|
||||||
|
// Then: interactive mode should be entered with formatted issue
|
||||||
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
|
'/test/cwd',
|
||||||
|
'## GitHub Issue #131: Issue #131',
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then: selectAndExecuteTask should receive issues
|
||||||
|
expect(mockSelectAndExecuteTask).toHaveBeenCalledWith(
|
||||||
|
'/test/cwd',
|
||||||
|
'summarized task',
|
||||||
|
expect.objectContaining({
|
||||||
|
issues: [issue131],
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('non-issue input', () => {
|
||||||
|
it('should pass regular text input to interactive mode without issues', async () => {
|
||||||
|
// When
|
||||||
|
await executeDefaultAction('refactor the code');
|
||||||
|
|
||||||
|
// Then: interactive mode should receive the original text
|
||||||
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
|
'/test/cwd',
|
||||||
|
'refactor the code',
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then: no issue fetching should occur
|
||||||
|
expect(mockFetchIssue).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Then: selectAndExecuteTask should be called without issues
|
||||||
|
const callArgs = mockSelectAndExecuteTask.mock.calls[0];
|
||||||
|
expect(callArgs?.[2]?.issues).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enter interactive mode with no input when no args provided', async () => {
|
||||||
|
// When
|
||||||
|
await executeDefaultAction();
|
||||||
|
|
||||||
|
// Then: interactive mode should be entered with undefined input
|
||||||
|
expect(mockInteractiveMode).toHaveBeenCalledWith(
|
||||||
|
'/test/cwd',
|
||||||
|
undefined,
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Then: no issue fetching should occur
|
||||||
|
expect(mockFetchIssue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interactive mode cancel', () => {
|
||||||
|
it('should not call selectAndExecuteTask when interactive mode is cancelled', async () => {
|
||||||
|
// Given
|
||||||
|
mockOpts.issue = 131;
|
||||||
|
const issue131 = createMockIssue(131);
|
||||||
|
mockCheckGhCli.mockReturnValue({ available: true });
|
||||||
|
mockFetchIssue.mockReturnValue(issue131);
|
||||||
|
mockFormatIssueAsTask.mockReturnValue('## GitHub Issue #131');
|
||||||
|
mockInteractiveMode.mockResolvedValue({ action: 'cancel', task: '' });
|
||||||
|
|
||||||
|
// When
|
||||||
|
await executeDefaultAction();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockSelectAndExecuteTask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -451,4 +451,41 @@ describe('resolveTaskExecution', () => {
|
|||||||
expect(mockGetCurrentBranch).not.toHaveBeenCalled();
|
expect(mockGetCurrentBranch).not.toHaveBeenCalled();
|
||||||
expect(result.baseBranch).toBeUndefined();
|
expect(result.baseBranch).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return issueNumber from task data when specified', async () => {
|
||||||
|
// Given: Task with issue number
|
||||||
|
const task: TaskInfo = {
|
||||||
|
name: 'task-with-issue',
|
||||||
|
content: 'Fix authentication bug',
|
||||||
|
filePath: '/tasks/task.yaml',
|
||||||
|
data: {
|
||||||
|
task: 'Fix authentication bug',
|
||||||
|
issue: 131,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await resolveTaskExecution(task, '/project', 'default');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.issueNumber).toBe(131);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined issueNumber when task data has no issue', async () => {
|
||||||
|
// Given: Task without issue
|
||||||
|
const task: TaskInfo = {
|
||||||
|
name: 'task-no-issue',
|
||||||
|
content: 'Task content',
|
||||||
|
filePath: '/tasks/task.yaml',
|
||||||
|
data: {
|
||||||
|
task: 'Task content',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = await resolveTaskExecution(task, '/project', 'default');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result.issueNumber).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { info, error } from '../../shared/ui/index.js';
|
import { info, error } from '../../shared/ui/index.js';
|
||||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers } from '../../infra/github/index.js';
|
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
|
||||||
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||||
import { executePipeline } from '../../features/pipeline/index.js';
|
import { executePipeline } from '../../features/pipeline/index.js';
|
||||||
import { interactiveMode } from '../../features/interactive/index.js';
|
import { interactiveMode } from '../../features/interactive/index.js';
|
||||||
@ -16,6 +16,48 @@ import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
|||||||
import { program, resolvedCwd, pipelineMode } from './program.js';
|
import { program, resolvedCwd, pipelineMode } from './program.js';
|
||||||
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve issue references from CLI input.
|
||||||
|
*
|
||||||
|
* Handles two sources:
|
||||||
|
* - --issue N option (numeric issue number)
|
||||||
|
* - Positional argument containing issue references (#N or "#1 #2")
|
||||||
|
*
|
||||||
|
* Returns resolved issues and the formatted task text for interactive mode.
|
||||||
|
* Throws on gh CLI unavailability or fetch failure.
|
||||||
|
*/
|
||||||
|
function resolveIssueInput(
|
||||||
|
issueOption: number | undefined,
|
||||||
|
task: string | undefined,
|
||||||
|
): { issues: GitHubIssue[]; initialInput: string } | null {
|
||||||
|
if (issueOption) {
|
||||||
|
info('Fetching GitHub Issue...');
|
||||||
|
const ghStatus = checkGhCli();
|
||||||
|
if (!ghStatus.available) {
|
||||||
|
throw new Error(ghStatus.error);
|
||||||
|
}
|
||||||
|
const issue = fetchIssue(issueOption);
|
||||||
|
return { issues: [issue], initialInput: formatIssueAsTask(issue) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task && isDirectTask(task)) {
|
||||||
|
info('Fetching GitHub Issue...');
|
||||||
|
const ghStatus = checkGhCli();
|
||||||
|
if (!ghStatus.available) {
|
||||||
|
throw new Error(ghStatus.error);
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
return { issues, initialInput: issues.map(formatIssueAsTask).join('\n\n---\n\n') };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute default action: handle task execution, pipeline mode, or interactive mode.
|
* Execute default action: handle task execution, pipeline mode, or interactive mode.
|
||||||
* Exported for use in slash-command fallback logic.
|
* Exported for use in slash-command fallback logic.
|
||||||
@ -54,58 +96,28 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
|||||||
|
|
||||||
// --- Normal (interactive) mode ---
|
// --- Normal (interactive) mode ---
|
||||||
|
|
||||||
// Resolve --task option to task text
|
// Resolve --task option to task text (direct execution, no interactive mode)
|
||||||
const taskFromOption = opts.task as string | undefined;
|
const taskFromOption = opts.task as string | undefined;
|
||||||
if (taskFromOption) {
|
if (taskFromOption) {
|
||||||
await selectAndExecuteTask(resolvedCwd, taskFromOption, selectOptions, agentOverrides);
|
await selectAndExecuteTask(resolvedCwd, taskFromOption, selectOptions, agentOverrides);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve --issue N to task text (same as #N)
|
// Resolve issue references (--issue N or #N positional arg) before interactive mode
|
||||||
const issueFromOption = opts.issue as number | undefined;
|
let initialInput: string | undefined = task;
|
||||||
if (issueFromOption) {
|
|
||||||
try {
|
try {
|
||||||
const ghStatus = checkGhCli();
|
const issueResult = resolveIssueInput(opts.issue as number | undefined, task);
|
||||||
if (!ghStatus.available) {
|
if (issueResult) {
|
||||||
throw new Error(ghStatus.error);
|
selectOptions.issues = issueResult.issues;
|
||||||
|
initialInput = issueResult.initialInput;
|
||||||
}
|
}
|
||||||
const issue = fetchIssue(issueFromOption);
|
|
||||||
const resolvedTask = formatIssueAsTask(issue);
|
|
||||||
selectOptions.issues = [issue];
|
|
||||||
await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(getErrorMessage(e));
|
error(getErrorMessage(e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task && isDirectTask(task)) {
|
// All paths below go through interactive mode
|
||||||
// isDirectTask() returns true only for issue references (e.g., "#6" or "#1 #2")
|
|
||||||
try {
|
|
||||||
info('Fetching GitHub Issue...');
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-issue inputs → interactive mode (with optional initial input)
|
|
||||||
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
||||||
if (pieceId === null) {
|
if (pieceId === null) {
|
||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
@ -113,7 +125,7 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pieceContext = getPieceDescription(pieceId, resolvedCwd);
|
const pieceContext = getPieceDescription(pieceId, resolvedCwd);
|
||||||
const result = await interactiveMode(resolvedCwd, task, pieceContext);
|
const result = await interactiveMode(resolvedCwd, initialInput, pieceContext);
|
||||||
|
|
||||||
switch (result.action) {
|
switch (result.action) {
|
||||||
case 'execute':
|
case 'execute':
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export interface ResolvedTaskExecution {
|
|||||||
startMovement?: string;
|
startMovement?: string;
|
||||||
retryNote?: string;
|
retryNote?: string;
|
||||||
autoPr?: boolean;
|
autoPr?: boolean;
|
||||||
|
issueNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,5 +69,5 @@ export async function resolveTaskExecution(
|
|||||||
autoPr = globalConfig.autoPr;
|
autoPr = globalConfig.autoPr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { execCwd, execPiece, isWorktree, branch, baseBranch, startMovement, retryNote, autoPr };
|
return { execCwd, execPiece, isWorktree, branch, baseBranch, startMovement, retryNote, autoPr, issueNumber: data.issue };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
|||||||
import { executePiece } from './pieceExecution.js';
|
import { executePiece } from './pieceExecution.js';
|
||||||
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
|
import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
|
||||||
import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js';
|
import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js';
|
||||||
import { createPullRequest, buildPrBody, pushBranch } from '../../../infra/github/index.js';
|
import { createPullRequest, buildPrBody, pushBranch, fetchIssue, checkGhCli } from '../../../infra/github/index.js';
|
||||||
import { runWithWorkerPool } from './parallelExecution.js';
|
import { runWithWorkerPool } from './parallelExecution.js';
|
||||||
import { resolveTaskExecution } from './resolveTask.js';
|
import { resolveTaskExecution } from './resolveTask.js';
|
||||||
|
|
||||||
@ -24,6 +24,30 @@ export type { TaskExecutionOptions, ExecuteTaskOptions };
|
|||||||
|
|
||||||
const log = createLogger('task');
|
const log = createLogger('task');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a GitHub issue from task data's issue number.
|
||||||
|
* Returns issue array for buildPrBody, or undefined if no issue or gh CLI unavailable.
|
||||||
|
*/
|
||||||
|
function resolveTaskIssue(issueNumber: number | undefined): ReturnType<typeof fetchIssue>[] | undefined {
|
||||||
|
if (issueNumber === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghStatus = checkGhCli();
|
||||||
|
if (!ghStatus.available) {
|
||||||
|
log.info('gh CLI unavailable, skipping issue resolution for PR body', { issueNumber });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issue = fetchIssue(issueNumber);
|
||||||
|
return [issue];
|
||||||
|
} catch (e) {
|
||||||
|
log.info('Failed to fetch issue for PR body, continuing without issue info', { issueNumber, error: getErrorMessage(e) });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a single task with piece.
|
* Execute a single task with piece.
|
||||||
*/
|
*/
|
||||||
@ -83,7 +107,7 @@ export async function executeAndCompleteTask(
|
|||||||
const executionLog: string[] = [];
|
const executionLog: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { execCwd, execPiece, isWorktree, branch, baseBranch, startMovement, retryNote, autoPr } = await resolveTaskExecution(task, cwd, pieceName);
|
const { execCwd, execPiece, isWorktree, branch, baseBranch, startMovement, retryNote, autoPr, issueNumber } = await resolveTaskExecution(task, cwd, pieceName);
|
||||||
|
|
||||||
// cwd is always the project root; pass it as projectCwd so reports/sessions go there
|
// cwd is always the project root; pass it as projectCwd so reports/sessions go there
|
||||||
const taskSuccess = await executeTask({
|
const taskSuccess = await executeTask({
|
||||||
@ -117,7 +141,8 @@ export async function executeAndCompleteTask(
|
|||||||
// Branch may already be pushed, continue to PR creation
|
// Branch may already be pushed, continue to PR creation
|
||||||
log.info('Branch push from project cwd failed (may already exist)', { error: pushError });
|
log.info('Branch push from project cwd failed (may already exist)', { error: pushError });
|
||||||
}
|
}
|
||||||
const prBody = buildPrBody(undefined, `Task "${task.name}" completed successfully.`);
|
const issues = resolveTaskIssue(issueNumber);
|
||||||
|
const prBody = buildPrBody(issues, `Piece \`${execPiece}\` completed successfully.`);
|
||||||
const prResult = createPullRequest(cwd, {
|
const prResult = createPullRequest(cwd, {
|
||||||
branch,
|
branch,
|
||||||
title: task.name.length > 100 ? `${task.name.slice(0, 97)}...` : task.name,
|
title: task.name.length > 100 ? `${task.name.slice(0, 97)}...` : task.name,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user