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