Remove worktree prompt from execute action (#414)

* fix: remove execute worktree prompt and deprecate create-worktree option

* test(e2e): align specs with removed --create-worktree

* fix: remove execute worktree leftovers and align docs/tests

---------

Co-authored-by: Takashi Morikubo <azurite0107@gmail.com>
This commit is contained in:
Takashi Morikubo 2026-03-02 16:12:18 +09:00 committed by GitHub
parent 769bd98724
commit 50935a1244
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 101 additions and 344 deletions

View File

@ -67,10 +67,9 @@ TAKT (TAKT Agent Koordination Topology) is a multi-agent orchestration system fo
| `--pr <number>` | PR number to fetch review comments and fix |
| `-w, --piece <name or path>` | Piece name or path to piece YAML file |
| `-b, --branch <name>` | Branch name (auto-generated if omitted) |
| `--auto-pr` | Create PR after execution (interactive: skip confirmation, pipeline: enable PR) |
| `--auto-pr` | Create PR after execution (pipeline mode only) |
| `--skip-git` | Skip branch creation, commit, and push (pipeline mode, piece-only) |
| `--repo <owner/repo>` | Repository for PR creation |
| `--create-worktree <yes\|no>` | Skip worktree confirmation prompt |
| `-q, --quiet` | Minimal output mode: suppress AI output (for CI) |
| `--provider <name>` | Override agent provider (claude\|codex\|opencode\|mock) |
| `--model <name>` | Override agent model |

View File

@ -18,7 +18,6 @@
| `--draft` | PR をドラフトとして作成(`--auto-pr` または `auto_pr` 設定が必要) |
| `--skip-git` | ブランチ作成、コミット、プッシュをスキップpipeline モード、piece のみ実行) |
| `--repo <owner/repo>` | リポジトリを指定PR 作成用) |
| `--create-worktree <yes\|no>` | worktree 確認プロンプトをスキップ |
| `-q, --quiet` | 最小出力モード: AI 出力を抑制CI 向け) |
| `--provider <name>` | エージェント provider を上書きclaude\|codex\|opencode\|cursor\|copilot\|mock |
| `--model <name>` | エージェントモデルを上書き |
@ -44,7 +43,7 @@ takt hello
2. インタラクティブモードを選択assistant / persona / quiet / passthrough
3. AI との会話でタスク内容を精緻化
4. `/go` でタスク指示を確定(`/go 追加の指示` のように追記も可能)、または `/play <task>` でタスクを即座に実行
5. 実行(worktree 作成、piece 実行、PR 作成)
5. 実行piece 実行、PR 作成)
### インタラクティブモードの種類
@ -89,8 +88,6 @@ Requirements:
Proceed with these task instructions? (Y/n) y
? Create worktree? (Y/n) y
[Piece の実行を開始...]
```

View File

@ -18,7 +18,6 @@ This document provides a complete reference for all TAKT CLI commands and option
| `--draft` | Create PR as draft (requires `--auto-pr` or `auto_pr` config) |
| `--skip-git` | Skip branch creation, commit, and push (pipeline mode, piece-only) |
| `--repo <owner/repo>` | Specify repository (for PR creation) |
| `--create-worktree <yes\|no>` | Skip worktree confirmation prompt |
| `-q, --quiet` | Minimal output mode: suppress AI output (for CI) |
| `--provider <name>` | Override agent provider (claude\|codex\|opencode\|cursor\|copilot\|mock) |
| `--model <name>` | Override agent model |
@ -44,7 +43,7 @@ takt hello
2. Select interactive mode (assistant / persona / quiet / passthrough)
3. Refine task content through conversation with AI
4. Finalize task instructions with `/go` (you can also add additional instructions like `/go additional instructions`), or use `/play <task>` to execute a task immediately
5. Execute (create worktree, run piece, create PR)
5. Execute (run piece, create PR)
### Interactive Mode Variants
@ -89,8 +88,6 @@ Requirements:
Proceed with these task instructions? (Y/n) y
? Create worktree? (Y/n) y
[Piece execution starts...]
```

View File

@ -18,7 +18,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
1. **CLI Layer** - ユーザー入力の受付
2. **Interactive Layer** - タスクの対話的な明確化
3. **Execution Orchestration Layer** - ピース選択とworktree管理
3. **Execution Orchestration Layer** - ピース選択と実行開始
4. **Piece Execution Layer** - セッション管理とイベント処理
5. **Engine Layer** - ステートマシンによるステップ実行
6. **Instruction Building Layer** - プロンプト生成
@ -72,13 +72,6 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
│ │ pieceIdentifier: string │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ confirmAndCreateWorktree() │ │
│ │ - AI branchname generation │ │
│ │ - createSharedClone() │ │
│ └─────────┬────────────────────────┘ │
│ │ { execCwd, isWorktree, branch } │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ executeTask() │ │
│ │ - task: string │ │
│ │ - cwd: string (実行ディレクトリ) │ │
@ -362,7 +355,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
### 3. Execution Orchestration Layer (`src/features/tasks/execute/selectAndExecute.ts`)
**役割**: ピース選択とworktree管理
**役割**: ピース選択と実行オーケストレーション
**主要な処理**:
@ -372,26 +365,18 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
- 名前形式 → バリデーション
- オーバーライドなし → インタラクティブ選択 (`selectPiece()`)
2. **Worktree作成** (`confirmAndCreateWorktree()`):
- ユーザー確認 (または `--create-worktree` フラグ)
- ブランチ名生成 (`summarizeTaskName()` - AIでタスクから英語スラグ生成)
- `createSharedClone()`: git clone --shared で軽量クローン作成
3. **タスク実行開始** (`selectAndExecuteTask()`):
2. **タスク実行開始** (`selectAndExecuteTask()`):
- `executeTask()` を呼び出し
- 成功時: Auto-commit & Push
- PR作成 (オプション)
- 成功/失敗を `tasks.yaml` に記録(`skipTaskList` 設定時を除く)
**データ入力**:
- `task: string`
- `options?: SelectAndExecuteOptions`:
- `piece?: string`
- `createWorktree?: boolean`
- `autoPr?: boolean`
- `skipTaskList?: boolean`
- `agentOverrides?: TaskExecutionOptions`
**データ出力**:
- `{ execCwd, isWorktree, branch }`
- タスク実行成功/失敗
---
@ -763,15 +748,9 @@ async call(
- `--piece` フラグ → 検証
- なし → インタラクティブ選択 (`selectPiece()`)
**Worktree作成** (オプション):
- `confirmAndCreateWorktree()`:
- ユーザー確認または `--create-worktree` フラグ
- `summarizeTaskName()`: タスク → 英語スラグ (AI呼び出し)
- `createSharedClone()`: git clone --shared
**データ**:
- `pieceIdentifier: string`
- `{ execCwd, isWorktree, branch }`
- `execCwd: string` (実行ディレクトリ)
---

View File

@ -48,10 +48,10 @@ E2Eテストを追加・変更した場合は、このドキュメントも更
- `README.md` に行が追加されることを確認する。
- 実行後にタスクが `tasks.yaml``completed` ステータスになることを確認する。
- Worktree/Clone isolation`e2e/specs/worktree.e2e.ts`
- 目的: `--create-worktree yes` 指定で隔離環境に実行されることを確認。
- 目的: `worktree: true` タスクが隔離環境に実行されることを確認。
- LLM: 条件付き(`TAKT_E2E_PROVIDER``claude` / `codex` の場合に呼び出す)
- 手順(ユーザー行動/コマンド):
- `takt --task 'Add a line "worktree test" to README.md' --piece e2e/fixtures/pieces/simple.yaml --create-worktree yes` を実行する。
- `.takt/tasks.yaml` に `worktree: true` のタスクを追加して `takt run` を実行する。
- コマンドが成功終了することを確認する。
- Pipeline mode`e2e/specs/pipeline.e2e.ts`
- 目的: ブランチ作成→タスク実行→コミット→push→PR作成の一連フローを確認。
@ -73,7 +73,7 @@ E2Eテストを追加・変更した場合は、このドキュメントも更
- 目的: `--task` の直接実行が、プロンプトなしで完了することを確認。
- LLM: 呼び出さない(`--provider mock` 固定)
- 手順(ユーザー行動/コマンド):
- `takt --task 'Create a file called noop.txt' --piece e2e/fixtures/pieces/mock-single-step.yaml --create-worktree no --provider mock` を実行する。
- `takt --task 'Create a file called noop.txt' --piece e2e/fixtures/pieces/mock-single-step.yaml --provider mock` を実行する。
- `TAKT_MOCK_SCENARIO=e2e/fixtures/scenarios/execute-done.json` を設定する。
- 出力に `Piece completed` が含まれることを確認する。
- Pipeline mode with --skip-git`e2e/specs/pipeline-skip-git.e2e.ts`
@ -87,7 +87,7 @@ E2Eテストを追加・変更した場合は、このドキュメントも更
- 目的: reportフェーズとjudgeフェーズを通ることを確認mockシナリオ
- LLM: 呼び出さない(`--provider mock` 固定)
- 手順(ユーザー行動/コマンド):
- `takt --task 'Create a short report and finish' --piece e2e/fixtures/pieces/report-judge.yaml --create-worktree no --provider mock` を実行する。
- `takt --task 'Create a short report and finish' --piece e2e/fixtures/pieces/report-judge.yaml --provider mock` を実行する。
- `TAKT_MOCK_SCENARIO=e2e/fixtures/scenarios/report-judge.json` を設定する。
- 出力に `Piece completed` が含まれることを確認する。
- Add task`e2e/specs/add.e2e.ts`
@ -135,7 +135,7 @@ E2Eテストを追加・変更した場合は、このドキュメントも更
- LLM: 条件付き(`TAKT_E2E_PROVIDER``claude` / `codex` / `opencode` の場合に実行、未指定時は skip
- 手順(ユーザー行動/コマンド):
- E2E用 `config.yaml``runtime.prepare: [gradle, node]` を設定する。
- `takt --task '<gradle/npm を実行する指示>' --piece e2e/fixtures/pieces/simple.yaml --create-worktree no` を実行する。
- `takt --task '<gradle/npm を実行する指示>' --piece e2e/fixtures/pieces/simple.yaml` を実行する。
- 正例では、作業リポジトリに `.runtime/env.sh``.runtime/{tmp,cache,config,state,gradle,npm}` が作成されていることを確認する。
- 負例(`runtime.prepare` 未設定)では、`GRADLE_USER_HOME is required` と npm キャッシュ書き込み失敗が出力され、`.runtime/env.sh` が生成されないことを確認する。
- List tasks non-interactive`e2e/specs/list-non-interactive.e2e.ts`

View File

@ -77,74 +77,21 @@ describe('E2E: Config priority (piece / autoPr)', () => {
it('should default auto_pr to true when unset in config/env', () => {
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
runTakt({
args: [
'--task', 'Auto PR default behavior',
'--piece', piecePath,
'--create-worktree', 'yes',
'--provider', 'mock',
],
cwd: testRepo.path,
env: {
...isolatedEnv.env,
TAKT_MOCK_SCENARIO: scenarioPath,
},
timeout: 240_000,
});
const task = readFirstTask(testRepo.path);
expect(task['auto_pr']).toBe(true);
}, 240_000);
it('should use auto_pr from config when set', () => {
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
updateIsolatedConfig(isolatedEnv.taktDir, { auto_pr: false });
const result = runTakt({
args: [
'--task', 'Auto PR from config',
'--piece', piecePath,
'--create-worktree', 'yes',
'--provider', 'mock',
'add',
'Auto PR default behavior',
],
cwd: testRepo.path,
env: {
...isolatedEnv.env,
TAKT_MOCK_SCENARIO: scenarioPath,
},
env: isolatedEnv.env,
timeout: 240_000,
});
expect(result.exitCode).toBe(0);
const task = readFirstTask(testRepo.path);
expect(task['auto_pr']).toBe(false);
}, 240_000);
it('should prioritize env auto_pr over config', () => {
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
updateIsolatedConfig(isolatedEnv.taktDir, { auto_pr: false });
runTakt({
args: [
'--task', 'Auto PR from env override',
'--piece', piecePath,
'--create-worktree', 'yes',
'--provider', 'mock',
],
cwd: testRepo.path,
env: {
...isolatedEnv.env,
TAKT_AUTO_PR: 'true',
TAKT_MOCK_SCENARIO: scenarioPath,
},
timeout: 240_000,
});
const task = readFirstTask(testRepo.path);
expect(task['auto_pr']).toBe(true);
}, 240_000);
});

View File

@ -39,7 +39,6 @@ describe('E2E: Cycle detection via loop_monitors (mock)', () => {
args: [
'--task', 'Test cycle detection abort',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -68,7 +67,6 @@ describe('E2E: Cycle detection via loop_monitors (mock)', () => {
args: [
'--task', 'Test cycle detection pass',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,

View File

@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// E2E更新時は docs/testing/e2e.md も更新すること
describe('E2E: Direct task execution (--task --create-worktree no)', () => {
describe('E2E: Direct task execution (--task)', () => {
let isolatedEnv: IsolatedEnv;
let testRepo: TestRepo;
@ -39,7 +39,6 @@ describe('E2E: Direct task execution (--task --create-worktree no)', () => {
args: [
'--task', 'Create a file called noop.txt',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: testRepo.path,

View File

@ -31,7 +31,6 @@ describe('E2E: Error handling edge cases (mock)', () => {
args: [
'--task', 'test',
'--piece', '/nonexistent/path/to/piece.yaml',
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -53,7 +52,6 @@ describe('E2E: Error handling edge cases (mock)', () => {
args: [
'--task', 'test',
'--piece', 'nonexistent-piece-name-xyz',
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -90,8 +88,8 @@ describe('E2E: Error handling edge cases (mock)', () => {
expect(combined).toMatch(/task|issue|required/i);
}, 240_000);
it('should error when --create-worktree receives an invalid value', () => {
// Given: invalid worktree value
it('should error when deprecated --create-worktree option is used', () => {
// Given: deprecated option value
const piecePath = resolve(__dirname, '../fixtures/pieces/mock-single-step.yaml');
// When: running with invalid worktree option
@ -107,10 +105,10 @@ describe('E2E: Error handling edge cases (mock)', () => {
timeout: 240_000,
});
// Then: exits with error or warning about invalid value
// Then: exits with migration error
const combined = result.stdout + result.stderr;
const hasError = result.exitCode !== 0 || combined.match(/invalid|error|must be/i);
expect(hasError).toBeTruthy();
expect(result.exitCode).not.toBe(0);
expect(combined).toContain('--create-worktree has been removed');
}, 240_000);
it('should error when piece file contains invalid YAML', () => {
@ -122,7 +120,6 @@ describe('E2E: Error handling edge cases (mock)', () => {
args: [
'--task', 'test',
'--piece', brokenPiecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,

View File

@ -31,7 +31,6 @@ describe('E2E: --model option override (mock)', () => {
args: [
'--task', 'Test model override direct',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
'--model', 'mock-model-override',
],

View File

@ -40,7 +40,6 @@ describe('E2E: Multi-step with parallel movements (mock)', () => {
args: [
'--task', 'Implement a feature',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: testRepo.path,
@ -62,7 +61,6 @@ describe('E2E: Multi-step with parallel movements (mock)', () => {
args: [
'--task', 'Implement a feature with issues',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: testRepo.path,

View File

@ -32,7 +32,6 @@ describe('E2E: Sequential multi-step session log transitions (mock)', () => {
args: [
'--task', 'Test sequential transitions',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,

View File

@ -33,7 +33,6 @@ describe('E2E: Piece error handling (mock)', () => {
args: [
'--task', 'Test error status abort',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -60,7 +59,6 @@ describe('E2E: Piece error handling (mock)', () => {
args: [
'--task', 'Test max movements',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -87,7 +85,6 @@ describe('E2E: Piece error handling (mock)', () => {
args: [
'--task', 'Test previous response passing',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,

View File

@ -54,7 +54,7 @@ function runTaskWithPiece(args: {
env: NodeJS.ProcessEnv;
}): ReturnType<typeof runTakt> {
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
const baseArgs = ['--task', 'Create a file called noop.txt', '--create-worktree', 'no', '--provider', 'mock'];
const baseArgs = ['--task', 'Create a file called noop.txt', '--provider', 'mock'];
const fullArgs = args.piece ? [...baseArgs, '--piece', args.piece] : baseArgs;
return runTakt({
args: fullArgs,

View File

@ -41,7 +41,6 @@ describe('E2E: Provider error handling (mock)', () => {
args: [
'--task', 'Test provider override',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -67,7 +66,6 @@ describe('E2E: Provider error handling (mock)', () => {
args: [
'--task', 'Test scenario exhaustion',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -93,7 +91,6 @@ describe('E2E: Provider error handling (mock)', () => {
args: [
'--task', 'Test bad scenario',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,

View File

@ -33,7 +33,6 @@ describe('E2E: Quiet mode (--quiet)', () => {
args: [
'--task', 'Test quiet mode',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
'--quiet',
],

View File

@ -39,7 +39,6 @@ describe('E2E: Report file output (mock)', () => {
args: [
'--task', 'Test report output',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,

View File

@ -39,7 +39,6 @@ describe('E2E: Report + Judge phases (mock)', () => {
args: [
'--task', 'Create a short report and finish',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: testRepo.path,

View File

@ -113,7 +113,6 @@ describe('E2E: runtime.prepare with provider', () => {
'If both commands succeed, respond exactly with: Task completed',
].join(' '),
'--piece', piecePath,
'--create-worktree', 'no',
],
cwd: repo.path,
env: isolatedEnv.env,
@ -151,7 +150,6 @@ describe('E2E: runtime.prepare with provider', () => {
'If both commands succeed, respond exactly with: Task completed',
].join(' '),
'--piece', piecePath,
'--create-worktree', 'no',
],
cwd: repo.path,
env: isolatedEnv.env,

View File

@ -32,7 +32,6 @@ describe('E2E: Session NDJSON log output (mock)', () => {
args: [
'--task', 'Test session log success',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,
@ -59,7 +58,6 @@ describe('E2E: Session NDJSON log output (mock)', () => {
args: [
'--task', 'Test session log abort',
'--piece', piecePath,
'--create-worktree', 'no',
'--provider', 'mock',
],
cwd: repo.path,

View File

@ -52,7 +52,6 @@ describe('E2E: Structured output rule matching', () => {
args: [
'--task', 'Say hello',
'--piece', piecePath,
'--create-worktree', 'no',
],
cwd: repo.path,
env: isolatedEnv.env,

View File

@ -39,8 +39,6 @@ describe('E2E: Team leader refill threshold', () => {
'Create exactly seven files: rt-1.txt, rt-2.txt, rt-3.txt, rt-4.txt, rt-5.txt, rt-6.txt, rt-7.txt. Each file must contain its own filename as content. Each part must create exactly one file.',
'--piece',
piecePath,
'--create-worktree',
'no',
],
cwd: repo.path,
env: {

View File

@ -38,8 +38,6 @@ describe('E2E: Team leader worker-pool dynamic scheduling', () => {
'Create exactly five files: wp-1.txt, wp-2.txt, wp-3.txt, wp-4.txt, wp-5.txt. Each file must contain its own filename as content. Each part must create exactly one file, and you must complete all five files.',
'--piece',
piecePath,
'--create-worktree',
'no',
],
cwd: repo.path,
env: isolatedEnv.env,

View File

@ -45,7 +45,6 @@ describe('E2E: Team leader movement', () => {
args: [
'--task', 'Create two files: hello-en.txt containing "Hello World" and hello-ja.txt containing "こんにちは世界"',
'--piece', piecePath,
'--create-worktree', 'no',
],
cwd: repo.path,
env: isolatedEnv.env,

View File

@ -9,7 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// E2E更新時は docs/testing/e2e.md も更新すること
describe('E2E: Worktree/Clone isolation (--create-worktree yes)', () => {
describe('E2E: Removed --create-worktree option', () => {
let isolatedEnv: IsolatedEnv;
let testRepo: TestRepo;
@ -31,7 +31,7 @@ describe('E2E: Worktree/Clone isolation (--create-worktree yes)', () => {
}
});
it('should execute task in an isolated worktree/clone', () => {
it('should fail fast with migration guidance', () => {
const piecePath = resolve(__dirname, '../fixtures/pieces/simple.yaml');
const result = runTakt({
@ -45,7 +45,8 @@ describe('E2E: Worktree/Clone isolation (--create-worktree yes)', () => {
timeout: 240_000,
});
// Task should succeed
expect(result.exitCode).toBe(0);
expect(result.exitCode).not.toBe(0);
const combined = result.stdout + result.stderr;
expect(combined).toContain('--create-worktree has been removed');
}, 240_000);
});

View File

@ -110,7 +110,6 @@ vi.mock('../app/cli/program.js', () => {
vi.mock('../app/cli/helpers.js', () => ({
resolveAgentOverrides: vi.fn(),
parseCreateWorktreeOption: vi.fn(),
isDirectTask: vi.fn(() => false),
}));
@ -120,7 +119,7 @@ import { interactiveMode } from '../features/interactive/index.js';
import { resolveConfigValues, loadPersonaSessions } from '../infra/config/index.js';
import { isDirectTask } from '../app/cli/helpers.js';
import { executeDefaultAction } from '../app/cli/routing.js';
import { info } from '../shared/ui/index.js';
import { info, error } from '../shared/ui/index.js';
import type { Issue } from '../infra/git/index.js';
const mockFormatIssueAsTask = vi.mocked(formatIssueAsTask);
@ -133,6 +132,7 @@ const mockLoadPersonaSessions = vi.mocked(loadPersonaSessions);
const mockResolveConfigValues = vi.mocked(resolveConfigValues);
const mockIsDirectTask = vi.mocked(isDirectTask);
const mockInfo = vi.mocked(info);
const mockError = vi.mocked(error);
const mockTaskRunnerListAllTaskItems = vi.mocked(mockListAllTaskItems);
function createMockIssue(number: number): Issue {
@ -161,6 +161,43 @@ beforeEach(() => {
});
describe('Issue resolution in routing', () => {
it('should show error and exit when --auto-pr/--draft are used outside pipeline mode', async () => {
mockOpts.autoPr = true;
mockOpts.draft = true;
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
await expect(executeDefaultAction()).rejects.toThrow('process.exit called');
expect(mockError).toHaveBeenCalledWith('--auto-pr/--draft are supported only in --pipeline mode');
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockInteractiveMode).not.toHaveBeenCalled();
expect(mockSelectAndExecuteTask).not.toHaveBeenCalled();
mockExit.mockRestore();
});
it('should show migration error and exit when deprecated --create-worktree is used', async () => {
mockOpts.createWorktree = 'yes';
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
await expect(executeDefaultAction()).rejects.toThrow('process.exit called');
expect(mockError).toHaveBeenCalledWith(
'--create-worktree has been removed. execute now always runs in-place. Use "takt add" (save_task) + "takt run" for worktree-based execution.'
);
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockInteractiveMode).not.toHaveBeenCalled();
expect(mockSelectAndExecuteTask).not.toHaveBeenCalled();
mockExit.mockRestore();
});
describe('--issue option', () => {
it('should resolve issue and pass to interactive mode when --issue is specified', async () => {
// Given

View File

@ -112,7 +112,6 @@ vi.mock('../app/cli/program.js', () => {
vi.mock('../app/cli/helpers.js', () => ({
resolveAgentOverrides: vi.fn(),
parseCreateWorktreeOption: vi.fn(),
isDirectTask: vi.fn(() => false),
}));

View File

@ -1,5 +1,5 @@
/**
* Tests for resolveAutoPr default behavior in selectAndExecuteTask
* Tests for selectAndExecuteTask behavior in execute path
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
@ -25,10 +25,6 @@ const {
mockResolvePieceConfigValue: vi.fn((_: string, key: string) => (key === 'autoPr' ? undefined : 'default')),
}));
vi.mock('../shared/prompt/index.js', () => ({
confirm: vi.fn(),
}));
vi.mock('../infra/config/index.js', () => ({
resolvePieceConfigValue: (...args: unknown[]) => mockResolvePieceConfigValue(...args),
listPieces: vi.fn(() => ['default']),
@ -86,17 +82,13 @@ vi.mock('../features/pieceSelection/index.js', () => ({
selectPiece: vi.fn(),
}));
import { confirm } from '../shared/prompt/index.js';
import { loadPieceByIdentifier } from '../infra/config/index.js';
import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../infra/task/index.js';
import { autoCommitAndPush } from '../infra/task/index.js';
import { selectPiece } from '../features/pieceSelection/index.js';
import { selectAndExecuteTask, determinePiece } from '../features/tasks/execute/selectAndExecute.js';
const mockConfirm = vi.mocked(confirm);
const mockLoadPieceByIdentifier = vi.mocked(loadPieceByIdentifier);
const mockCreateSharedClone = vi.mocked(createSharedClone);
const mockAutoCommitAndPush = vi.mocked(autoCommitAndPush);
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
const mockSelectPiece = vi.mocked(selectPiece);
beforeEach(() => {
@ -104,74 +96,14 @@ beforeEach(() => {
mockExecuteTask.mockResolvedValue(true);
});
describe('resolveAutoPr default in selectAndExecuteTask', () => {
it('should call auto-PR confirm with default true when no CLI option or config', async () => {
// Given: worktree is enabled via override, no autoPr option, no config autoPr
mockConfirm.mockResolvedValue(true);
mockSummarizeTaskName.mockResolvedValue('test-task');
mockCreateSharedClone.mockReturnValue({
path: '/project/../clone',
branch: 'takt/test-task',
});
mockAutoCommitAndPush.mockReturnValue({
success: false,
message: 'no changes',
});
// When
describe('selectAndExecuteTask (execute path)', () => {
it('should execute in-place without worktree setup or PR prompts', async () => {
await selectAndExecuteTask('/project', 'test task', {
piece: 'default',
createWorktree: true,
});
const autoPrCall = mockConfirm.mock.calls.find((call) => call[0] === 'Create pull request?');
expect(autoPrCall).toBeDefined();
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();
expect(mockAutoCommitAndPush).not.toHaveBeenCalled();
expect(mockAddTask).toHaveBeenCalledWith('test task', { piece: 'default' });
});
it('should call selectPiece when no override is provided', async () => {
@ -192,17 +124,10 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => {
});
it('should fail task record when executeTask throws', async () => {
mockConfirm.mockResolvedValue(true);
mockSummarizeTaskName.mockResolvedValue('test-task');
mockCreateSharedClone.mockReturnValue({
path: '/project/../clone',
branch: 'takt/test-task',
});
mockExecuteTask.mockRejectedValue(new Error('boom'));
await expect(selectAndExecuteTask('/project', 'test task', {
piece: 'default',
createWorktree: true,
})).rejects.toThrow('boom');
expect(mockAddTask).toHaveBeenCalledTimes(1);
@ -211,38 +136,18 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => {
});
it('should record task and complete when executeTask returns true', async () => {
mockConfirm.mockResolvedValue(true);
mockSummarizeTaskName.mockResolvedValue('test-task');
mockCreateSharedClone.mockReturnValue({
path: '/project/../clone',
branch: 'takt/test-task',
});
mockExecuteTask.mockResolvedValue(true);
await selectAndExecuteTask('/project', 'test task', {
piece: 'default',
createWorktree: true,
});
expect(mockAddTask).toHaveBeenCalledWith('test task', expect.objectContaining({
piece: 'default',
worktree: true,
branch: 'takt/test-task',
worktree_path: '/project/../clone',
auto_pr: true,
draft_pr: true,
}));
expect(mockAddTask).toHaveBeenCalledWith('test task', { piece: 'default' });
expect(mockCompleteTask).toHaveBeenCalledTimes(1);
expect(mockFailTask).not.toHaveBeenCalled();
});
it('should record task and fail when executeTask returns false', async () => {
mockConfirm.mockResolvedValue(false);
mockSummarizeTaskName.mockResolvedValue('test-task');
mockCreateSharedClone.mockReturnValue({
path: '/project/../clone',
branch: 'takt/test-task',
});
mockExecuteTask.mockResolvedValue(false);
const processExitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {
@ -251,16 +156,9 @@ describe('resolveAutoPr default in selectAndExecuteTask', () => {
await expect(selectAndExecuteTask('/project', 'test task', {
piece: 'default',
createWorktree: true,
})).rejects.toThrow('process exit');
expect(mockAddTask).toHaveBeenCalledWith('test task', expect.objectContaining({
piece: 'default',
worktree: true,
branch: 'takt/test-task',
worktree_path: '/project/../clone',
auto_pr: false,
}));
expect(mockAddTask).toHaveBeenCalledWith('test task', { piece: 'default' });
expect(mockFailTask).toHaveBeenCalledTimes(1);
expect(mockCompleteTask).not.toHaveBeenCalled();
processExitSpy.mockRestore();

View File

@ -26,7 +26,6 @@ const {
}));
vi.mock('../shared/prompt/index.js', () => ({
confirm: vi.fn(),
}));
vi.mock('../infra/config/index.js', () => ({
@ -85,16 +84,11 @@ vi.mock('../features/pieceSelection/index.js', () => ({
selectPiece: vi.fn(),
}));
import { confirm } from '../shared/prompt/index.js';
import { selectAndExecuteTask } from '../features/tasks/execute/selectAndExecute.js';
const mockConfirm = vi.mocked(confirm);
beforeEach(() => {
vi.clearAllMocks();
mockExecuteTask.mockResolvedValue(true);
// worktree を使わないconfirm で false
mockConfirm.mockResolvedValue(false);
});
describe('skipTaskList option in selectAndExecuteTask', () => {

View File

@ -7,7 +7,6 @@
import type { Command } from 'commander';
import type { TaskExecutionOptions } from '../../features/tasks/index.js';
import type { ProviderType } from '../../infra/providers/index.js';
import { error } from '../../shared/ui/index.js';
import { isIssueReference } from '../../infra/github/index.js';
/**
@ -26,28 +25,6 @@ export function resolveAgentOverrides(program: Command): TaskExecutionOptions |
return { provider, model };
}
/**
* Parse --create-worktree option value (yes/no/true/false).
* Returns undefined if not specified, boolean otherwise.
* Exits with error on invalid value.
*/
export function parseCreateWorktreeOption(value?: string): boolean | undefined {
if (!value) {
return undefined;
}
const normalized = value.toLowerCase();
if (normalized === 'yes' || normalized === 'true') {
return true;
}
if (normalized === 'no' || normalized === 'false') {
return false;
}
error('Invalid value for --create-worktree. Use yes or no.');
process.exit(1);
}
/**
* Check if the input is a task description that should execute directly
* vs one that should enter interactive mode.

View File

@ -6,7 +6,7 @@
*/
import { createRequire } from 'node:module';
import { Command } from 'commander';
import { Command, Option } from 'commander';
import { resolve } from 'node:path';
import {
initGlobalDirs,
@ -52,7 +52,8 @@ program
.option('-t, --task <string>', 'Task content (as alternative to GitHub issue)')
.option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation')
.option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)')
.option('--create-worktree <yes|no>', 'Skip the worktree prompt by explicitly specifying yes or no')
// Deprecated compatibility option: keep parsing to show migration guidance.
.addOption(new Option('--create-worktree <yes|no>').hideHelp())
.option('-q, --quiet', 'Minimal output mode: suppress AI output (for CI)')
.option('-c, --continue', 'Continue from the last assistant session');

View File

@ -25,7 +25,7 @@ import {
} from '../../features/interactive/index.js';
import { getPieceDescription, resolveConfigValue, resolveConfigValues, loadPersonaSessions } from '../../infra/config/index.js';
import { program, resolvedCwd, pipelineMode } from './program.js';
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
import { resolveAgentOverrides, isDirectTask } from './helpers.js';
import { loadTaskHistory } from './taskHistory.js';
/**
@ -113,6 +113,18 @@ async function resolvePrInput(
*/
export async function executeDefaultAction(task?: string): Promise<void> {
const opts = program.opts();
if (opts.createWorktree !== undefined) {
logError(
'--create-worktree has been removed. ' +
'execute now always runs in-place. ' +
'Use "takt add" (save_task) + "takt run" for worktree-based execution.'
);
process.exit(1);
}
if (!pipelineMode && (opts.autoPr === true || opts.draft === true)) {
logError('--auto-pr/--draft are supported only in --pipeline mode');
process.exit(1);
}
const prNumber = opts.pr as number | undefined;
const issueNumber = opts.issue as number | undefined;
@ -125,9 +137,7 @@ export async function executeDefaultAction(task?: string): Promise<void> {
logError('--pr and --task cannot be used together');
process.exit(1);
}
const agentOverrides = resolveAgentOverrides(program);
const createWorktreeOverride = parseCreateWorktreeOption(opts.createWorktree as string | undefined);
const resolvedPipelinePiece = (opts.piece as string | undefined) ?? resolveConfigValue(resolvedCwd, 'piece');
const resolvedPipelineAutoPr = opts.autoPr === true
? true
@ -136,11 +146,8 @@ export async function executeDefaultAction(task?: string): Promise<void> {
? 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,
};
// --- Pipeline mode (non-interactive): triggered by --pipeline ---
@ -158,7 +165,6 @@ export async function executeDefaultAction(task?: string): Promise<void> {
cwd: resolvedCwd,
provider: agentOverrides?.provider,
model: agentOverrides?.model,
createWorktree: createWorktreeOverride,
});
if (exitCode !== 0) {

View File

@ -1,7 +1,7 @@
/**
* Task execution orchestration.
*
* Coordinates piece selection, worktree creation, task execution,
* Coordinates piece selection and task execution,
* auto-commit, and PR creation. Extracted from cli.ts to avoid
* mixing CLI parsing with business logic.
*/
@ -15,7 +15,6 @@ import { createSharedClone, summarizeTaskName, resolveBaseBranch, TaskRunner } f
import { info, error, withProgress } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/index.js';
import { executeTask } from './taskExecution.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';
@ -76,7 +75,7 @@ export async function confirmAndCreateWorktree(
}
/**
* Execute a task with piece selection, optional worktree, and auto-commit.
* Execute a task with piece selection.
* Shared by direct task execution and interactive mode.
*/
export async function selectAndExecuteTask(
@ -92,35 +91,14 @@ export async function selectAndExecuteTask(
return;
}
const { execCwd, isWorktree, branch, baseBranch, taskSlug } = await confirmAndCreateWorktree(
cwd,
task,
options?.createWorktree,
options?.branch,
);
// 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, draftPr: shouldDraftPr });
// execute action always runs in-place (no worktree prompt/creation).
const execCwd = cwd;
log.info('Starting task execution', { piece: pieceIdentifier, worktree: false });
const taskRunner = new TaskRunner(cwd);
let taskRecord: Awaited<ReturnType<TaskRunner['addTask']>> | null = null;
if (options?.skipTaskList !== true || isWorktree) {
if (options?.skipTaskList !== true) {
taskRecord = taskRunner.addTask(task, {
piece: pieceIdentifier,
...(isWorktree ? { worktree: true } : {}),
...(branch ? { branch } : {}),
...(isWorktree ? { worktree_path: execCwd } : {}),
auto_pr: shouldCreatePr,
draft_pr: shouldDraftPr,
...(taskSlug ? { slug: taskSlug } : {}),
});
}
const startedAt = new Date().toISOString();
@ -148,36 +126,15 @@ export async function selectAndExecuteTask(
const completedAt = new Date().toISOString();
let prFailed = false;
let prError: string | undefined;
if (taskSuccess && isWorktree) {
const postResult = await postExecutionFlow({
execCwd,
projectCwd: cwd,
task,
branch,
baseBranch,
shouldCreatePr,
draftPr: shouldDraftPr,
pieceIdentifier,
issues: options?.issues,
repo: options?.repo,
});
prFailed = postResult.prFailed ?? false;
prError = postResult.prError;
}
const effectiveSuccess = taskSuccess && !prFailed;
const effectiveSuccess = taskSuccess;
if (taskRecord) {
const taskResult = buildBooleanTaskResult({
task: taskRecord,
taskSuccess: effectiveSuccess,
successResponse: 'Task completed successfully',
failureResponse: prFailed ? `PR creation failed: ${prError}` : 'Task failed',
failureResponse: 'Task failed',
startedAt,
completedAt,
branch,
...(isWorktree ? { worktreePath: execCwd } : {}),
});
persistTaskResult(taskRunner, taskResult);
}

View File

@ -136,11 +136,8 @@ export interface WorktreeConfirmationResult {
}
export interface SelectAndExecuteOptions {
autoPr?: boolean;
draftPr?: boolean;
repo?: string;
piece?: string;
createWorktree?: boolean | undefined;
/** Override branch name (e.g., PR head branch for --pr) */
branch?: string;
/** Enable interactive user input during step transitions */