worktree時にディレクトリを正しく読み込めるように修正

This commit is contained in:
nrslib 2026-01-28 16:47:27 +09:00
parent d612c412f9
commit 2c738d8009
13 changed files with 430 additions and 50 deletions

View File

@ -51,11 +51,15 @@ AI生成コードには特有の特徴があります:
| 古いパターン | 学習データからの非推奨アプローチの使用 | | 古いパターン | 学習データからの非推奨アプローチの使用 |
| 過剰エンジニアリング | タスクに不要な抽象化レイヤーの追加 | | 過剰エンジニアリング | タスクに不要な抽象化レイヤーの追加 |
| 過小エンジニアリング | 現実的なシナリオのエラーハンドリングの欠如 | | 過小エンジニアリング | 現実的なシナリオのエラーハンドリングの欠如 |
| 配線忘れ | 機構は実装されているが、エントリポイントから渡されていない |
**検証アプローチ:** **検証アプローチ:**
1. このコードは実際にコンパイル/実行できるか? 1. このコードは実際にコンパイル/実行できるか?
2. インポートされたモジュール/関数は存在するか? 2. インポートされたモジュール/関数は存在するか?
3. このライブラリバージョンでAPIは正しく使用されているか 3. このライブラリバージョンでAPIは正しく使用されているか
4. 新しいパラメータ/フィールドが追加された場合、呼び出し元から実際に渡されているか?
- AIは個々のファイル内では正しく実装するが、ファイル横断の結合を忘れがち
- `options.xxx ?? fallback` で常にフォールバックが使われていないか grep で確認
### 3. コピペパターン検出 ### 3. コピペパターン検出
@ -96,7 +100,26 @@ AI生成コードには特有の特徴があります:
**原則:** 最良のコードは、問題を解決する最小限のコード。 **原則:** 最良のコードは、問題を解決する最小限のコード。
### 6. 決定トレーサビリティレビュー ### 6. デッドコード検出
**AIは新しいコードを追加するが、不要になったコードの削除を忘れることが多い。**
| パターン | 例 |
|---------|-----|
| 未使用の関数・メソッド | リファクタリング後に残った旧実装 |
| 未使用の変数・定数 | 条件変更で不要になった定義 |
| 到達不能コード | 早期returnの後に残った処理、常に真/偽になる条件分岐 |
| 未使用のインポート・依存 | 削除された機能のimport文やパッケージ依存 |
| 孤立したエクスポート・公開API | 実体が消えたのにre-exportやindex登録が残っている |
| 未使用のインターフェース・型定義 | 実装側が変更されたのに残った古い型 |
| 無効化されたコード | コメントアウトされたまま放置されたコード |
**検証アプローチ:**
1. 変更・削除されたコードを参照している箇所がないか grep で確認
2. 公開モジュールindex ファイル等)のエクスポート一覧と実体が一致しているか確認
3. 新規追加されたコードに対応する古いコードが残っていないか確認
### 7. 決定トレーサビリティレビュー
**Coderの決定ログが妥当か検証する。** **Coderの決定ログが妥当か検証する。**

View File

@ -384,7 +384,40 @@ function createOrder(data: OrderData) {
- 3回の重複 → 即抽出 - 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 | 変更・修正が容易か | | Maintainability | 変更・修正が容易か |
| Observability | ログ・監視が可能な設計か | | Observability | ログ・監視が可能な設計か |
### 9. 大局観 ### 10. 大局観
**注意**: 細かい「クリーンコード」の指摘に終始しない。 **注意**: 細かい「クリーンコード」の指摘に終始しない。
@ -403,7 +436,7 @@ function createOrder(data: OrderData) {
- ビジネス要件と整合しているか - ビジネス要件と整合しているか
- 命名がドメインと一貫しているか - 命名がドメインと一貫しているか
### 10. 変更スコープの評価 ### 11. 変更スコープの評価
**変更スコープを確認し、レポートに記載する(ブロッキングではない)。** **変更スコープを確認し、レポートに記載する(ブロッキングではない)。**
@ -422,7 +455,7 @@ function createOrder(data: OrderData) {
**提案として記載すること(ブロッキングではない):** **提案として記載すること(ブロッキングではない):**
- 分割可能な場合は分割案を提示 - 分割可能な場合は分割案を提示
### 11. 堂々巡りの検出 ### 12. 堂々巡りの検出
レビュー回数が渡される場合(例: 「レビュー回数: 3回目」、回数に応じて判断を変える。 レビュー回数が渡される場合(例: 「レビュー回数: 3回目」、回数に応じて判断を変える。

View File

@ -90,6 +90,7 @@
| 構文エラー | ビルド・コンパイル | | 構文エラー | ビルド・コンパイル |
| テスト | テスト実行 | | テスト | テスト実行 |
| 要求充足 | 元のタスク要求と照合 | | 要求充足 | 元のタスク要求と照合 |
| デッドコード | 変更・削除した機能を参照する未使用コードが残っていないか確認(未使用の関数、変数、インポート、エクスポート、型定義、到達不能コード) |
**すべて確認してから `[DONE]` を出力。** **すべて確認してから `[DONE]` を出力。**

View File

@ -336,7 +336,9 @@ steps:
- 構造・設計の妥当性 - 構造・設計の妥当性
- コード品質 - コード品質
- 変更スコープの適切性 - 変更スコープの適切性
- **テストカバレッジ**: 実装に対応する単体テストが追加されているか - テストカバレッジ
- デッドコード
- 呼び出しチェーン検証
**レポート出力:** 上記の `Report File` に出力してください。 **レポート出力:** 上記の `Report File` に出力してください。
- ファイルが存在しない場合: 新規作成 - ファイルが存在しない場合: 新規作成
@ -356,6 +358,8 @@ steps:
- [x] コード品質 - [x] コード品質
- [x] 変更スコープ - [x] 変更スコープ
- [x] テストカバレッジ - [x] テストカバレッジ
- [x] デッドコード
- [x] 呼び出しチェーン検証
## 問題点REJECTの場合 ## 問題点REJECTの場合
| # | 場所 | 問題 | 修正案 | | # | 場所 | 問題 | 修正案 |

View File

@ -2,9 +2,9 @@
# CQRS+ES、フロントエンド、セキュリティ、QAの専門家によるレビューワークフロー # CQRS+ES、フロントエンド、セキュリティ、QAの専門家によるレビューワークフロー
# #
# フロー: # フロー:
# plan -> implement -> cqrs_es_review -> frontend_review -> ai_review -> security_review -> qa_review -> supervise -> COMPLETE # plan -> implement -> architect_review -> 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 # fix_architect fix_cqrs_es fix_frontend ai_fix fix_security fix_qa fix_supervisor
# #
# 修正時の戻り先はCoderが判断: # 修正時の戻り先はCoderが判断:
# - fix_security: MINOR→security_review, MAJOR→cqrs_es_review # - fix_security: MINOR→security_review, MAJOR→cqrs_es_review
@ -196,12 +196,169 @@ steps:
進行できない場合は [CODER:BLOCKED] を出力し、planに戻ります。 進行できない場合は [CODER:BLOCKED] を出力し、planに戻ります。
transitions: transitions:
- condition: done - condition: done
next_step: cqrs_es_review next_step: architect_review
- condition: blocked - condition: blocked
next_step: plan 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 - name: cqrs_es_review
agent: ~/.takt/agents/expert-review/cqrs-es-reviewer.md agent: ~/.takt/agents/expert-review/cqrs-es-reviewer.md
@ -229,7 +386,7 @@ steps:
- Step Iteration: {step_iteration}(このステップの実行回数) - Step Iteration: {step_iteration}(このステップの実行回数)
- Step: cqrs_es_review (CQRS+ES専門レビュー) - Step: cqrs_es_review (CQRS+ES専門レビュー)
- Report Directory: .takt/reports/{report_dir}/ - 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 ## Original User Request
{task} {task}
@ -376,7 +533,7 @@ steps:
- Step Iteration: {step_iteration}(このステップの実行回数) - Step Iteration: {step_iteration}(このステップの実行回数)
- Step: frontend_review (フロントエンド専門レビュー) - Step: frontend_review (フロントエンド専門レビュー)
- Report Directory: .takt/reports/{report_dir}/ - 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 ## Original User Request
{task} {task}
@ -523,7 +680,7 @@ steps:
- Step Iteration: {step_iteration}(このステップの実行回数) - Step Iteration: {step_iteration}(このステップの実行回数)
- Step: ai_review (AI生成コードレビュー) - Step: ai_review (AI生成コードレビュー)
- Report Directory: .takt/reports/{report_dir}/ - 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 (ワークフロー開始時の元の要求) ## Original User Request (ワークフロー開始時の元の要求)
{task} {task}
@ -664,7 +821,7 @@ steps:
- Step Iteration: {step_iteration}(このステップの実行回数) - Step Iteration: {step_iteration}(このステップの実行回数)
- Step: security_review (セキュリティ専門レビュー) - Step: security_review (セキュリティ専門レビュー)
- Report Directory: .takt/reports/{report_dir}/ - 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 ## Original User Request
{task} {task}
@ -818,7 +975,7 @@ steps:
- Step Iteration: {step_iteration}(このステップの実行回数) - Step Iteration: {step_iteration}(このステップの実行回数)
- Step: qa_review (QA専門レビュー) - Step: qa_review (QA専門レビュー)
- Report Directory: .takt/reports/{report_dir}/ - 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 ## Original User Request
{task} {task}
@ -978,7 +1135,7 @@ steps:
- Step: supervise (最終確認) - Step: supervise (最終確認)
- Report Directory: .takt/reports/{report_dir}/ - Report Directory: .takt/reports/{report_dir}/
- Report Files: - 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 - Summary: .takt/reports/{report_dir}/summary.md
## Original User Request ## Original User Request
@ -991,6 +1148,7 @@ steps:
## Previous Reviews Summary ## Previous Reviews Summary
このステップに到達したということは、以下のレビューがすべてAPPROVEされています このステップに到達したということは、以下のレビューがすべてAPPROVEされています
- Architecture Review: APPROVED
- CQRS+ES Review: APPROVED - CQRS+ES Review: APPROVED
- Frontend Review: APPROVED - Frontend Review: APPROVED
- AI Review: APPROVED - AI Review: APPROVED
@ -1054,6 +1212,7 @@ steps:
## レビュー結果 ## レビュー結果
| レビュー | 結果 | | レビュー | 結果 |
|---------|------| |---------|------|
| Architecture | ✅ APPROVE |
| CQRS+ES | ✅ APPROVE | | CQRS+ES | ✅ APPROVE |
| Frontend | ✅ APPROVE | | Frontend | ✅ APPROVE |
| AI Review | ✅ APPROVE | | AI Review | ✅ APPROVE |

View File

@ -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> = {}): 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');
});
});
});

View File

@ -219,7 +219,7 @@ program
const { execCwd, isWorktree } = await confirmAndCreateWorktree(cwd, task); const { execCwd, isWorktree } = await confirmAndCreateWorktree(cwd, task);
log.info('Starting task execution', { task, workflow: selectedWorkflow, worktree: isWorktree }); 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) { if (taskSuccess && isWorktree) {
const commitResult = autoCommitWorktree(execCwd, task); const commitResult = autoCommitWorktree(execCwd, task);

View File

@ -22,11 +22,16 @@ const log = createLogger('task');
/** /**
* Execute a single task with workflow * 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( export async function executeTask(
task: string, task: string,
cwd: string, cwd: string,
workflowName: string = DEFAULT_WORKFLOW_NAME workflowName: string = DEFAULT_WORKFLOW_NAME,
projectCwd?: string
): Promise<boolean> { ): Promise<boolean> {
const workflowConfig = loadWorkflow(workflowName); const workflowConfig = loadWorkflow(workflowName);
@ -42,7 +47,9 @@ export async function executeTask(
steps: workflowConfig.steps.map(s => s.name), 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; return result.success;
} }
@ -66,7 +73,8 @@ export async function executeAndCompleteTask(
try { try {
const { execCwd, execWorkflow, isWorktree } = resolveTaskExecution(task, cwd, workflowName); 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(); const completedAt = new Date().toISOString();
if (taskSuccess && isWorktree) { if (taskSuccess && isWorktree) {

View File

@ -56,6 +56,8 @@ export interface WorkflowExecutionResult {
export interface WorkflowExecutionOptions { export interface WorkflowExecutionOptions {
/** Header prefix for display */ /** Header prefix for display */
headerPrefix?: string; headerPrefix?: string;
/** Project root directory (where .takt/ lives). Defaults to cwd. */
projectCwd?: string;
} }
/** /**
@ -71,13 +73,16 @@ export async function executeWorkflow(
headerPrefix = 'Running Workflow:', headerPrefix = 'Running Workflow:',
} = options; } = options;
// projectCwd is where .takt/ lives (project root, not worktree)
const projectCwd = options.projectCwd ?? cwd;
// Always continue from previous sessions (use /clear to reset) // Always continue from previous sessions (use /clear to reset)
log.debug('Continuing session (use /clear to reset)'); log.debug('Continuing session (use /clear to reset)');
header(`${headerPrefix} ${workflowConfig.name}`); header(`${headerPrefix} ${workflowConfig.name}`);
const workflowSessionId = generateSessionId(); const workflowSessionId = generateSessionId();
const sessionLog = createSessionLog(task, cwd, workflowConfig.name); const sessionLog = createSessionLog(task, projectCwd, workflowConfig.name);
// Track current display for streaming // Track current display for streaming
const displayRef: { current: StreamDisplay | null } = { current: null }; const displayRef: { current: StreamDisplay | null } = { current: null };
@ -91,12 +96,12 @@ export async function executeWorkflow(
displayRef.current.createHandler()(event); displayRef.current.createHandler()(event);
}; };
// Load saved agent sessions for continuity // Load saved agent sessions for continuity (from project root)
const savedSessions = loadAgentSessions(cwd); 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 => { const sessionUpdateHandler = (agentName: string, agentSessionId: string): void => {
updateAgentSession(cwd, agentName, agentSessionId); updateAgentSession(projectCwd, agentName, agentSessionId);
}; };
const iterationLimitHandler = async ( const iterationLimitHandler = async (
@ -147,6 +152,7 @@ export async function executeWorkflow(
initialSessions: savedSessions, initialSessions: savedSessions,
onSessionUpdate: sessionUpdateHandler, onSessionUpdate: sessionUpdateHandler,
onIterationLimit: iterationLimitHandler, onIterationLimit: iterationLimitHandler,
projectCwd,
}); });
let abortReason: string | undefined; let abortReason: string | undefined;
@ -179,8 +185,8 @@ export async function executeWorkflow(
engine.on('workflow:complete', (state) => { engine.on('workflow:complete', (state) => {
log.info('Workflow completed successfully', { iterations: state.iteration }); log.info('Workflow completed successfully', { iterations: state.iteration });
finalizeSessionLog(sessionLog, 'completed'); finalizeSessionLog(sessionLog, 'completed');
// Save log to original cwd so user can find it easily // Save log to project root so user can find it easily
const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd); const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
const elapsed = sessionLog.endTime const elapsed = sessionLog.endTime
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime) ? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)
@ -200,8 +206,8 @@ export async function executeWorkflow(
} }
abortReason = reason; abortReason = reason;
finalizeSessionLog(sessionLog, 'aborted'); finalizeSessionLog(sessionLog, 'aborted');
// Save log to original cwd so user can find it easily // Save log to project root so user can find it easily
const logPath = saveSessionLog(sessionLog, workflowSessionId, cwd); const logPath = saveSessionLog(sessionLog, workflowSessionId, projectCwd);
const elapsed = sessionLog.endTime const elapsed = sessionLog.endTime
? formatElapsedTime(sessionLog.startTime, sessionLog.endTime) ? formatElapsedTime(sessionLog.startTime, sessionLog.endTime)

View File

@ -44,7 +44,7 @@ export { COMPLETE_STEP, ABORT_STEP } from './constants.js';
export class WorkflowEngine extends EventEmitter { export class WorkflowEngine extends EventEmitter {
private state: WorkflowState; private state: WorkflowState;
private config: WorkflowConfig; private config: WorkflowConfig;
private originalCwd: string; private projectCwd: string;
private cwd: string; private cwd: string;
private task: string; private task: string;
private options: WorkflowEngineOptions; private options: WorkflowEngineOptions;
@ -54,7 +54,7 @@ export class WorkflowEngine extends EventEmitter {
constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions = {}) { constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions = {}) {
super(); super();
this.config = config; this.config = config;
this.originalCwd = cwd; this.projectCwd = options.projectCwd ?? cwd;
this.cwd = cwd; this.cwd = cwd;
this.task = task; this.task = task;
this.options = options; 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 { private ensureReportDirExists(): void {
const reportDirPath = join(this.originalCwd, '.takt', 'reports', this.reportDir); const reportDirPath = join(this.projectCwd, '.takt', 'reports', this.reportDir);
if (!existsSync(reportDirPath)) { if (!existsSync(reportDirPath)) {
mkdirSync(reportDirPath, { recursive: true }); mkdirSync(reportDirPath, { recursive: true });
} }
@ -121,9 +121,9 @@ export class WorkflowEngine extends EventEmitter {
return this.cwd; return this.cwd;
} }
/** Get original working directory (for .takt data) */ /** Get project root directory (where .takt/ lives) */
getOriginalCwd(): string { getProjectCwd(): string {
return this.originalCwd; return this.projectCwd;
} }
/** Build instruction from template */ /** Build instruction from template */
@ -134,6 +134,7 @@ export class WorkflowEngine extends EventEmitter {
maxIterations: this.config.maxIterations, maxIterations: this.config.maxIterations,
stepIteration, stepIteration,
cwd: this.cwd, cwd: this.cwd,
projectCwd: this.projectCwd,
userInputs: this.state.userInputs, userInputs: this.state.userInputs,
previousOutput: getPreviousOutput(this.state), previousOutput: getPreviousOutput(this.state),
reportDir: this.reportDir, reportDir: this.reportDir,
@ -325,13 +326,3 @@ export class WorkflowEngine extends EventEmitter {
return { response, nextStep, isComplete, loopDetected: loopCheck.isLoop }; return { response, nextStep, isComplete, loopDetected: loopCheck.isLoop };
} }
} }
/** Create and run a workflow */
export async function executeWorkflow(
config: WorkflowConfig,
cwd: string,
task: string
): Promise<WorkflowState> {
const engine = new WorkflowEngine(config, cwd, task);
return engine.run();
}

View File

@ -6,7 +6,7 @@
*/ */
// Main engine // Main engine
export { WorkflowEngine, executeWorkflow } from './engine.js'; export { WorkflowEngine } from './engine.js';
// Constants // Constants
export { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from './constants.js'; export { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from './constants.js';

View File

@ -5,6 +5,7 @@
* template placeholders with actual values. * template placeholders with actual values.
*/ */
import { join } from 'node:path';
import type { WorkflowStep, AgentResponse } from '../models/types.js'; import type { WorkflowStep, AgentResponse } from '../models/types.js';
import { getGitDiff } from '../agents/runner.js'; import { getGitDiff } from '../agents/runner.js';
@ -20,8 +21,10 @@ export interface InstructionContext {
maxIterations: number; maxIterations: number;
/** Current step's iteration number (how many times this step has been executed) */ /** Current step's iteration number (how many times this step has been executed) */
stepIteration: number; stepIteration: number;
/** Working directory */ /** Working directory (agent work dir, may be a worktree) */
cwd: string; cwd: string;
/** Project root directory (where .takt/ lives). Defaults to cwd. */
projectCwd?: string;
/** User inputs accumulated during workflow */ /** User inputs accumulated during workflow */
userInputs: string[]; userInputs: string[];
/** Previous step output if available */ /** Previous step output if available */
@ -87,8 +90,14 @@ export function buildInstruction(
escapeTemplateChars(userInputsStr) 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) { 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); instruction = instruction.replace(/\{report_dir\}/g, context.reportDir);
} }

View File

@ -70,6 +70,8 @@ export interface WorkflowEngineOptions {
onIterationLimit?: IterationLimitCallback; onIterationLimit?: IterationLimitCallback;
/** Bypass all permission checks (sacrifice-my-pc mode) */ /** Bypass all permission checks (sacrifice-my-pc mode) */
bypassPermissions?: boolean; bypassPermissions?: boolean;
/** Project root directory (where .takt/ lives). Defaults to cwd if not specified. */
projectCwd?: string;
} }
/** Loop detection result */ /** Loop detection result */