worktree時にディレクトリを正しく読み込めるように修正
This commit is contained in:
parent
d612c412f9
commit
2c738d8009
@ -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の決定ログが妥当か検証する。**
|
||||
|
||||
|
||||
@ -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回目」)、回数に応じて判断を変える。
|
||||
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
| 構文エラー | ビルド・コンパイル |
|
||||
| テスト | テスト実行 |
|
||||
| 要求充足 | 元のタスク要求と照合 |
|
||||
| デッドコード | 変更・削除した機能を参照する未使用コードが残っていないか確認(未使用の関数、変数、インポート、エクスポート、型定義、到達不能コード) |
|
||||
|
||||
**すべて確認してから `[DONE]` を出力。**
|
||||
|
||||
|
||||
@ -336,7 +336,9 @@ steps:
|
||||
- 構造・設計の妥当性
|
||||
- コード品質
|
||||
- 変更スコープの適切性
|
||||
- **テストカバレッジ**: 実装に対応する単体テストが追加されているか
|
||||
- テストカバレッジ
|
||||
- デッドコード
|
||||
- 呼び出しチェーン検証
|
||||
|
||||
**レポート出力:** 上記の `Report File` に出力してください。
|
||||
- ファイルが存在しない場合: 新規作成
|
||||
@ -356,6 +358,8 @@ steps:
|
||||
- [x] コード品質
|
||||
- [x] 変更スコープ
|
||||
- [x] テストカバレッジ
|
||||
- [x] デッドコード
|
||||
- [x] 呼び出しチェーン検証
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | 場所 | 問題 | 修正案 |
|
||||
|
||||
@ -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 |
|
||||
|
||||
144
src/__tests__/instructionBuilder.test.ts
Normal file
144
src/__tests__/instructionBuilder.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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<boolean> {
|
||||
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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<WorkflowState> {
|
||||
const engine = new WorkflowEngine(config, cwd, task);
|
||||
return engine.run();
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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 */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user