From 2c738d8009f356e7e3fd69df9c7d1c5681a266fe Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:47:27 +0900 Subject: [PATCH] =?UTF-8?q?worktree=E6=99=82=E3=81=AB=E3=83=87=E3=82=A3?= =?UTF-8?q?=E3=83=AC=E3=82=AF=E3=83=88=E3=83=AA=E3=82=92=E6=AD=A3=E3=81=97?= =?UTF-8?q?=E3=81=8F=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=82=81=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/ja/agents/default/ai-reviewer.md | 25 ++- .../global/ja/agents/default/architect.md | 41 +++- resources/global/ja/agents/default/coder.md | 1 + resources/global/ja/workflows/default.yaml | 6 +- .../global/ja/workflows/expert-review.yaml | 181 ++++++++++++++++-- src/__tests__/instructionBuilder.test.ts | 144 ++++++++++++++ src/cli.ts | 2 +- src/commands/taskExecution.ts | 14 +- src/commands/workflowExecution.ts | 24 ++- src/workflow/engine.ts | 25 +-- src/workflow/index.ts | 2 +- src/workflow/instruction-builder.ts | 13 +- src/workflow/types.ts | 2 + 13 files changed, 430 insertions(+), 50 deletions(-) create mode 100644 src/__tests__/instructionBuilder.test.ts diff --git a/resources/global/ja/agents/default/ai-reviewer.md b/resources/global/ja/agents/default/ai-reviewer.md index 49b5605..93c9e53 100644 --- a/resources/global/ja/agents/default/ai-reviewer.md +++ b/resources/global/ja/agents/default/ai-reviewer.md @@ -51,11 +51,15 @@ AI生成コードには特有の特徴があります: | 古いパターン | 学習データからの非推奨アプローチの使用 | | 過剰エンジニアリング | タスクに不要な抽象化レイヤーの追加 | | 過小エンジニアリング | 現実的なシナリオのエラーハンドリングの欠如 | +| 配線忘れ | 機構は実装されているが、エントリポイントから渡されていない | **検証アプローチ:** 1. このコードは実際にコンパイル/実行できるか? 2. インポートされたモジュール/関数は存在するか? 3. このライブラリバージョンでAPIは正しく使用されているか? +4. 新しいパラメータ/フィールドが追加された場合、呼び出し元から実際に渡されているか? + - AIは個々のファイル内では正しく実装するが、ファイル横断の結合を忘れがち + - `options.xxx ?? fallback` で常にフォールバックが使われていないか grep で確認 ### 3. コピペパターン検出 @@ -96,7 +100,26 @@ AI生成コードには特有の特徴があります: **原則:** 最良のコードは、問題を解決する最小限のコード。 -### 6. 決定トレーサビリティレビュー +### 6. デッドコード検出 + +**AIは新しいコードを追加するが、不要になったコードの削除を忘れることが多い。** + +| パターン | 例 | +|---------|-----| +| 未使用の関数・メソッド | リファクタリング後に残った旧実装 | +| 未使用の変数・定数 | 条件変更で不要になった定義 | +| 到達不能コード | 早期returnの後に残った処理、常に真/偽になる条件分岐 | +| 未使用のインポート・依存 | 削除された機能のimport文やパッケージ依存 | +| 孤立したエクスポート・公開API | 実体が消えたのにre-exportやindex登録が残っている | +| 未使用のインターフェース・型定義 | 実装側が変更されたのに残った古い型 | +| 無効化されたコード | コメントアウトされたまま放置されたコード | + +**検証アプローチ:** +1. 変更・削除されたコードを参照している箇所がないか grep で確認 +2. 公開モジュール(index ファイル等)のエクスポート一覧と実体が一致しているか確認 +3. 新規追加されたコードに対応する古いコードが残っていないか確認 + +### 7. 決定トレーサビリティレビュー **Coderの決定ログが妥当か検証する。** diff --git a/resources/global/ja/agents/default/architect.md b/resources/global/ja/agents/default/architect.md index 6d8739e..d43bdc1 100644 --- a/resources/global/ja/agents/default/architect.md +++ b/resources/global/ja/agents/default/architect.md @@ -384,7 +384,40 @@ function createOrder(data: OrderData) { - 3回の重複 → 即抽出 - ドメインが異なる重複 → 抽象化しない(例: 顧客用バリデーションと管理者用バリデーションは別物) -### 8. 品質特性 +### 8. 呼び出しチェーン検証 + +**新しいパラメータ・フィールドが追加された場合、変更ファイル内だけでなく呼び出し元も検証する。** + +**検証手順:** +1. 新しいオプショナルパラメータや interface フィールドを見つけたら、`Grep` で全呼び出し元を検索 +2. 全呼び出し元が新しいパラメータを渡しているか確認 +3. フォールバック値(`?? default`)がある場合、フォールバックが使われるケースが意図通りか確認 + +**危険パターン:** + +| パターン | 問題 | 検出方法 | +|---------|------|---------| +| `options.xxx ?? fallback` で全呼び出し元が `xxx` を省略 | 機能が実装されているのに常にフォールバック | grep で呼び出し元を確認 | +| テストがモックで直接値をセット | 実際の呼び出しチェーンを経由しない | テストの構築方法を確認 | +| `executeXxx()` が内部で使う `options` を引数で受け取らない | 上位から値を渡す口がない | 関数シグネチャを確認 | + +**具体例:** + +```typescript +// ❌ 配線漏れ: projectCwd を受け取る口がない +export async function executeWorkflow(config, cwd, task) { + const engine = new WorkflowEngine(config, cwd, task); // options なし +} + +// ✅ 配線済み: projectCwd を渡せる +export async function executeWorkflow(config, cwd, task, options?) { + const engine = new WorkflowEngine(config, cwd, task, options); +} +``` + +**このパターンを見つけたら REJECT。** 個々のファイルが正しくても、結合されていなければ機能しない。 + +### 9. 品質特性 | 特性 | 確認観点 | |------|---------| @@ -392,7 +425,7 @@ function createOrder(data: OrderData) { | Maintainability | 変更・修正が容易か | | Observability | ログ・監視が可能な設計か | -### 9. 大局観 +### 10. 大局観 **注意**: 細かい「クリーンコード」の指摘に終始しない。 @@ -403,7 +436,7 @@ function createOrder(data: OrderData) { - ビジネス要件と整合しているか - 命名がドメインと一貫しているか -### 10. 変更スコープの評価 +### 11. 変更スコープの評価 **変更スコープを確認し、レポートに記載する(ブロッキングではない)。** @@ -422,7 +455,7 @@ function createOrder(data: OrderData) { **提案として記載すること(ブロッキングではない):** - 分割可能な場合は分割案を提示 -### 11. 堂々巡りの検出 +### 12. 堂々巡りの検出 レビュー回数が渡される場合(例: 「レビュー回数: 3回目」)、回数に応じて判断を変える。 diff --git a/resources/global/ja/agents/default/coder.md b/resources/global/ja/agents/default/coder.md index 2c9c013..6d73fd7 100644 --- a/resources/global/ja/agents/default/coder.md +++ b/resources/global/ja/agents/default/coder.md @@ -90,6 +90,7 @@ | 構文エラー | ビルド・コンパイル | | テスト | テスト実行 | | 要求充足 | 元のタスク要求と照合 | +| デッドコード | 変更・削除した機能を参照する未使用コードが残っていないか確認(未使用の関数、変数、インポート、エクスポート、型定義、到達不能コード) | **すべて確認してから `[DONE]` を出力。** diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/workflows/default.yaml index 035a573..ef5e0cb 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/workflows/default.yaml @@ -336,7 +336,9 @@ steps: - 構造・設計の妥当性 - コード品質 - 変更スコープの適切性 - - **テストカバレッジ**: 実装に対応する単体テストが追加されているか + - テストカバレッジ + - デッドコード + - 呼び出しチェーン検証 **レポート出力:** 上記の `Report File` に出力してください。 - ファイルが存在しない場合: 新規作成 @@ -356,6 +358,8 @@ steps: - [x] コード品質 - [x] 変更スコープ - [x] テストカバレッジ + - [x] デッドコード + - [x] 呼び出しチェーン検証 ## 問題点(REJECTの場合) | # | 場所 | 問題 | 修正案 | diff --git a/resources/global/ja/workflows/expert-review.yaml b/resources/global/ja/workflows/expert-review.yaml index ba41cdf..8cdaff5 100644 --- a/resources/global/ja/workflows/expert-review.yaml +++ b/resources/global/ja/workflows/expert-review.yaml @@ -2,9 +2,9 @@ # CQRS+ES、フロントエンド、セキュリティ、QAの専門家によるレビューワークフロー # # フロー: -# plan -> implement -> cqrs_es_review -> frontend_review -> ai_review -> security_review -> qa_review -> supervise -> COMPLETE -# ↓ ↓ ↓ ↓ ↓ ↓ -# fix_cqrs_es fix_frontend ai_fix fix_security fix_qa fix_supervisor +# plan -> implement -> architect_review -> cqrs_es_review -> frontend_review -> ai_review -> security_review -> qa_review -> supervise -> COMPLETE +# ↓ ↓ ↓ ↓ ↓ ↓ ↓ +# fix_architect fix_cqrs_es fix_frontend ai_fix fix_security fix_qa fix_supervisor # # 修正時の戻り先はCoderが判断: # - fix_security: MINOR→security_review, MAJOR→cqrs_es_review @@ -196,12 +196,169 @@ steps: 進行できない場合は [CODER:BLOCKED] を出力し、planに戻ります。 transitions: - condition: done - next_step: cqrs_es_review + next_step: architect_review - condition: blocked next_step: plan # =========================================== - # Phase 2: CQRS+ES Review + # Phase 2: Architecture Review + # =========================================== + - name: architect_review + agent: ~/.takt/agents/default/architect.md + allowed_tools: + - Read + - Glob + - Grep + - WebSearch + - WebFetch + status_rules_prompt: | + # ⚠️ 必須: ステータス出力ルール ⚠️ + + **このタグがないとワークフローが停止します。** + 最終出力には必ず以下のルールに従ったステータスタグを含めてください。 + + ## 判定基準 + + | 状況 | 判定 | + |------|------| + | 構造に問題がある | REJECT | + | 設計原則違反がある | REJECT | + | 呼び出しチェーンの配線漏れ | REJECT | + | テストが不十分 | REJECT | + | 改善すべき点がある(軽微) | IMPROVE | + | 問題なし | APPROVE | + + ## 出力フォーマット + + | 状況 | タグ | + |------|------| + | 問題なし | `[ARCHITECT:APPROVE]` | + | 軽微な改善必要 | `[ARCHITECT:IMPROVE]` | + | 構造的な修正必要 | `[ARCHITECT:REJECT]` | + instruction_template: | + ## Workflow Context + - Iteration: {iteration}/{max_iterations}(ワークフロー全体) + - Step Iteration: {step_iteration}(このステップの実行回数) + - Step: architect_review (アーキテクチャレビュー) + - Report Directory: .takt/reports/{report_dir}/ + - Report File: .takt/reports/{report_dir}/03-architect-review.md + + ## Original User Request (ワークフロー開始時の元の要求) + {task} + + ## Git Diff + ```diff + {git_diff} + ``` + + ## Instructions + **アーキテクチャと設計**のレビューに集中してください。 + + **レビュー観点:** + - 構造・設計の妥当性 + - コード品質 + - 変更スコープの適切性 + - テストカバレッジ + - デッドコード + - 呼び出しチェーン検証 + + **レポート出力:** 上記の `Report File` に出力してください。 + - ファイルが存在しない場合: 新規作成 + - ファイルが存在する場合: `## Iteration {step_iteration}` セクションを追記 + + **レポートフォーマット:** + ```markdown + # アーキテクチャレビュー + + ## 結果: APPROVE / IMPROVE / REJECT + + ## サマリー + {1-2文で結果を要約} + + ## 確認した観点 + - [x] 構造・設計 + - [x] コード品質 + - [x] 変更スコープ + - [x] テストカバレッジ + - [x] デッドコード + - [x] 呼び出しチェーン検証 + + ## 問題点(REJECTの場合) + | # | 場所 | 問題 | 修正案 | + |---|------|------|--------| + | 1 | `src/file.ts:42` | 問題の説明 | 修正方法 | + + ## 改善提案(任意・ブロッキングではない) + - {将来的な改善提案} + ``` + + **認知負荷軽減ルール:** + - APPROVE + 問題なし → サマリーのみ(5行以内) + - APPROVE + 軽微な提案 → サマリー + 改善提案(15行以内) + - REJECT → 問題点を表形式で(30行以内) + transitions: + - condition: approved + next_step: cqrs_es_review + - condition: improve + next_step: fix_architect + - condition: rejected + next_step: fix_architect + + - name: fix_architect + agent: ~/.takt/agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: acceptEdits + status_rules_prompt: | + # ⚠️ 必須: ステータス出力ルール ⚠️ + + **このタグがないとワークフローが停止します。** + 最終出力には必ず以下のルールに従ったステータスタグを含めてください。 + + ## 出力フォーマット + + | 状況 | タグ | + |------|------| + | 修正完了 | `[CODER:DONE]` | + | 進行不可 | `[CODER:BLOCKED]` | + instruction_template: | + ## Workflow Context + - Iteration: {iteration}/{max_iterations}(ワークフロー全体) + - Step Iteration: {step_iteration}(このステップの実行回数) + - Step: fix_architect + + ## Architect Feedback (これが最新の指示です - 優先して対応してください) + {previous_response} + + ## Original User Request (ワークフロー開始時の元の要求 - 参考情報) + {task} + + ## Additional User Inputs + {user_inputs} + + ## Instructions + **重要**: Architectのフィードバックに対応してください。 + 「Original User Request」は参考情報であり、最新の指示ではありません。 + セッションの会話履歴を確認し、Architectの指摘事項を修正してください。 + + 完了時は [CODER:DONE] を含めてください。 + 進行できない場合は [CODER:BLOCKED] を含めてください。 + pass_previous_response: true + transitions: + - condition: done + next_step: architect_review + - condition: blocked + next_step: plan + + # =========================================== + # Phase 3: CQRS+ES Review # =========================================== - name: cqrs_es_review agent: ~/.takt/agents/expert-review/cqrs-es-reviewer.md @@ -229,7 +386,7 @@ steps: - Step Iteration: {step_iteration}(このステップの実行回数) - Step: cqrs_es_review (CQRS+ES専門レビュー) - Report Directory: .takt/reports/{report_dir}/ - - Report File: .takt/reports/{report_dir}/03-cqrs-es-review.md + - Report File: .takt/reports/{report_dir}/04-cqrs-es-review.md ## Original User Request {task} @@ -376,7 +533,7 @@ steps: - Step Iteration: {step_iteration}(このステップの実行回数) - Step: frontend_review (フロントエンド専門レビュー) - Report Directory: .takt/reports/{report_dir}/ - - Report File: .takt/reports/{report_dir}/04-frontend-review.md + - Report File: .takt/reports/{report_dir}/05-frontend-review.md ## Original User Request {task} @@ -523,7 +680,7 @@ steps: - Step Iteration: {step_iteration}(このステップの実行回数) - Step: ai_review (AI生成コードレビュー) - Report Directory: .takt/reports/{report_dir}/ - - Report File: .takt/reports/{report_dir}/05-ai-review.md + - Report File: .takt/reports/{report_dir}/06-ai-review.md ## Original User Request (ワークフロー開始時の元の要求) {task} @@ -664,7 +821,7 @@ steps: - Step Iteration: {step_iteration}(このステップの実行回数) - Step: security_review (セキュリティ専門レビュー) - Report Directory: .takt/reports/{report_dir}/ - - Report File: .takt/reports/{report_dir}/06-security-review.md + - Report File: .takt/reports/{report_dir}/07-security-review.md ## Original User Request {task} @@ -818,7 +975,7 @@ steps: - Step Iteration: {step_iteration}(このステップの実行回数) - Step: qa_review (QA専門レビュー) - Report Directory: .takt/reports/{report_dir}/ - - Report File: .takt/reports/{report_dir}/07-qa-review.md + - Report File: .takt/reports/{report_dir}/08-qa-review.md ## Original User Request {task} @@ -978,7 +1135,7 @@ steps: - Step: supervise (最終確認) - Report Directory: .takt/reports/{report_dir}/ - Report Files: - - Validation: .takt/reports/{report_dir}/08-supervisor-validation.md + - Validation: .takt/reports/{report_dir}/09-supervisor-validation.md - Summary: .takt/reports/{report_dir}/summary.md ## Original User Request @@ -991,6 +1148,7 @@ steps: ## Previous Reviews Summary このステップに到達したということは、以下のレビューがすべてAPPROVEされています: + - Architecture Review: APPROVED - CQRS+ES Review: APPROVED - Frontend Review: APPROVED - AI Review: APPROVED @@ -1054,6 +1212,7 @@ steps: ## レビュー結果 | レビュー | 結果 | |---------|------| + | Architecture | ✅ APPROVE | | CQRS+ES | ✅ APPROVE | | Frontend | ✅ APPROVE | | AI Review | ✅ APPROVE | diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts new file mode 100644 index 0000000..686ff41 --- /dev/null +++ b/src/__tests__/instructionBuilder.test.ts @@ -0,0 +1,144 @@ +/** + * Tests for instruction-builder module + */ + +import { describe, it, expect } from 'vitest'; +import { buildInstruction, type InstructionContext } from '../workflow/instruction-builder.js'; +import type { WorkflowStep } from '../models/types.js'; + +function createMinimalStep(template: string): WorkflowStep { + return { + name: 'test-step', + agent: 'test-agent', + agentDisplayName: 'Test Agent', + instructionTemplate: template, + transitions: [], + passPreviousResponse: false, + }; +} + +function createMinimalContext(overrides: Partial = {}): InstructionContext { + return { + task: 'Test task', + iteration: 1, + maxIterations: 10, + stepIteration: 1, + cwd: '/project', + userInputs: [], + ...overrides, + }; +} + +describe('instruction-builder', () => { + describe('report_dir replacement', () => { + it('should replace .takt/reports/{report_dir} with full absolute path', () => { + const step = createMinimalStep( + '- Report Directory: .takt/reports/{report_dir}/' + ); + const context = createMinimalContext({ + cwd: '/project', + reportDir: '20260128-test-report', + }); + + const result = buildInstruction(step, context); + + expect(result).toBe( + '- Report Directory: /project/.takt/reports/20260128-test-report/' + ); + }); + + it('should use projectCwd for report path when cwd is a worktree', () => { + const step = createMinimalStep( + '- Report: .takt/reports/{report_dir}/00-plan.md' + ); + const context = createMinimalContext({ + cwd: '/project/.takt/worktrees/my-task', + projectCwd: '/project', + reportDir: '20260128-worktree-report', + }); + + const result = buildInstruction(step, context); + + expect(result).toBe( + '- Report: /project/.takt/reports/20260128-worktree-report/00-plan.md' + ); + // Should NOT contain the worktree path + expect(result).not.toContain('/project/.takt/worktrees/'); + }); + + it('should replace multiple .takt/reports/{report_dir} occurrences', () => { + const step = createMinimalStep( + '- Scope: .takt/reports/{report_dir}/01-scope.md\n- Decisions: .takt/reports/{report_dir}/02-decisions.md' + ); + const context = createMinimalContext({ + projectCwd: '/project', + cwd: '/worktree', + reportDir: '20260128-multi', + }); + + const result = buildInstruction(step, context); + + expect(result).toContain('/project/.takt/reports/20260128-multi/01-scope.md'); + expect(result).toContain('/project/.takt/reports/20260128-multi/02-decisions.md'); + }); + + it('should replace standalone {report_dir} with directory name only', () => { + const step = createMinimalStep( + 'Report dir name: {report_dir}' + ); + const context = createMinimalContext({ + reportDir: '20260128-standalone', + }); + + const result = buildInstruction(step, context); + + expect(result).toBe('Report dir name: 20260128-standalone'); + }); + + it('should fall back to cwd when projectCwd is not provided', () => { + const step = createMinimalStep( + '- Dir: .takt/reports/{report_dir}/' + ); + const context = createMinimalContext({ + cwd: '/fallback-project', + reportDir: '20260128-fallback', + }); + // projectCwd intentionally omitted + + const result = buildInstruction(step, context); + + expect(result).toBe( + '- Dir: /fallback-project/.takt/reports/20260128-fallback/' + ); + }); + }); + + describe('basic placeholder replacement', () => { + it('should replace {task} placeholder', () => { + const step = createMinimalStep('Execute: {task}'); + const context = createMinimalContext({ task: 'Build the app' }); + + const result = buildInstruction(step, context); + + expect(result).toContain('Build the app'); + }); + + it('should replace {iteration} and {max_iterations}', () => { + const step = createMinimalStep('Step {iteration}/{max_iterations}'); + const context = createMinimalContext({ iteration: 3, maxIterations: 20 }); + + const result = buildInstruction(step, context); + + expect(result).toBe('Step 3/20'); + }); + + it('should replace {step_iteration}', () => { + const step = createMinimalStep('Run #{step_iteration}'); + const context = createMinimalContext({ stepIteration: 2 }); + + const result = buildInstruction(step, context); + + expect(result).toBe('Run #2'); + }); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index 8a7081d..01c3c4d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -219,7 +219,7 @@ program const { execCwd, isWorktree } = await confirmAndCreateWorktree(cwd, task); log.info('Starting task execution', { task, workflow: selectedWorkflow, worktree: isWorktree }); - const taskSuccess = await executeTask(task, execCwd, selectedWorkflow); + const taskSuccess = await executeTask(task, execCwd, selectedWorkflow, cwd); if (taskSuccess && isWorktree) { const commitResult = autoCommitWorktree(execCwd, task); diff --git a/src/commands/taskExecution.ts b/src/commands/taskExecution.ts index 4cf7a87..f5c2be0 100644 --- a/src/commands/taskExecution.ts +++ b/src/commands/taskExecution.ts @@ -22,11 +22,16 @@ const log = createLogger('task'); /** * Execute a single task with workflow + * @param task - Task content + * @param cwd - Working directory (may be a worktree path) + * @param workflowName - Workflow to use + * @param projectCwd - Project root (where .takt/ lives). Defaults to cwd. */ export async function executeTask( task: string, cwd: string, - workflowName: string = DEFAULT_WORKFLOW_NAME + workflowName: string = DEFAULT_WORKFLOW_NAME, + projectCwd?: string ): Promise { const workflowConfig = loadWorkflow(workflowName); @@ -42,7 +47,9 @@ export async function executeTask( steps: workflowConfig.steps.map(s => s.name), }); - const result = await executeWorkflow(workflowConfig, task, cwd); + const result = await executeWorkflow(workflowConfig, task, cwd, { + projectCwd, + }); return result.success; } @@ -66,7 +73,8 @@ export async function executeAndCompleteTask( try { const { execCwd, execWorkflow, isWorktree } = resolveTaskExecution(task, cwd, workflowName); - const taskSuccess = await executeTask(task.content, execCwd, execWorkflow); + // cwd is always the project root; pass it as projectCwd so reports/sessions go there + const taskSuccess = await executeTask(task.content, execCwd, execWorkflow, cwd); const completedAt = new Date().toISOString(); if (taskSuccess && isWorktree) { diff --git a/src/commands/workflowExecution.ts b/src/commands/workflowExecution.ts index edb1c2b..3f16a7d 100644 --- a/src/commands/workflowExecution.ts +++ b/src/commands/workflowExecution.ts @@ -56,6 +56,8 @@ export interface WorkflowExecutionResult { export interface WorkflowExecutionOptions { /** Header prefix for display */ headerPrefix?: string; + /** Project root directory (where .takt/ lives). Defaults to cwd. */ + projectCwd?: string; } /** @@ -71,13 +73,16 @@ export async function executeWorkflow( headerPrefix = 'Running Workflow:', } = options; + // projectCwd is where .takt/ lives (project root, not worktree) + const projectCwd = options.projectCwd ?? cwd; + // Always continue from previous sessions (use /clear to reset) log.debug('Continuing session (use /clear to reset)'); header(`${headerPrefix} ${workflowConfig.name}`); const workflowSessionId = generateSessionId(); - const sessionLog = createSessionLog(task, cwd, workflowConfig.name); + const sessionLog = createSessionLog(task, projectCwd, workflowConfig.name); // Track current display for streaming const displayRef: { current: StreamDisplay | null } = { current: null }; @@ -91,12 +96,12 @@ export async function executeWorkflow( displayRef.current.createHandler()(event); }; - // Load saved agent sessions for continuity - const savedSessions = loadAgentSessions(cwd); + // Load saved agent sessions for continuity (from project root) + const savedSessions = loadAgentSessions(projectCwd); - // Session update handler - persist session IDs when they change + // Session update handler - persist session IDs when they change (to project root) const sessionUpdateHandler = (agentName: string, agentSessionId: string): void => { - updateAgentSession(cwd, agentName, agentSessionId); + updateAgentSession(projectCwd, agentName, agentSessionId); }; const iterationLimitHandler = async ( @@ -147,6 +152,7 @@ export async function executeWorkflow( initialSessions: savedSessions, onSessionUpdate: sessionUpdateHandler, onIterationLimit: iterationLimitHandler, + projectCwd, }); let abortReason: string | undefined; @@ -179,8 +185,8 @@ export async function executeWorkflow( engine.on('workflow:complete', (state) => { log.info('Workflow completed successfully', { iterations: state.iteration }); finalizeSessionLog(sessionLog, 'completed'); - // Save log to original cwd so user can find it easily - const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd); + // Save log to project root so user can find it easily + const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd); const elapsed = sessionLog.endTime ? formatElapsedTime(sessionLog.startTime, sessionLog.endTime) @@ -200,8 +206,8 @@ export async function executeWorkflow( } abortReason = reason; finalizeSessionLog(sessionLog, 'aborted'); - // Save log to original cwd so user can find it easily - const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd); + // Save log to project root so user can find it easily + const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd); const elapsed = sessionLog.endTime ? formatElapsedTime(sessionLog.startTime, sessionLog.endTime) diff --git a/src/workflow/engine.ts b/src/workflow/engine.ts index 9b6b389..3e6bbb5 100644 --- a/src/workflow/engine.ts +++ b/src/workflow/engine.ts @@ -44,7 +44,7 @@ export { COMPLETE_STEP, ABORT_STEP } from './constants.js'; export class WorkflowEngine extends EventEmitter { private state: WorkflowState; private config: WorkflowConfig; - private originalCwd: string; + private projectCwd: string; private cwd: string; private task: string; private options: WorkflowEngineOptions; @@ -54,7 +54,7 @@ export class WorkflowEngine extends EventEmitter { constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions = {}) { super(); this.config = config; - this.originalCwd = cwd; + this.projectCwd = options.projectCwd ?? cwd; this.cwd = cwd; this.task = task; this.options = options; @@ -71,9 +71,9 @@ export class WorkflowEngine extends EventEmitter { }); } - /** Ensure report directory exists (always in original cwd) */ + /** Ensure report directory exists (always in project root, not worktree) */ private ensureReportDirExists(): void { - const reportDirPath = join(this.originalCwd, '.takt', 'reports', this.reportDir); + const reportDirPath = join(this.projectCwd, '.takt', 'reports', this.reportDir); if (!existsSync(reportDirPath)) { mkdirSync(reportDirPath, { recursive: true }); } @@ -121,9 +121,9 @@ export class WorkflowEngine extends EventEmitter { return this.cwd; } - /** Get original working directory (for .takt data) */ - getOriginalCwd(): string { - return this.originalCwd; + /** Get project root directory (where .takt/ lives) */ + getProjectCwd(): string { + return this.projectCwd; } /** Build instruction from template */ @@ -134,6 +134,7 @@ export class WorkflowEngine extends EventEmitter { maxIterations: this.config.maxIterations, stepIteration, cwd: this.cwd, + projectCwd: this.projectCwd, userInputs: this.state.userInputs, previousOutput: getPreviousOutput(this.state), reportDir: this.reportDir, @@ -325,13 +326,3 @@ export class WorkflowEngine extends EventEmitter { return { response, nextStep, isComplete, loopDetected: loopCheck.isLoop }; } } - -/** Create and run a workflow */ -export async function executeWorkflow( - config: WorkflowConfig, - cwd: string, - task: string -): Promise { - const engine = new WorkflowEngine(config, cwd, task); - return engine.run(); -} diff --git a/src/workflow/index.ts b/src/workflow/index.ts index 872d732..f4b654b 100644 --- a/src/workflow/index.ts +++ b/src/workflow/index.ts @@ -6,7 +6,7 @@ */ // Main engine -export { WorkflowEngine, executeWorkflow } from './engine.js'; +export { WorkflowEngine } from './engine.js'; // Constants export { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from './constants.js'; diff --git a/src/workflow/instruction-builder.ts b/src/workflow/instruction-builder.ts index d8e89e4..2de0b77 100644 --- a/src/workflow/instruction-builder.ts +++ b/src/workflow/instruction-builder.ts @@ -5,6 +5,7 @@ * template placeholders with actual values. */ +import { join } from 'node:path'; import type { WorkflowStep, AgentResponse } from '../models/types.js'; import { getGitDiff } from '../agents/runner.js'; @@ -20,8 +21,10 @@ export interface InstructionContext { maxIterations: number; /** Current step's iteration number (how many times this step has been executed) */ stepIteration: number; - /** Working directory */ + /** Working directory (agent work dir, may be a worktree) */ cwd: string; + /** Project root directory (where .takt/ lives). Defaults to cwd. */ + projectCwd?: string; /** User inputs accumulated during workflow */ userInputs: string[]; /** Previous step output if available */ @@ -87,8 +90,14 @@ export function buildInstruction( escapeTemplateChars(userInputsStr) ); - // Replace {report_dir} + // Replace .takt/reports/{report_dir} with absolute path first, + // then replace standalone {report_dir} with the directory name. + // This ensures agents always use the correct project root for reports, + // even when their cwd is a worktree. if (context.reportDir) { + const projectRoot = context.projectCwd ?? context.cwd; + const reportDirFullPath = join(projectRoot, '.takt', 'reports', context.reportDir); + instruction = instruction.replace(/\.takt\/reports\/\{report_dir\}/g, reportDirFullPath); instruction = instruction.replace(/\{report_dir\}/g, context.reportDir); } diff --git a/src/workflow/types.ts b/src/workflow/types.ts index baa4781..3c84642 100644 --- a/src/workflow/types.ts +++ b/src/workflow/types.ts @@ -70,6 +70,8 @@ export interface WorkflowEngineOptions { onIterationLimit?: IterationLimitCallback; /** Bypass all permission checks (sacrifice-my-pc mode) */ bypassPermissions?: boolean; + /** Project root directory (where .takt/ lives). Defaults to cwd if not specified. */ + projectCwd?: string; } /** Loop detection result */