fix: ピース再利用確認を task.data.piece から取得 & config テンプレート拡充

- retryFailedTask / instructBranch でピース名の取得元を
  runInfo?.piece から task.data?.piece に変更
  (worktree 内に .takt/runs/ が存在しないため runInfo は常に null だった)
- ~/.takt/config.yaml テンプレートに不足していた設定項目を追加
  (provider, model, concurrency, analytics, pipeline, persona_providers 等)
This commit is contained in:
nrslib 2026-03-06 00:37:54 +09:00
parent a69e9f4fb3
commit ebbd1a67a9
6 changed files with 177 additions and 65 deletions

View File

@ -2,17 +2,36 @@
# Location: ~/.takt/config.yaml # Location: ~/.takt/config.yaml
# ===================================== # =====================================
# General settings (piece-independent) # General settings
# ===================================== # =====================================
# Note: this template contains global-only settings for ~/.takt/config.yaml.
language: en # UI language: en | ja language: en # UI language: en | ja
# Default provider and model
# provider: claude # Default provider: claude | codex | opencode | cursor | copilot | mock
# model: sonnet # Default model (passed directly to provider)
# Execution control # Execution control
# worktree_dir: ~/takt-worktrees # Base directory for shared clone execution # worktree_dir: ~/takt-worktrees # Base directory for shared clone execution
# prevent_sleep: false # Prevent macOS idle sleep while running # prevent_sleep: false # Prevent macOS idle sleep while running
# auto_fetch: false # Fetch before clone to keep shared clones up-to-date # auto_fetch: false # Fetch before clone to keep shared clones up-to-date
# base_branch: main # Base branch to clone from (default: current branch)
# concurrency: 1 # Number of tasks to run concurrently in takt run (1-10)
# task_poll_interval_ms: 500 # Polling interval in ms for picking up new tasks (100-5000)
# PR / branch
# auto_pr: false # Auto-create PR after worktree execution
# draft_pr: false # Create PR as draft
# branch_name_strategy: romaji # Branch name generation: romaji | ai
# Pipeline execution
# pipeline:
# default_branch_prefix: "takt/" # Branch prefix for pipeline-created branches
# commit_message_template: "{title}" # Commit message template. Variables: {title}, {issue}
# pr_body_template: "{report}" # PR body template. Variables: {issue_body}, {report}, {issue}
# Output / notifications # Output / notifications
# verbose: false # Shortcut: enable trace/debug and set logging.level=debug
# minimal_output: false # Suppress detailed agent output
# notification_sound: true # Master switch for sounds # notification_sound: true # Master switch for sounds
# notification_sound_events: # Per-event sound toggle (unset means true) # notification_sound_events: # Per-event sound toggle (unset means true)
# iteration_limit: true # iteration_limit: true
@ -20,12 +39,63 @@ language: en # UI language: en | ja
# piece_abort: true # piece_abort: true
# run_complete: true # run_complete: true
# run_abort: true # run_abort: true
# verbose: false # Shortcut: enable trace/debug and set logging.level=debug
# logging: # logging:
# level: info # Log level for console and file output # level: info # Log level for console and file output
# trace: true # Generate human-readable execution trace report (trace.md) # trace: true # Generate human-readable execution trace report (trace.md)
# debug: false # Enable debug.log + prompts.jsonl # debug: false # Enable debug.log + prompts.jsonl
# provider_events: false # Persist provider stream events # provider_events: false # Persist provider stream events
# usage_events: false # Persist usage event logs
# Analytics
# analytics:
# enabled: true # Enable local analytics collection
# events_path: ~/.takt/analytics/events # Custom events directory
# retention_days: 30 # Retention period for event files
# Interactive mode
# interactive_preview_movements: 3 # Number of movement previews in interactive mode (0-10)
# Per-persona provider/model overrides
# persona_providers:
# coder:
# provider: claude
# model: opus
# reviewer:
# provider: codex
# model: gpt-5.2-codex
# Provider-specific options (lowest priority, overridden by piece/movement)
# provider_options:
# codex:
# network_access: true
# claude:
# sandbox:
# allow_unsandboxed_commands: true
# Provider permission profiles
# provider_profiles:
# claude:
# default_permission_mode: edit
# codex:
# default_permission_mode: edit
# Runtime environment preparation
# runtime:
# prepare: [node, gradle, ./custom-script.sh]
# Piece-level overrides
# piece_overrides:
# quality_gates:
# - "All tests pass"
# quality_gates_edit_only: true
# movements:
# review:
# quality_gates:
# - "No security vulnerabilities"
# personas:
# coder:
# quality_gates:
# - "Code follows conventions"
# Credentials (environment variables take priority) # Credentials (environment variables take priority)
# anthropic_api_key: "sk-ant-..." # Claude API key # anthropic_api_key: "sk-ant-..." # Claude API key
@ -35,7 +105,14 @@ language: en # UI language: en | ja
# groq_api_key: "..." # Groq API key # groq_api_key: "..." # Groq API key
# openrouter_api_key: "..." # OpenRouter API key # openrouter_api_key: "..." # OpenRouter API key
# opencode_api_key: "..." # OpenCode API key # opencode_api_key: "..." # OpenCode API key
# codex_cli_path: "/absolute/path/to/codex" # Absolute path to Codex CLI # cursor_api_key: "..." # Cursor API key
# CLI paths
# codex_cli_path: "/absolute/path/to/codex" # Absolute path to Codex CLI
# claude_cli_path: "/absolute/path/to/claude" # Absolute path to Claude Code CLI
# cursor_cli_path: "/absolute/path/to/cursor" # Absolute path to cursor-agent CLI
# copilot_cli_path: "/absolute/path/to/copilot" # Absolute path to Copilot CLI
# copilot_github_token: "ghp_..." # Copilot GitHub token
# Misc # Misc
# bookmarks_file: ~/.takt/preferences/bookmarks.yaml # Bookmark file location # bookmarks_file: ~/.takt/preferences/bookmarks.yaml # Bookmark file location

View File

@ -2,17 +2,36 @@
# 配置場所: ~/.takt/config.yaml # 配置場所: ~/.takt/config.yaml
# ===================================== # =====================================
# 通常設定(ピース非依存) # 通常設定
# ===================================== # =====================================
# 注意: このテンプレートは global 専用設定(~/.takt/config.yamlだけを扱う
language: ja # 表示言語: ja | en language: ja # 表示言語: ja | en
# デフォルトプロバイダー・モデル
# provider: claude # デフォルトプロバイダー: claude | codex | opencode | cursor | copilot | mock
# model: sonnet # デフォルトモデル(プロバイダーにそのまま渡される)
# 実行制御 # 実行制御
# worktree_dir: ~/takt-worktrees # 共有clone作成先ディレクトリ # worktree_dir: ~/takt-worktrees # 共有clone作成先ディレクトリ
# prevent_sleep: false # macOS実行中のスリープ防止caffeinate # prevent_sleep: false # macOS実行中のスリープ防止caffeinate
# auto_fetch: false # clone前にfetchして最新化するか # auto_fetch: false # clone前にfetchして最新化するか
# base_branch: main # cloneのベースブランチデフォルト: カレントブランチ)
# concurrency: 1 # takt run の同時実行タスク数 (1-10)
# task_poll_interval_ms: 500 # 新規タスク検出のポーリング間隔ms, 100-5000
# PR / ブランチ
# auto_pr: false # worktree実行後にPR自動作成
# draft_pr: false # ドラフトPRとして作成
# branch_name_strategy: romaji # ブランチ名生成: romaji | ai
# パイプライン実行
# pipeline:
# default_branch_prefix: "takt/" # パイプラインで作成するブランチのプレフィックス
# commit_message_template: "{title}" # コミットメッセージテンプレート。変数: {title}, {issue}
# pr_body_template: "{report}" # PR本文テンプレート。変数: {issue_body}, {report}, {issue}
# 出力・通知 # 出力・通知
# verbose: false # ショートカット: trace/debug有効化 + logging.level=debug
# minimal_output: false # エージェント詳細出力を抑制
# notification_sound: true # 通知音全体のON/OFF # notification_sound: true # 通知音全体のON/OFF
# notification_sound_events: # イベント別通知音未指定はtrue扱い # notification_sound_events: # イベント別通知音未指定はtrue扱い
# iteration_limit: true # iteration_limit: true
@ -20,12 +39,63 @@ language: ja # 表示言語: ja | en
# piece_abort: true # piece_abort: true
# run_complete: true # run_complete: true
# run_abort: true # run_abort: true
# verbose: false # ショートカット: trace/debug有効化 + logging.level=debug
# logging: # logging:
# level: info # ログレベル: debug | info | warn | error # level: info # ログレベル: debug | info | warn | error
# trace: true # trace.md 実行レポート生成 # trace: true # trace.md 実行レポート生成
# debug: false # debug.log + prompts.jsonl を有効化 # debug: false # debug.log + prompts.jsonl を有効化
# provider_events: false # providerイベントログを記録 # provider_events: false # providerイベントログを記録
# usage_events: false # 使用量イベントログを記録
# アナリティクス
# analytics:
# enabled: true # ローカルアナリティクス収集を有効化
# events_path: ~/.takt/analytics/events # イベントディレクトリのカスタムパス
# retention_days: 30 # イベントファイルの保持期間(日)
# インタラクティブモード
# interactive_preview_movements: 3 # インタラクティブモードでのムーブメントプレビュー数 (0-10)
# ペルソナ別プロバイダー・モデル指定
# persona_providers:
# coder:
# provider: claude
# model: opus
# reviewer:
# provider: codex
# model: gpt-5.2-codex
# プロバイダー固有オプション(最低優先度、ピース/ムーブメントで上書き可能)
# provider_options:
# codex:
# network_access: true
# claude:
# sandbox:
# allow_unsandboxed_commands: true
# プロバイダー権限プロファイル
# provider_profiles:
# claude:
# default_permission_mode: edit
# codex:
# default_permission_mode: edit
# ランタイム環境の準備
# runtime:
# prepare: [node, gradle, ./custom-script.sh]
# ピースレベルのオーバーライド
# piece_overrides:
# quality_gates:
# - "All tests pass"
# quality_gates_edit_only: true
# movements:
# review:
# quality_gates:
# - "No security vulnerabilities"
# personas:
# coder:
# quality_gates:
# - "Code follows conventions"
# 認証情報(環境変数優先) # 認証情報(環境変数優先)
# anthropic_api_key: "sk-ant-..." # Claude APIキー # anthropic_api_key: "sk-ant-..." # Claude APIキー
@ -35,7 +105,14 @@ language: ja # 表示言語: ja | en
# groq_api_key: "..." # Groq APIキー # groq_api_key: "..." # Groq APIキー
# openrouter_api_key: "..." # OpenRouter APIキー # openrouter_api_key: "..." # OpenRouter APIキー
# opencode_api_key: "..." # OpenCode APIキー # opencode_api_key: "..." # OpenCode APIキー
# codex_cli_path: "/absolute/path/to/codex" # Codex CLI絶対パス # cursor_api_key: "..." # Cursor APIキー
# CLIパス
# codex_cli_path: "/absolute/path/to/codex" # Codex CLI絶対パス
# claude_cli_path: "/absolute/path/to/claude" # Claude Code CLI絶対パス
# cursor_cli_path: "/absolute/path/to/cursor" # cursor-agent CLI絶対パス
# copilot_cli_path: "/absolute/path/to/copilot" # Copilot CLI絶対パス
# copilot_github_token: "ghp_..." # Copilot GitHubトークン
# その他 # その他
# bookmarks_file: ~/.takt/preferences/bookmarks.yaml # ブックマーク保存先 # bookmarks_file: ~/.takt/preferences/bookmarks.yaml # ブックマーク保存先

View File

@ -210,15 +210,7 @@ describe('instructBranch direct execution flow', () => {
expect(originalTaskInfo.data.piece).toBe('original-piece'); expect(originalTaskInfo.data.piece).toBe('original-piece');
}); });
it('should reuse previous piece when confirmed', async () => { it('should reuse previous piece from task data when confirmed', async () => {
mockFindRunForTask.mockReturnValue('run-previous');
mockLoadRunSessionContext.mockReturnValue({
task: 'done',
piece: 'default',
status: 'completed',
movementLogs: [],
reports: [],
});
mockConfirm mockConfirm
.mockResolvedValueOnce(true); .mockResolvedValueOnce(true);
@ -230,7 +222,7 @@ describe('instructBranch direct execution flow', () => {
content: 'done', content: 'done',
branch: 'takt/done-task', branch: 'takt/done-task',
worktreePath: '/project/.takt/worktrees/done-task', worktreePath: '/project/.takt/worktrees/done-task',
data: { task: 'done' }, data: { task: 'done', piece: 'default' },
}); });
expect(mockSelectPiece).not.toHaveBeenCalled(); expect(mockSelectPiece).not.toHaveBeenCalled();
@ -240,14 +232,6 @@ describe('instructBranch direct execution flow', () => {
}); });
it('should call selectPiece when previous piece reuse is declined', async () => { it('should call selectPiece when previous piece reuse is declined', async () => {
mockFindRunForTask.mockReturnValue('run-previous');
mockLoadRunSessionContext.mockReturnValue({
task: 'done',
piece: 'default',
status: 'completed',
movementLogs: [],
reports: [],
});
mockConfirm mockConfirm
.mockResolvedValueOnce(false); .mockResolvedValueOnce(false);
mockSelectPiece.mockResolvedValue('selected-piece'); mockSelectPiece.mockResolvedValue('selected-piece');
@ -260,22 +244,14 @@ describe('instructBranch direct execution flow', () => {
content: 'done', content: 'done',
branch: 'takt/done-task', branch: 'takt/done-task',
worktreePath: '/project/.takt/worktrees/done-task', worktreePath: '/project/.takt/worktrees/done-task',
data: { task: 'done' }, data: { task: 'done', piece: 'default' },
}); });
expect(mockSelectPiece).toHaveBeenCalledWith('/project'); expect(mockSelectPiece).toHaveBeenCalledWith('/project');
expect(mockStartReExecution).toHaveBeenCalled(); expect(mockStartReExecution).toHaveBeenCalled();
}); });
it('should ignore previous piece when run metadata contains piece path', async () => { it('should skip reuse prompt when task data has no piece', async () => {
mockFindRunForTask.mockReturnValue('run-previous');
mockLoadRunSessionContext.mockReturnValue({
task: 'done',
piece: '../secrets.yaml',
status: 'completed',
movementLogs: [],
reports: [],
});
mockSelectPiece.mockResolvedValue('selected-piece'); mockSelectPiece.mockResolvedValue('selected-piece');
await instructBranch('/project', { await instructBranch('/project', {
@ -291,18 +267,9 @@ describe('instructBranch direct execution flow', () => {
expect(mockConfirm).not.toHaveBeenCalled(); expect(mockConfirm).not.toHaveBeenCalled();
expect(mockSelectPiece).toHaveBeenCalledWith('/project'); expect(mockSelectPiece).toHaveBeenCalledWith('/project');
expect(mockStartReExecution).toHaveBeenCalled();
}); });
it('should return false when replacement piece selection is cancelled after declining reuse', async () => { it('should return false when replacement piece selection is cancelled after declining reuse', async () => {
mockFindRunForTask.mockReturnValue('run-previous');
mockLoadRunSessionContext.mockReturnValue({
task: 'done',
piece: 'default',
status: 'completed',
movementLogs: [],
reports: [],
});
mockConfirm.mockResolvedValueOnce(false); mockConfirm.mockResolvedValueOnce(false);
mockSelectPiece.mockResolvedValue(null); mockSelectPiece.mockResolvedValue(null);
@ -314,7 +281,7 @@ describe('instructBranch direct execution flow', () => {
content: 'done', content: 'done',
branch: 'takt/done-task', branch: 'takt/done-task',
worktreePath: '/project/.takt/worktrees/done-task', worktreePath: '/project/.takt/worktrees/done-task',
data: { task: 'done' }, data: { task: 'done', piece: 'default' },
}); });
expect(result).toBe(false); expect(result).toBe(false);

View File

@ -184,11 +184,12 @@ beforeEach(() => {
describe('retryFailedTask', () => { describe('retryFailedTask', () => {
it('should run retry mode in existing worktree and execute directly', async () => { it('should run retry mode in existing worktree and execute directly', async () => {
const task = makeFailedTask(); const task = makeFailedTask();
mockConfirm.mockResolvedValue(true);
const result = await retryFailedTask(task, '/project'); const result = await retryFailedTask(task, '/project');
expect(result).toBe(true); expect(result).toBe(true);
expect(mockSelectPiece).toHaveBeenCalledWith('/project'); expect(mockSelectPiece).not.toHaveBeenCalled();
expect(mockRunRetryMode).toHaveBeenCalledWith( expect(mockRunRetryMode).toHaveBeenCalledWith(
'/project/.takt/worktrees/my-task', '/project/.takt/worktrees/my-task',
expect.objectContaining({ expect.objectContaining({
@ -201,6 +202,7 @@ describe('retryFailedTask', () => {
}); });
it('should execute with selected piece without mutating taskInfo', async () => { it('should execute with selected piece without mutating taskInfo', async () => {
mockConfirm.mockResolvedValue(false);
mockSelectPiece.mockResolvedValue('selected-piece'); mockSelectPiece.mockResolvedValue('selected-piece');
const originalTaskInfo = { const originalTaskInfo = {
name: 'my-task', name: 'my-task',
@ -319,6 +321,7 @@ describe('retryFailedTask', () => {
it('should return false when piece selection is cancelled', async () => { it('should return false when piece selection is cancelled', async () => {
const task = makeFailedTask(); const task = makeFailedTask();
mockConfirm.mockResolvedValue(false);
mockSelectPiece.mockResolvedValue(null); mockSelectPiece.mockResolvedValue(null);
const result = await retryFailedTask(task, '/project'); const result = await retryFailedTask(task, '/project');
@ -358,11 +361,7 @@ describe('retryFailedTask', () => {
expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A'); expect(mockRequeueTask).toHaveBeenCalledWith('my-task', ['failed'], undefined, '既存ノート\n\n追加指示A');
}); });
describe('when previous piece exists', () => { describe('when previous piece exists in task data', () => {
beforeEach(() => {
mockFindRunForTask.mockReturnValue('run-123');
});
it('should ask whether to reuse previous piece with default yes', async () => { it('should ask whether to reuse previous piece with default yes', async () => {
const task = makeFailedTask(); const task = makeFailedTask();
@ -403,21 +402,13 @@ describe('retryFailedTask', () => {
expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled(); expect(mockLoadPieceByIdentifier).not.toHaveBeenCalled();
}); });
it('should ignore previous piece when run metadata contains piece path', async () => { it('should skip reuse prompt when task data has no piece', async () => {
const task = makeFailedTask(); const task = makeFailedTask({ data: { task: 'Do something' } });
mockLoadRunSessionContext.mockReturnValue({
task: 'Do something',
piece: '../secrets.yaml',
status: 'failed',
movementLogs: [],
reports: [],
});
await retryFailedTask(task, '/project'); await retryFailedTask(task, '/project');
expect(mockConfirm).not.toHaveBeenCalled(); expect(mockConfirm).not.toHaveBeenCalled();
expect(mockSelectPiece).toHaveBeenCalledWith('/project'); expect(mockSelectPiece).toHaveBeenCalledWith('/project');
expect(mockLoadPieceByIdentifier).toHaveBeenCalledWith('default', '/project');
}); });
}); });
}); });

View File

@ -99,7 +99,7 @@ export async function instructBranch(
const previousRunContext = matchedSlug const previousRunContext = matchedSlug
? loadRunSessionContext(worktreePath, matchedSlug) ? loadRunSessionContext(worktreePath, matchedSlug)
: undefined; : undefined;
const selectedPiece = await selectPieceWithOptionalReuse(projectDir, previousRunContext?.piece, lang); const selectedPiece = await selectPieceWithOptionalReuse(projectDir, target.data?.piece, lang);
if (!selectedPiece) { if (!selectedPiece) {
info('Cancelled'); info('Cancelled');
return false; return false;

View File

@ -136,7 +136,7 @@ export async function retryFailedTask(
const matchedSlug = findRunForTask(worktreePath, task.content); const matchedSlug = findRunForTask(worktreePath, task.content);
const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null; const runInfo = matchedSlug ? buildRetryRunInfo(worktreePath, matchedSlug) : null;
const selectedPiece = await selectPieceWithOptionalReuse(projectDir, runInfo?.piece); const selectedPiece = await selectPieceWithOptionalReuse(projectDir, task.data?.piece);
if (!selectedPiece) { if (!selectedPiece) {
info('Cancelled'); info('Cancelled');
return false; return false;