takt: add-draft-pr-option (#323)
This commit is contained in:
parent
5960a0d212
commit
4f8255d509
@ -42,6 +42,15 @@ describe('config env overrides', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('TAKT_DRAFT_PR が draft_pr に反映される', () => {
|
||||
process.env.TAKT_DRAFT_PR = 'true';
|
||||
|
||||
const raw: Record<string, unknown> = {};
|
||||
applyGlobalConfigEnvOverrides(raw);
|
||||
|
||||
expect(raw.draft_pr).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply project env overrides from generated env names', () => {
|
||||
process.env.TAKT_VERBOSE = 'true';
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
getErrorMessage: (e: unknown) => String(e),
|
||||
}));
|
||||
|
||||
import { buildPrBody, findExistingPr } from '../infra/github/pr.js';
|
||||
import { buildPrBody, findExistingPr, createPullRequest } from '../infra/github/pr.js';
|
||||
import type { GitHubIssue } from '../infra/github/types.js';
|
||||
|
||||
describe('findExistingPr', () => {
|
||||
@ -59,6 +59,53 @@ describe('findExistingPr', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPullRequest', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('draft: true の場合、args に --draft が含まれる', () => {
|
||||
mockExecFileSync.mockReturnValue('https://github.com/org/repo/pull/1\n');
|
||||
|
||||
createPullRequest('/project', {
|
||||
branch: 'feat/my-branch',
|
||||
title: 'My PR',
|
||||
body: 'PR body',
|
||||
draft: true,
|
||||
});
|
||||
|
||||
const call = mockExecFileSync.mock.calls[0];
|
||||
expect(call[1]).toContain('--draft');
|
||||
});
|
||||
|
||||
it('draft: false の場合、args に --draft が含まれない', () => {
|
||||
mockExecFileSync.mockReturnValue('https://github.com/org/repo/pull/2\n');
|
||||
|
||||
createPullRequest('/project', {
|
||||
branch: 'feat/my-branch',
|
||||
title: 'My PR',
|
||||
body: 'PR body',
|
||||
draft: false,
|
||||
});
|
||||
|
||||
const call = mockExecFileSync.mock.calls[0];
|
||||
expect(call[1]).not.toContain('--draft');
|
||||
});
|
||||
|
||||
it('draft が未指定の場合、args に --draft が含まれない', () => {
|
||||
mockExecFileSync.mockReturnValue('https://github.com/org/repo/pull/3\n');
|
||||
|
||||
createPullRequest('/project', {
|
||||
branch: 'feat/my-branch',
|
||||
title: 'My PR',
|
||||
body: 'PR body',
|
||||
});
|
||||
|
||||
const call = mockExecFileSync.mock.calls[0];
|
||||
expect(call[1]).not.toContain('--draft');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPrBody', () => {
|
||||
it('should build body with single issue and report', () => {
|
||||
const issue: GitHubIssue = {
|
||||
|
||||
@ -218,6 +218,46 @@ describe('executePipeline', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('draftPr: true の場合、createPullRequest に draft: true が渡される', async () => {
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/test/pr/1' });
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
task: 'Fix the bug',
|
||||
piece: 'default',
|
||||
branch: 'fix/my-branch',
|
||||
autoPr: true,
|
||||
draftPr: true,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
expect.objectContaining({ draft: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it('draftPr: false の場合、createPullRequest に draft: false が渡される', async () => {
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/test/pr/1' });
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
task: 'Fix the bug',
|
||||
piece: 'default',
|
||||
branch: 'fix/my-branch',
|
||||
autoPr: true,
|
||||
draftPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
expect.objectContaining({ draft: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass baseBranch as base to createPullRequest', async () => {
|
||||
// Given: getCurrentBranch returns 'develop' before branch creation
|
||||
mockExecFileSync.mockImplementation((_cmd: string, args: string[]) => {
|
||||
|
||||
@ -51,7 +51,12 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import { postExecutionFlow } from '../features/tasks/execute/postExecution.js';
|
||||
import { postExecutionFlow, resolveDraftPr } from '../features/tasks/execute/postExecution.js';
|
||||
import { resolvePieceConfigValue } from '../infra/config/index.js';
|
||||
import { confirm } from '../shared/prompt/index.js';
|
||||
|
||||
const mockResolvePieceConfigValue = vi.mocked(resolvePieceConfigValue);
|
||||
const mockConfirm = vi.mocked(confirm);
|
||||
|
||||
const baseOptions = {
|
||||
execCwd: '/clone',
|
||||
@ -60,6 +65,7 @@ const baseOptions = {
|
||||
branch: 'task/fix-the-bug',
|
||||
baseBranch: 'main',
|
||||
shouldCreatePr: true,
|
||||
draftPr: false,
|
||||
pieceIdentifier: 'default',
|
||||
};
|
||||
|
||||
@ -113,4 +119,60 @@ describe('postExecutionFlow', () => {
|
||||
expect(mockFindExistingPr).not.toHaveBeenCalled();
|
||||
expect(mockCreatePullRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('draftPr: true の場合、createPullRequest に draft: true が渡される', async () => {
|
||||
mockFindExistingPr.mockReturnValue(undefined);
|
||||
|
||||
await postExecutionFlow({ ...baseOptions, draftPr: true });
|
||||
|
||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||
'/project',
|
||||
expect.objectContaining({ draft: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it('draftPr: false の場合、createPullRequest に draft: false が渡される', async () => {
|
||||
mockFindExistingPr.mockReturnValue(undefined);
|
||||
|
||||
await postExecutionFlow({ ...baseOptions, draftPr: false });
|
||||
|
||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||
'/project',
|
||||
expect.objectContaining({ draft: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveDraftPr', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('CLI オプション true が渡された場合は true を返す', async () => {
|
||||
const result = await resolveDraftPr(true, '/project');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('CLI オプション false が渡された場合は false を返す', async () => {
|
||||
const result = await resolveDraftPr(false, '/project');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('CLI オプションが未指定で config が true の場合は true を返す', async () => {
|
||||
mockResolvePieceConfigValue.mockReturnValue(true);
|
||||
|
||||
const result = await resolveDraftPr(undefined, '/project');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('CLI オプション・config ともに未指定の場合はプロンプトを表示する', async () => {
|
||||
mockResolvePieceConfigValue.mockReturnValue(undefined);
|
||||
mockConfirm.mockResolvedValue(false);
|
||||
|
||||
const result = await resolveDraftPr(undefined, '/project');
|
||||
|
||||
expect(mockConfirm).toHaveBeenCalledWith('Create as draft?', true);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -48,6 +48,7 @@ describe('resolveTaskExecution', () => {
|
||||
execPiece: 'default',
|
||||
isWorktree: false,
|
||||
autoPr: false,
|
||||
draftPr: false,
|
||||
});
|
||||
});
|
||||
|
||||
@ -76,6 +77,7 @@ describe('resolveTaskExecution', () => {
|
||||
execPiece: 'default',
|
||||
isWorktree: false,
|
||||
autoPr: true,
|
||||
draftPr: false,
|
||||
reportDirName: 'issue-task-123',
|
||||
issueNumber: 12345,
|
||||
taskPrompt: expect.stringContaining('Primary spec: `.takt/runs/issue-task-123/context/task/order.md`'),
|
||||
@ -83,4 +85,20 @@ describe('resolveTaskExecution', () => {
|
||||
expect(fs.existsSync(expectedReportOrderPath)).toBe(true);
|
||||
expect(fs.readFileSync(expectedReportOrderPath, 'utf-8')).toBe('# task instruction');
|
||||
});
|
||||
|
||||
it('draft_pr: true が draftPr: true として解決される', async () => {
|
||||
const root = createTempProjectDir();
|
||||
const task = createTask({
|
||||
data: {
|
||||
task: 'Run draft task',
|
||||
auto_pr: true,
|
||||
draft_pr: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resolveTaskExecution(task, root, 'default');
|
||||
|
||||
expect(result.draftPr).toBe(true);
|
||||
expect(result.autoPr).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -103,6 +103,17 @@ describe('saveTaskFile', () => {
|
||||
expect(task.task_dir).toBeTypeOf('string');
|
||||
});
|
||||
|
||||
it('draftPr: true が draft_pr: true として保存される', async () => {
|
||||
await saveTaskFile(testDir, 'Draft task', {
|
||||
autoPr: true,
|
||||
draftPr: true,
|
||||
});
|
||||
|
||||
const task = loadTasks(testDir).tasks[0]!;
|
||||
expect(task.auto_pr).toBe(true);
|
||||
expect(task.draft_pr).toBe(true);
|
||||
});
|
||||
|
||||
it('should generate unique names on duplicates', async () => {
|
||||
const first = await saveTaskFile(testDir, 'Same title');
|
||||
const second = await saveTaskFile(testDir, 'Same title');
|
||||
@ -122,7 +133,8 @@ describe('saveTaskFromInteractive', () => {
|
||||
it('should always save task with worktree settings', async () => {
|
||||
mockPromptInput.mockResolvedValueOnce('');
|
||||
mockPromptInput.mockResolvedValueOnce('');
|
||||
mockConfirm.mockResolvedValueOnce(true);
|
||||
mockConfirm.mockResolvedValueOnce(true); // auto-create PR?
|
||||
mockConfirm.mockResolvedValueOnce(true); // create as draft?
|
||||
|
||||
await saveTaskFromInteractive(testDir, 'Task content');
|
||||
|
||||
@ -130,6 +142,7 @@ describe('saveTaskFromInteractive', () => {
|
||||
const task = loadTasks(testDir).tasks[0]!;
|
||||
expect(task.worktree).toBe(true);
|
||||
expect(task.auto_pr).toBe(true);
|
||||
expect(task.draft_pr).toBe(true);
|
||||
});
|
||||
|
||||
it('should keep worktree enabled even when auto-pr is declined', async () => {
|
||||
|
||||
@ -127,6 +127,50 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => {
|
||||
expect(autoPrCall![1]).toBe(true);
|
||||
});
|
||||
|
||||
it('shouldCreatePr=true の場合、"Create as draft?" プロンプトが表示される', async () => {
|
||||
// confirm はすべての呼び出しに対して true を返す(autoPr=true → draftPr prompt)
|
||||
mockConfirm.mockResolvedValue(true);
|
||||
mockSummarizeTaskName.mockResolvedValue('test-task');
|
||||
mockCreateSharedClone.mockReturnValue({
|
||||
path: '/project/../clone',
|
||||
branch: 'takt/test-task',
|
||||
});
|
||||
mockAutoCommitAndPush.mockReturnValue({
|
||||
success: false,
|
||||
message: 'no changes',
|
||||
});
|
||||
|
||||
await selectAndExecuteTask('/project', 'test task', {
|
||||
piece: 'default',
|
||||
createWorktree: true,
|
||||
});
|
||||
|
||||
const draftPrCall = mockConfirm.mock.calls.find((call) => call[0] === 'Create as draft?');
|
||||
expect(draftPrCall).toBeDefined();
|
||||
expect(draftPrCall![1]).toBe(true);
|
||||
});
|
||||
|
||||
it('shouldCreatePr=false の場合、"Create as draft?" プロンプトは表示されない', async () => {
|
||||
mockConfirm.mockResolvedValue(false); // autoPr=false → draft prompt skipped
|
||||
mockSummarizeTaskName.mockResolvedValue('test-task');
|
||||
mockCreateSharedClone.mockReturnValue({
|
||||
path: '/project/../clone',
|
||||
branch: 'takt/test-task',
|
||||
});
|
||||
mockAutoCommitAndPush.mockReturnValue({
|
||||
success: false,
|
||||
message: 'no changes',
|
||||
});
|
||||
|
||||
await selectAndExecuteTask('/project', 'test task', {
|
||||
piece: 'default',
|
||||
createWorktree: true,
|
||||
});
|
||||
|
||||
const draftPrCall = mockConfirm.mock.calls.find((call) => call[0] === 'Create as draft?');
|
||||
expect(draftPrCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should call selectPiece when no override is provided', async () => {
|
||||
mockSelectPiece.mockResolvedValue('selected-piece');
|
||||
|
||||
@ -175,6 +219,7 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => {
|
||||
branch: 'takt/test-task',
|
||||
worktree_path: '/project/../clone',
|
||||
auto_pr: true,
|
||||
draft_pr: true,
|
||||
}));
|
||||
expect(mockCompleteTask).toHaveBeenCalledTimes(1);
|
||||
expect(mockFailTask).not.toHaveBeenCalled();
|
||||
|
||||
@ -44,6 +44,7 @@ program
|
||||
.option('-w, --piece <name>', 'Piece name or path to piece file')
|
||||
.option('-b, --branch <name>', 'Branch name (auto-generated if omitted)')
|
||||
.option('--auto-pr', 'Create PR after successful execution')
|
||||
.option('--draft', 'Create PR as draft (requires --auto-pr or auto_pr config)')
|
||||
.option('--repo <owner/repo>', 'Repository (defaults to current)')
|
||||
.option('--provider <name>', 'Override agent provider (claude|codex|opencode|mock)')
|
||||
.option('--model <name>', 'Override agent model')
|
||||
|
||||
@ -86,8 +86,12 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
const resolvedPipelineAutoPr = opts.autoPr === true
|
||||
? true
|
||||
: (resolveConfigValue(resolvedCwd, 'autoPr') ?? false);
|
||||
const resolvedPipelineDraftPr = opts.draft === true
|
||||
? true
|
||||
: (resolveConfigValue(resolvedCwd, 'draftPr') ?? false);
|
||||
const selectOptions: SelectAndExecuteOptions = {
|
||||
autoPr: opts.autoPr === true ? true : undefined,
|
||||
draftPr: opts.draft === true ? true : undefined,
|
||||
repo: opts.repo as string | undefined,
|
||||
piece: opts.piece as string | undefined,
|
||||
createWorktree: createWorktreeOverride,
|
||||
@ -101,6 +105,7 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
piece: resolvedPipelinePiece,
|
||||
branch: opts.branch as string | undefined,
|
||||
autoPr: resolvedPipelineAutoPr,
|
||||
draftPr: resolvedPipelineDraftPr,
|
||||
repo: opts.repo as string | undefined,
|
||||
skipGit: opts.skipGit === true,
|
||||
cwd: resolvedCwd,
|
||||
|
||||
@ -72,6 +72,8 @@ export interface GlobalConfig {
|
||||
worktreeDir?: string;
|
||||
/** Auto-create PR after worktree execution (default: prompt in interactive mode) */
|
||||
autoPr?: boolean;
|
||||
/** Create PR as draft (default: prompt in interactive mode when autoPr is true) */
|
||||
draftPr?: boolean;
|
||||
/** List of builtin piece/agent names to exclude from fallback loading */
|
||||
disabledBuiltins?: string[];
|
||||
/** Enable builtin pieces from builtins/{lang}/pieces */
|
||||
|
||||
@ -421,6 +421,8 @@ export const GlobalConfigSchema = z.object({
|
||||
worktree_dir: z.string().optional(),
|
||||
/** Auto-create PR after worktree execution (default: prompt in interactive mode) */
|
||||
auto_pr: z.boolean().optional(),
|
||||
/** Create PR as draft (default: prompt in interactive mode when auto_pr is true) */
|
||||
draft_pr: z.boolean().optional(),
|
||||
/** List of builtin piece/agent names to exclude from fallback loading */
|
||||
disabled_builtins: z.array(z.string()).optional().default([]),
|
||||
/** Enable builtin pieces from builtins/{lang}/pieces */
|
||||
|
||||
@ -105,7 +105,7 @@ function buildPipelinePrBody(
|
||||
* Returns a process exit code (0 on success, 2-5 on specific failures).
|
||||
*/
|
||||
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
|
||||
const { cwd, piece, autoPr, skipGit } = options;
|
||||
const { cwd, piece, autoPr, draftPr, skipGit } = options;
|
||||
const globalConfig = resolveConfigValues(cwd, ['pipeline']);
|
||||
const pipelineConfig = globalConfig.pipeline;
|
||||
let issue: GitHubIssue | undefined;
|
||||
@ -210,6 +210,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
|
||||
body: prBody,
|
||||
base: baseBranch,
|
||||
repo: options.repo,
|
||||
draft: draftPr,
|
||||
});
|
||||
|
||||
if (prResult.success) {
|
||||
|
||||
@ -37,7 +37,7 @@ function resolveUniqueTaskSlug(cwd: string, baseSlug: string): string {
|
||||
export async function saveTaskFile(
|
||||
cwd: string,
|
||||
taskContent: string,
|
||||
options?: { piece?: string; issue?: number; worktree?: boolean | string; branch?: string; autoPr?: boolean },
|
||||
options?: { piece?: string; issue?: number; worktree?: boolean | string; branch?: string; autoPr?: boolean; draftPr?: boolean },
|
||||
): Promise<{ taskName: string; tasksFile: string }> {
|
||||
const runner = new TaskRunner(cwd);
|
||||
const slug = await summarizeTaskName(taskContent, { cwd });
|
||||
@ -54,6 +54,7 @@ export async function saveTaskFile(
|
||||
...(options?.piece && { piece: options.piece }),
|
||||
...(options?.issue !== undefined && { issue: options.issue }),
|
||||
...(options?.autoPr !== undefined && { auto_pr: options.autoPr }),
|
||||
...(options?.draftPr !== undefined && { draft_pr: options.draftPr }),
|
||||
};
|
||||
const created = runner.addTask(taskContent, {
|
||||
...config,
|
||||
@ -95,6 +96,7 @@ interface WorktreeSettings {
|
||||
worktree?: boolean | string;
|
||||
branch?: string;
|
||||
autoPr?: boolean;
|
||||
draftPr?: boolean;
|
||||
}
|
||||
|
||||
function displayTaskCreationResult(
|
||||
@ -113,6 +115,9 @@ function displayTaskCreationResult(
|
||||
if (settings.autoPr) {
|
||||
info(` Auto-PR: yes`);
|
||||
}
|
||||
if (settings.draftPr) {
|
||||
info(` Draft PR: yes`);
|
||||
}
|
||||
if (piece) info(` Piece: ${piece}`);
|
||||
}
|
||||
|
||||
@ -137,8 +142,9 @@ async function promptWorktreeSettings(): Promise<WorktreeSettings> {
|
||||
const branch = customBranch || undefined;
|
||||
|
||||
const autoPr = await confirm('Auto-create PR?', true);
|
||||
const draftPr = autoPr ? await confirm('Create as draft?', true) : false;
|
||||
|
||||
return { worktree, branch, autoPr };
|
||||
return { worktree, branch, autoPr, draftPr };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -15,19 +15,38 @@ import type { GitHubIssue } from '../../../infra/github/index.js';
|
||||
|
||||
const log = createLogger('postExecution');
|
||||
|
||||
/**
|
||||
* Resolve a boolean PR option with priority: CLI option > config > prompt.
|
||||
*/
|
||||
async function resolvePrBooleanOption(
|
||||
option: boolean | undefined,
|
||||
cwd: string,
|
||||
configKey: 'autoPr' | 'draftPr',
|
||||
promptMessage: string,
|
||||
): Promise<boolean> {
|
||||
if (typeof option === 'boolean') {
|
||||
return option;
|
||||
}
|
||||
const configValue = resolvePieceConfigValue(cwd, configKey);
|
||||
if (typeof configValue === 'boolean') {
|
||||
return configValue;
|
||||
}
|
||||
return confirm(promptMessage, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve auto-PR setting with priority: CLI option > config > prompt.
|
||||
*/
|
||||
export async function resolveAutoPr(optionAutoPr: boolean | undefined, cwd: string): Promise<boolean> {
|
||||
if (typeof optionAutoPr === 'boolean') {
|
||||
return optionAutoPr;
|
||||
return resolvePrBooleanOption(optionAutoPr, cwd, 'autoPr', 'Create pull request?');
|
||||
}
|
||||
|
||||
const autoPr = resolvePieceConfigValue(cwd, 'autoPr');
|
||||
if (typeof autoPr === 'boolean') {
|
||||
return autoPr;
|
||||
}
|
||||
return confirm('Create pull request?', true);
|
||||
/**
|
||||
* Resolve draft-PR setting with priority: CLI option > config > prompt.
|
||||
* Only called when shouldCreatePr is true.
|
||||
*/
|
||||
export async function resolveDraftPr(optionDraftPr: boolean | undefined, cwd: string): Promise<boolean> {
|
||||
return resolvePrBooleanOption(optionDraftPr, cwd, 'draftPr', 'Create as draft?');
|
||||
}
|
||||
|
||||
export interface PostExecutionOptions {
|
||||
@ -37,6 +56,7 @@ export interface PostExecutionOptions {
|
||||
branch?: string;
|
||||
baseBranch?: string;
|
||||
shouldCreatePr: boolean;
|
||||
draftPr: boolean;
|
||||
pieceIdentifier?: string;
|
||||
issues?: GitHubIssue[];
|
||||
repo?: string;
|
||||
@ -50,7 +70,7 @@ export interface PostExecutionResult {
|
||||
* Auto-commit, push, and optionally create a PR after successful task execution.
|
||||
*/
|
||||
export async function postExecutionFlow(options: PostExecutionOptions): Promise<PostExecutionResult> {
|
||||
const { execCwd, projectCwd, task, branch, baseBranch, shouldCreatePr, pieceIdentifier, issues, repo } = options;
|
||||
const { execCwd, projectCwd, task, branch, baseBranch, shouldCreatePr, draftPr, pieceIdentifier, issues, repo } = options;
|
||||
|
||||
const commitResult = autoCommitAndPush(execCwd, task, projectCwd);
|
||||
if (commitResult.success && commitResult.commitHash) {
|
||||
@ -86,6 +106,7 @@ export async function postExecutionFlow(options: PostExecutionOptions): Promise<
|
||||
body: prBody,
|
||||
base: baseBranch,
|
||||
repo,
|
||||
draft: draftPr,
|
||||
});
|
||||
if (prResult.success) {
|
||||
success(`PR created: ${prResult.url}`);
|
||||
|
||||
@ -25,6 +25,7 @@ export interface ResolvedTaskExecution {
|
||||
startMovement?: string;
|
||||
retryNote?: string;
|
||||
autoPr: boolean;
|
||||
draftPr: boolean;
|
||||
issueNumber?: number;
|
||||
}
|
||||
|
||||
@ -103,7 +104,7 @@ export async function resolveTaskExecution(
|
||||
|
||||
const data = task.data;
|
||||
if (!data) {
|
||||
return { execCwd: defaultCwd, execPiece: defaultPiece, isWorktree: false, autoPr: false };
|
||||
return { execCwd: defaultCwd, execPiece: defaultPiece, isWorktree: false, autoPr: false, draftPr: false };
|
||||
}
|
||||
|
||||
let execCwd = defaultCwd;
|
||||
@ -165,18 +166,15 @@ export async function resolveTaskExecution(
|
||||
const startMovement = data.start_movement;
|
||||
const retryNote = data.retry_note;
|
||||
|
||||
let autoPr: boolean;
|
||||
if (data.auto_pr !== undefined) {
|
||||
autoPr = data.auto_pr;
|
||||
} else {
|
||||
autoPr = resolvePieceConfigValue(defaultCwd, 'autoPr') ?? false;
|
||||
}
|
||||
const autoPr = data.auto_pr ?? resolvePieceConfigValue(defaultCwd, 'autoPr') ?? false;
|
||||
const draftPr = data.draft_pr ?? resolvePieceConfigValue(defaultCwd, 'draftPr') ?? false;
|
||||
|
||||
return {
|
||||
execCwd,
|
||||
execPiece,
|
||||
isWorktree,
|
||||
autoPr,
|
||||
draftPr,
|
||||
...(taskPrompt ? { taskPrompt } : {}),
|
||||
...(reportDirName ? { reportDirName } : {}),
|
||||
...(branch ? { branch } : {}),
|
||||
|
||||
@ -16,7 +16,7 @@ import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js';
|
||||
import { info, error, withProgress } from '../../../shared/ui/index.js';
|
||||
import { createLogger } from '../../../shared/utils/index.js';
|
||||
import { executeTask } from './taskExecution.js';
|
||||
import { resolveAutoPr, postExecutionFlow } from './postExecution.js';
|
||||
import { resolveAutoPr, resolveDraftPr, postExecutionFlow } from './postExecution.js';
|
||||
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
||||
import { selectPiece } from '../../pieceSelection/index.js';
|
||||
import { buildBooleanTaskResult, persistTaskError, persistTaskResult } from './taskResultHandler.js';
|
||||
@ -100,11 +100,15 @@ export async function selectAndExecuteTask(
|
||||
|
||||
// Ask for PR creation BEFORE execution (only if worktree is enabled)
|
||||
let shouldCreatePr = false;
|
||||
let shouldDraftPr = false;
|
||||
if (isWorktree) {
|
||||
shouldCreatePr = await resolveAutoPr(options?.autoPr, cwd);
|
||||
if (shouldCreatePr) {
|
||||
shouldDraftPr = await resolveDraftPr(options?.draftPr, cwd);
|
||||
}
|
||||
}
|
||||
|
||||
log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree, autoPr: shouldCreatePr });
|
||||
log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree, autoPr: shouldCreatePr, draftPr: shouldDraftPr });
|
||||
const taskRunner = new TaskRunner(cwd);
|
||||
const taskRecord = taskRunner.addTask(task, {
|
||||
piece: pieceIdentifier,
|
||||
@ -112,6 +116,7 @@ export async function selectAndExecuteTask(
|
||||
...(branch ? { branch } : {}),
|
||||
...(isWorktree ? { worktree_path: execCwd } : {}),
|
||||
auto_pr: shouldCreatePr,
|
||||
draft_pr: shouldDraftPr,
|
||||
...(taskSlug ? { slug: taskSlug } : {}),
|
||||
});
|
||||
const startedAt = new Date().toISOString();
|
||||
@ -157,6 +162,7 @@ export async function selectAndExecuteTask(
|
||||
branch,
|
||||
baseBranch,
|
||||
shouldCreatePr,
|
||||
draftPr: shouldDraftPr,
|
||||
pieceIdentifier,
|
||||
issues: options?.issues,
|
||||
repo: options?.repo,
|
||||
|
||||
@ -144,6 +144,7 @@ export async function executeAndCompleteTask(
|
||||
startMovement,
|
||||
retryNote,
|
||||
autoPr,
|
||||
draftPr,
|
||||
issueNumber,
|
||||
} = await resolveTaskExecution(task, cwd, pieceName, taskAbortSignal);
|
||||
|
||||
@ -176,6 +177,7 @@ export async function executeAndCompleteTask(
|
||||
branch,
|
||||
baseBranch,
|
||||
shouldCreatePr: autoPr,
|
||||
draftPr,
|
||||
pieceIdentifier: execPiece,
|
||||
issues,
|
||||
});
|
||||
|
||||
@ -107,6 +107,8 @@ export interface PipelineExecutionOptions {
|
||||
branch?: string;
|
||||
/** Whether to create a PR after successful execution */
|
||||
autoPr: boolean;
|
||||
/** Whether to create PR as draft */
|
||||
draftPr?: boolean;
|
||||
/** Repository in owner/repo format */
|
||||
repo?: string;
|
||||
/** Skip branch creation, commit, and push (piece-only execution) */
|
||||
@ -127,6 +129,7 @@ export interface WorktreeConfirmationResult {
|
||||
|
||||
export interface SelectAndExecuteOptions {
|
||||
autoPr?: boolean;
|
||||
draftPr?: boolean;
|
||||
repo?: string;
|
||||
piece?: string;
|
||||
createWorktree?: boolean | undefined;
|
||||
|
||||
1
src/infra/config/env/config-env-overrides.ts
vendored
1
src/infra/config/env/config-env-overrides.ts
vendored
@ -84,6 +84,7 @@ const GLOBAL_ENV_SPECS: readonly EnvSpec[] = [
|
||||
{ path: 'observability.provider_events', type: 'boolean' },
|
||||
{ path: 'worktree_dir', type: 'string' },
|
||||
{ path: 'auto_pr', type: 'boolean' },
|
||||
{ path: 'draft_pr', type: 'boolean' },
|
||||
{ path: 'disabled_builtins', type: 'json' },
|
||||
{ path: 'enable_builtin_pieces', type: 'boolean' },
|
||||
{ path: 'anthropic_api_key', type: 'string' },
|
||||
|
||||
@ -171,6 +171,7 @@ export class GlobalConfigManager {
|
||||
} : undefined,
|
||||
worktreeDir: parsed.worktree_dir,
|
||||
autoPr: parsed.auto_pr,
|
||||
draftPr: parsed.draft_pr,
|
||||
disabledBuiltins: parsed.disabled_builtins,
|
||||
enableBuiltinPieces: parsed.enable_builtin_pieces,
|
||||
anthropicApiKey: parsed.anthropic_api_key,
|
||||
@ -242,6 +243,9 @@ export class GlobalConfigManager {
|
||||
if (config.autoPr !== undefined) {
|
||||
raw.auto_pr = config.autoPr;
|
||||
}
|
||||
if (config.draftPr !== undefined) {
|
||||
raw.draft_pr = config.draftPr;
|
||||
}
|
||||
if (config.disabledBuiltins && config.disabledBuiltins.length > 0) {
|
||||
raw.disabled_builtins = config.disabledBuiltins;
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ export function loadConfig(projectDir: string): LoadedConfig {
|
||||
piece: project.piece ?? 'default',
|
||||
provider,
|
||||
autoPr: project.auto_pr ?? global.autoPr,
|
||||
draftPr: project.draft_pr ?? global.draftPr,
|
||||
model: resolveModel(global, provider),
|
||||
verbose: resolveVerbose(project.verbose, global.verbose),
|
||||
providerOptions: mergeProviderOptions(global.providerOptions, project.providerOptions),
|
||||
|
||||
@ -13,6 +13,8 @@ export interface ProjectLocalConfig {
|
||||
provider?: 'claude' | 'codex' | 'opencode' | 'mock';
|
||||
/** Auto-create PR after worktree execution */
|
||||
auto_pr?: boolean;
|
||||
/** Create PR as draft */
|
||||
draft_pr?: boolean;
|
||||
/** Verbose output mode */
|
||||
verbose?: boolean;
|
||||
/** Provider-specific options (overrides global, overridden by piece/movement) */
|
||||
|
||||
@ -97,7 +97,11 @@ export function createPullRequest(cwd: string, options: CreatePrOptions): Create
|
||||
args.push('--repo', options.repo);
|
||||
}
|
||||
|
||||
log.info('Creating PR', { branch: options.branch, title: options.title });
|
||||
if (options.draft) {
|
||||
args.push('--draft');
|
||||
}
|
||||
|
||||
log.info('Creating PR', { branch: options.branch, title: options.title, draft: options.draft });
|
||||
|
||||
try {
|
||||
const output = execFileSync('gh', args, {
|
||||
|
||||
@ -26,6 +26,8 @@ export interface CreatePrOptions {
|
||||
base?: string;
|
||||
/** Repository in owner/repo format (optional, uses current repo if omitted) */
|
||||
repo?: string;
|
||||
/** Create PR as draft */
|
||||
draft?: boolean;
|
||||
}
|
||||
|
||||
export interface CreatePrResult {
|
||||
|
||||
@ -55,6 +55,7 @@ export function toTaskData(projectDir: string, task: TaskRecord): TaskFileData {
|
||||
start_movement: task.start_movement,
|
||||
retry_note: task.retry_note,
|
||||
auto_pr: task.auto_pr,
|
||||
draft_pr: task.draft_pr,
|
||||
});
|
||||
}
|
||||
|
||||
@ -78,6 +79,7 @@ export function toTaskInfo(projectDir: string, tasksFile: string, task: TaskReco
|
||||
start_movement: task.start_movement,
|
||||
retry_note: task.retry_note,
|
||||
auto_pr: task.auto_pr,
|
||||
draft_pr: task.draft_pr,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ export const TaskExecutionConfigSchema = z.object({
|
||||
start_movement: z.string().optional(),
|
||||
retry_note: z.string().optional(),
|
||||
auto_pr: z.boolean().optional(),
|
||||
draft_pr: z.boolean().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user