takt: add-draft-pr-option (#323)

This commit is contained in:
nrs 2026-02-20 00:35:41 +09:00 committed by GitHub
parent 5960a0d212
commit 4f8255d509
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 323 additions and 25 deletions

View File

@ -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';

View File

@ -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 = {

View File

@ -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[]) => {

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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 () => {

View File

@ -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();

View File

@ -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')

View File

@ -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,

View File

@ -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 */

View File

@ -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 */

View File

@ -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) {

View File

@ -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 };
}
/**

View File

@ -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}`);

View File

@ -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 } : {}),

View File

@ -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,

View File

@ -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,
});

View File

@ -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;

View File

@ -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' },

View File

@ -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;
}

View File

@ -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),

View File

@ -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) */

View File

@ -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, {

View File

@ -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 {

View File

@ -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,
}),
};
}

View File

@ -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(),
});
/**