refacotr
This commit is contained in:
parent
7d8ba10abb
commit
b944349d8f
361
docs/vertical-slice-migration-plan.md
Normal file
361
docs/vertical-slice-migration-plan.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
# Vertical Slice + Core ハイブリッド構成 移行計画(構造移動と import 整理)
|
||||||
|
|
||||||
|
## 目的
|
||||||
|
- `docs/vertical-slice-migration-map.md` に従い、機能追加なしで構造移動と import 整理を行うための作業計画を定義する。
|
||||||
|
- 既存の `src/` 構成と依存関係を把握し、移行対象・参照元・Public API の整理ポイントを明確化する。
|
||||||
|
|
||||||
|
## 前提
|
||||||
|
- 変更対象は `src/` と `docs/` の参照更新のみ。
|
||||||
|
- 実装は行わず、移行の具体手順と注意点を記載する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 現状の `src/` 構成(トップレベル)
|
||||||
|
- `src/app/cli/*`
|
||||||
|
- `src/features/*`
|
||||||
|
- `src/core/*`
|
||||||
|
- `src/infra/*`
|
||||||
|
- `src/shared/*`
|
||||||
|
- その他: `src/agents/*`, `src/index.ts`
|
||||||
|
|
||||||
|
## 現状の依存関係(観測ベース)
|
||||||
|
- `app` -> `features`, `infra`, `shared`
|
||||||
|
- `features` -> `core`, `infra`, `shared`(`shared/prompt`, `shared/constants`, `shared/context`, `shared/exitCodes`)
|
||||||
|
- `infra` -> `core/models`, `shared`
|
||||||
|
- `core` -> `shared`, `agents`
|
||||||
|
- `agents` -> `infra`, `shared`, `core/models`, `infra/claude`
|
||||||
|
|
||||||
|
### 依存ルールとの差分(注意点)
|
||||||
|
- `core` が `shared` と `agents` に依存している(`core` は外側に依存しない想定)。
|
||||||
|
- `agents` が `infra` に依存しているため、`core -> agents -> infra` の依存経路が発生している。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 移動対象の分類と移動先(確定)
|
||||||
|
|
||||||
|
### core
|
||||||
|
| 現在のパス | 移動先 | 備考 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/core/models/*` | `src/core/models/*` | 既に配置済み |
|
||||||
|
| `src/core/workflow/*` | `src/core/workflow/*` | 既に配置済み |
|
||||||
|
|
||||||
|
### infra
|
||||||
|
| 現在のパス | 移動先 | 備考 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/infra/providers/*` | `src/infra/providers/*` | 既に配置済み |
|
||||||
|
| `src/infra/github/*` | `src/infra/github/*` | 既に配置済み |
|
||||||
|
| `src/infra/config/*` | `src/infra/config/*` | 既に配置済み |
|
||||||
|
| `src/infra/task/*` | `src/infra/task/*` | 既に配置済み |
|
||||||
|
| `src/infra/fs/session.ts` | `src/infra/fs/session.ts` | 既に配置済み |
|
||||||
|
| `src/infra/claude/*` | `src/infra/claude/*` | 外部API連携のため infra に集約 |
|
||||||
|
| `src/infra/codex/*` | `src/infra/codex/*` | 外部API連携のため infra に集約 |
|
||||||
|
| `src/infra/mock/*` | `src/infra/mock/*` | Provider 用 mock のため infra に集約 |
|
||||||
|
| `src/infra/resources/*` | `src/infra/resources/*` | FS 依存を含むため infra に集約 |
|
||||||
|
|
||||||
|
### features
|
||||||
|
| 現在のパス | 移動先 | 備考 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/features/tasks/*` | `src/features/tasks/*` | 既に配置済み |
|
||||||
|
| `src/features/pipeline/*` | `src/features/pipeline/*` | 既に配置済み |
|
||||||
|
| `src/features/config/*` | `src/features/config/*` | 既に配置済み |
|
||||||
|
| `src/features/interactive/*` | `src/features/interactive/*` | 既に配置済み |
|
||||||
|
|
||||||
|
### app
|
||||||
|
| 現在のパス | 移動先 | 備考 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/cli/*` | `src/app/cli/*` | 既に配置済み |
|
||||||
|
|
||||||
|
### shared
|
||||||
|
| 現在のパス | 移動先 | 備考 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/shared/utils/*` | `src/shared/utils/*` | 既に配置済み |
|
||||||
|
| `src/shared/ui/*` | `src/shared/ui/*` | 既に配置済み |
|
||||||
|
| `src/shared/prompt/*` | `src/shared/prompt/*` | 共有UIユーティリティとして集約 |
|
||||||
|
| `src/shared/constants.ts` | `src/shared/constants.ts` | 共有定数として集約 |
|
||||||
|
| `src/shared/context.ts` | `src/shared/context.ts` | 共有コンテキストとして集約 |
|
||||||
|
| `src/shared/exitCodes.ts` | `src/shared/exitCodes.ts` | 共有エラーコードとして集約 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 移行対象と参照元の洗い出し(現状)
|
||||||
|
|
||||||
|
### core/models の参照元
|
||||||
|
- `src/infra/claude/client.ts`
|
||||||
|
- `src/infra/codex/client.ts`
|
||||||
|
- `src/agents/runner.ts`
|
||||||
|
- `src/agents/types.ts`
|
||||||
|
- `src/infra/config/global/globalConfig.ts`
|
||||||
|
- `src/infra/config/global/initialization.ts`
|
||||||
|
- `src/infra/config/loaders/agentLoader.ts`
|
||||||
|
- `src/infra/config/loaders/workflowParser.ts`
|
||||||
|
- `src/infra/config/loaders/workflowResolver.ts`
|
||||||
|
- `src/infra/config/paths.ts`
|
||||||
|
- `src/infra/providers/*`
|
||||||
|
- `src/features/interactive/interactive.ts`
|
||||||
|
- `src/features/tasks/execute/*`
|
||||||
|
- `src/features/pipeline/execute.ts`
|
||||||
|
- `src/shared/utils/debug.ts`
|
||||||
|
- `src/shared/constants.ts`
|
||||||
|
- `src/infra/resources/index.ts`
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
### core/workflow の参照元
|
||||||
|
- `src/features/tasks/execute/workflowExecution.ts`
|
||||||
|
- `src/__tests__/engine-*.test.ts`
|
||||||
|
- `src/__tests__/instructionBuilder.test.ts`
|
||||||
|
- `src/__tests__/it-*.test.ts`
|
||||||
|
- `src/__tests__/parallel-logger.test.ts`
|
||||||
|
- `src/__tests__/transitions.test.ts`
|
||||||
|
|
||||||
|
### infra/config の参照元
|
||||||
|
- `src/app/cli/*`
|
||||||
|
- `src/agents/runner.ts`
|
||||||
|
- `src/features/interactive/interactive.ts`
|
||||||
|
- `src/features/config/*`
|
||||||
|
- `src/features/tasks/execute/*`
|
||||||
|
- `src/features/tasks/add/index.ts`
|
||||||
|
- `src/features/tasks/list/taskActions.ts`
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
### infra/task の参照元
|
||||||
|
- `src/features/tasks/*`
|
||||||
|
- `src/features/pipeline/execute.ts`
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
### infra/github の参照元
|
||||||
|
- `src/app/cli/routing.ts`
|
||||||
|
- `src/features/pipeline/execute.ts`
|
||||||
|
- `src/features/tasks/execute/selectAndExecute.ts`
|
||||||
|
- `src/features/tasks/add/index.ts`
|
||||||
|
- `src/__tests__/github-*.test.ts`
|
||||||
|
|
||||||
|
### infra/providers の参照元
|
||||||
|
- `src/agents/runner.ts`
|
||||||
|
- `src/features/interactive/interactive.ts`
|
||||||
|
- `src/features/tasks/add/index.ts`
|
||||||
|
- `src/features/tasks/execute/types.ts`
|
||||||
|
- `src/__tests__/addTask.test.ts`
|
||||||
|
- `src/__tests__/interactive.test.ts`
|
||||||
|
- `src/__tests__/summarize.test.ts`
|
||||||
|
|
||||||
|
### infra/fs の参照元
|
||||||
|
- `src/features/tasks/execute/workflowExecution.ts`
|
||||||
|
- `src/__tests__/session.test.ts`
|
||||||
|
- `src/__tests__/utils.test.ts`
|
||||||
|
|
||||||
|
### shared/utils の参照元
|
||||||
|
- `src/app/cli/*`
|
||||||
|
- `src/infra/*`
|
||||||
|
- `src/features/*`
|
||||||
|
- `src/agents/runner.ts`
|
||||||
|
- `src/infra/claude/*`
|
||||||
|
- `src/infra/codex/*`
|
||||||
|
- `src/core/workflow/*`
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
### shared/ui の参照元
|
||||||
|
- `src/app/cli/*`
|
||||||
|
- `src/infra/task/display.ts`
|
||||||
|
- `src/features/*`
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
### shared/prompt の参照元
|
||||||
|
- `src/features/*`
|
||||||
|
- `src/infra/config/global/initialization.ts`
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
### shared/constants・shared/context・shared/exitCodes の参照元
|
||||||
|
- `src/features/*`
|
||||||
|
- `src/infra/config/global/*`
|
||||||
|
- `src/core/models/schemas.ts`
|
||||||
|
- `src/core/workflow/engine/*`
|
||||||
|
- `src/app/cli/routing.ts`
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Public API(index.ts)整理ポイント
|
||||||
|
|
||||||
|
### 既存 Public API
|
||||||
|
- `src/core/models/index.ts`
|
||||||
|
- `src/core/workflow/index.ts`
|
||||||
|
- `src/features/tasks/index.ts`
|
||||||
|
- `src/features/pipeline/index.ts`
|
||||||
|
- `src/features/config/index.ts`
|
||||||
|
- `src/features/interactive/index.ts`
|
||||||
|
- `src/infra/config/index.ts`
|
||||||
|
- `src/infra/task/index.ts`
|
||||||
|
- `src/infra/providers/index.ts`
|
||||||
|
- `src/shared/utils/index.ts`
|
||||||
|
- `src/shared/ui/index.ts`
|
||||||
|
- `src/index.ts`
|
||||||
|
|
||||||
|
### 新設/拡張が必要な Public API
|
||||||
|
- `src/infra/github/index.ts`(`issue.ts`, `pr.ts`, `types.ts` の集約)
|
||||||
|
- `src/infra/fs/index.ts`(`session.ts` の集約)
|
||||||
|
- `src/infra/resources/index.ts`(resources API の集約)
|
||||||
|
- `src/infra/config/index.ts` の拡張(`globalConfig`, `projectConfig`, `workflowLoader` などの再エクスポート)
|
||||||
|
- `src/shared/prompt/index.ts`(共通プロンプトの入口)
|
||||||
|
- `src/shared/constants.ts`, `src/shared/context.ts`, `src/shared/exitCodes.ts` の Public API 反映
|
||||||
|
- `src/infra/claude/index.ts`, `src/infra/codex/index.ts`, `src/infra/mock/index.ts`(移動後の入口)
|
||||||
|
|
||||||
|
### 深い import 禁止の置換方針
|
||||||
|
- `core/*` と `features/*` は Public API(`index.ts`)からのみ import。
|
||||||
|
- `features` から `infra` の deep import を廃止し、`infra/*/index.ts` 経由に置換。
|
||||||
|
- `app/cli` から `infra` への direct import は必要最小限に限定し、可能なら `features` Public API に集約。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 影響範囲一覧
|
||||||
|
|
||||||
|
### CLI エントリ
|
||||||
|
- `src/app/cli/index.ts`
|
||||||
|
- `src/app/cli/program.ts`
|
||||||
|
- `src/app/cli/commands.ts`
|
||||||
|
- `src/app/cli/routing.ts`
|
||||||
|
- `src/app/cli/helpers.ts`
|
||||||
|
- `bin/takt`
|
||||||
|
|
||||||
|
### features 呼び出し
|
||||||
|
- `src/features/tasks/*`
|
||||||
|
- `src/features/pipeline/*`
|
||||||
|
- `src/features/config/*`
|
||||||
|
- `src/features/interactive/*`
|
||||||
|
|
||||||
|
### docs 参照更新対象
|
||||||
|
- `docs/data-flow.md`
|
||||||
|
- `docs/data-flow-diagrams.md`
|
||||||
|
- `docs/agents.md`
|
||||||
|
- `docs/workflows.md`
|
||||||
|
- `docs/README.ja.md`
|
||||||
|
|
||||||
|
### テスト
|
||||||
|
- `src/__tests__/*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 実施手順(推奨順序)
|
||||||
|
|
||||||
|
### 1. core
|
||||||
|
- `core/workflow` と `core/models` の Public API を点検し、外部参照を `index.ts` 経由に統一。
|
||||||
|
- `core` 内での `shared` 依存を整理する(ログ/エラー/レポート生成の配置を明確化)。
|
||||||
|
- `agents` 依存の扱いを決定し、依存方向を破らない構成に合わせて移動計画を確定する。
|
||||||
|
|
||||||
|
### 2. infra
|
||||||
|
- `infra/github` と `infra/fs` の `index.ts` を新設し、deep import を解消する前提の API を定義。
|
||||||
|
- `infra/config/index.ts` の再エクスポート対象を拡張し、`globalConfig`・`projectConfig`・`workflowLoader` 等を Public API 化。
|
||||||
|
- `claude/codex/mock/resources` を `infra` 配下に移動し、参照を更新する。
|
||||||
|
|
||||||
|
### 3. features
|
||||||
|
- `features` から `infra` への deep import を Public API 経由に置換。
|
||||||
|
- `prompt` の移動に合わせ、`features` 内の import を `shared/prompt` に変更。
|
||||||
|
- `constants/context/exitCodes` の移動に合わせて参照を更新。
|
||||||
|
|
||||||
|
### 4. app
|
||||||
|
- `app/cli` から `features` Public API のみを使用する形に整理。
|
||||||
|
- `app/cli` から `infra` へ直接参照している箇所は、必要に応じて `features` 経由に寄せる。
|
||||||
|
|
||||||
|
### 5. Public API
|
||||||
|
- `src/index.ts` の再エクスポート対象を新パスに合わせて更新。
|
||||||
|
- 新設した `index.ts` のエクスポート整合を確認する。
|
||||||
|
|
||||||
|
### 6. docs
|
||||||
|
- `docs/data-flow.md` など、`src/` 参照を新パスに合わせて更新。
|
||||||
|
- 参照パスが `index.ts` の Public API 方針に沿っているか点検。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 判断ポイント
|
||||||
|
- `src/models/workflow.ts` が追加される場合、
|
||||||
|
- **廃止**するか、
|
||||||
|
- **`core/models/index.ts` へ統合**するかを決める。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 再開指示(2026-02-02 時点の差分観測ベース)
|
||||||
|
|
||||||
|
### 現在のブランチ
|
||||||
|
- `refactoring`
|
||||||
|
|
||||||
|
### 進捗(差分ベースの整理)
|
||||||
|
#### core
|
||||||
|
- `src/core/models/*` と `src/core/workflow/*` が広範囲に変更されている。
|
||||||
|
|
||||||
|
#### infra
|
||||||
|
- 既存: `src/infra/config/*`, `src/infra/providers/*`, `src/infra/task/*`, `src/infra/github/*`, `src/infra/fs/session.ts` が更新されている。
|
||||||
|
- 追加: `src/infra/claude/*`, `src/infra/codex/*`, `src/infra/mock/*`, `src/infra/resources/*` が新規追加されている。
|
||||||
|
- 追加: `src/infra/github/index.ts`, `src/infra/fs/index.ts` が新規追加されている。
|
||||||
|
|
||||||
|
#### features
|
||||||
|
- `src/features/*` が広範囲に変更されている。
|
||||||
|
|
||||||
|
#### app
|
||||||
|
- `src/app/cli/*` が変更されている。
|
||||||
|
|
||||||
|
#### shared
|
||||||
|
- `src/shared/utils/index.ts` と `src/shared/ui/StreamDisplay.ts` が更新されている。
|
||||||
|
- `src/shared/prompt/*`, `src/shared/constants.ts`, `src/shared/context.ts`, `src/shared/exitCodes.ts` が新規追加されている。
|
||||||
|
|
||||||
|
#### 削除された旧パス
|
||||||
|
- `src/claude/*`, `src/codex/*`, `src/mock/*`, `src/prompt/*`, `src/resources/index.ts`
|
||||||
|
- `src/constants.ts`, `src/context.ts`, `src/exitCodes.ts`
|
||||||
|
|
||||||
|
#### tests
|
||||||
|
- `src/__tests__/*` が広範囲に更新されている。
|
||||||
|
|
||||||
|
#### resources
|
||||||
|
- `resources/global/{en,ja}/*` に更新があるため、移行作業とは独立して取り扱う。
|
||||||
|
|
||||||
|
#### docs
|
||||||
|
- `docs/vertical-slice-migration-plan.md` が未追跡ファイルとして存在する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 未完了セクション(要確認事項)
|
||||||
|
以下は差分観測のみでは断定できないため、再開時に確認する。
|
||||||
|
|
||||||
|
### core
|
||||||
|
- `core` から外部層(`shared` / `agents`)への依存が残っていないか確認する。
|
||||||
|
- `core/models` と `core/workflow` の Public API が `index.ts` 経由に統一されているか点検する。
|
||||||
|
|
||||||
|
### infra
|
||||||
|
- `infra/github/index.ts`, `infra/fs/index.ts`, `infra/resources/index.ts` の再エクスポート範囲を確定する。
|
||||||
|
- `infra/config/index.ts` の再エクスポート対象(`globalConfig`, `projectConfig`, `workflowLoader` 等)が揃っているか確認する。
|
||||||
|
- `infra/claude`, `infra/codex`, `infra/mock` の Public API が `index.ts` に統一されているか確認する。
|
||||||
|
|
||||||
|
### features
|
||||||
|
- `features` から `infra` への deep import が残っていないか確認する。
|
||||||
|
- `shared/prompt`, `shared/constants`, `shared/context`, `shared/exitCodes` への参照統一が完了しているか確認する。
|
||||||
|
|
||||||
|
### app
|
||||||
|
- `app/cli` が `features` Public API 経由に統一されているか確認する。
|
||||||
|
- `app/cli` から `infra` への direct import が残っていないか確認する。
|
||||||
|
|
||||||
|
### Public API
|
||||||
|
- `src/index.ts` の再エクスポートが新パスに揃っているか確認する。
|
||||||
|
- `infra`/`shared`/`features` の `index.ts` 追加分を反映できているか点検する。
|
||||||
|
|
||||||
|
### docs
|
||||||
|
- `docs/*` の参照パスを新構成(Public API)へ更新する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 判断ポイント(再掲)
|
||||||
|
- `src/models/workflow.ts` は直近コミットで削除されているため、
|
||||||
|
- 廃止のまま進めるか、
|
||||||
|
- `core/models/index.ts` へ統合して復活させるかを確定する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参照更新の対象一覧(docs)
|
||||||
|
- `docs/data-flow.md`
|
||||||
|
- `docs/data-flow-diagrams.md`
|
||||||
|
- `docs/agents.md`
|
||||||
|
- `docs/workflows.md`
|
||||||
|
- `docs/README.ja.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 付記
|
||||||
|
- ここに記載した移動は、既存の機能追加なしで行うこと。
|
||||||
|
- 実装時は `core -> infra -> features -> app -> Public API -> docs` の順序を厳守する。
|
||||||
58
report/plan.md
Normal file
58
report/plan.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# 実装計画: Vertical Slice + Core ハイブリッド構成移行の検証・修正
|
||||||
|
|
||||||
|
## 現状分析
|
||||||
|
|
||||||
|
### ビルド・テスト状況
|
||||||
|
- **ビルド (`npm run build`)**: ✅ パス(エラーなし)
|
||||||
|
- **テスト (`npm test`)**: ✅ 全54ファイル、802テストパス
|
||||||
|
|
||||||
|
### レビュー結果サマリ
|
||||||
|
|
||||||
|
| 観点 | 結果 | 詳細 |
|
||||||
|
|------|------|------|
|
||||||
|
| 依存方向の違反 | ✅ 違反なし | core→infra/features/app なし、features→app なし、infra→features/app なし |
|
||||||
|
| 旧パスの残留 | ✅ 残留なし | src/claude/, src/codex/, src/mock/, src/prompt/, src/resources/, src/constants.ts, src/context.ts, src/exitCodes.ts への参照なし |
|
||||||
|
| docs参照パスの整合 | ✅ 問題なし | 5ファイルすべて新構成のパスに更新済み |
|
||||||
|
| Public API (index.ts) 経由の統一 | ⚠️ 修正必要 | プロダクションコードに4箇所のdeep import違反あり |
|
||||||
|
|
||||||
|
## 修正が必要な箇所
|
||||||
|
|
||||||
|
### Public API (index.ts) 経由の統一 — deep import 違反(プロダクションコード4箇所)
|
||||||
|
|
||||||
|
#### 1. `src/infra/claude/types.ts`
|
||||||
|
- **現状**: `import type { PermissionResult } from '../../core/workflow/types.js'`
|
||||||
|
- **問題**: `core/workflow/index.ts` を経由せず直接 `types.js` を参照
|
||||||
|
- **対応**: `PermissionResult` を `core/workflow/index.ts` からエクスポートし、import パスを `../../core/workflow/index.js` に変更
|
||||||
|
|
||||||
|
#### 2. `src/shared/ui/StreamDisplay.ts`
|
||||||
|
- **現状**: `import type { StreamEvent, StreamCallback } from '../../core/workflow/types.js'`
|
||||||
|
- **問題**: `core/workflow/index.ts` を経由せず直接 `types.js` を参照(これらの型は既にindex.tsでエクスポート済み)
|
||||||
|
- **対応**: import パスを `../../core/workflow/index.js` に変更
|
||||||
|
|
||||||
|
#### 3. `src/features/config/switchConfig.ts`(2箇所)
|
||||||
|
- **現状**: `import type { PermissionMode } from '../../infra/config/types.js'` および `export type { PermissionMode } from '../../infra/config/types.js'`
|
||||||
|
- **問題**: `infra/config/index.ts` を経由せず直接 `types.js` を参照
|
||||||
|
- **対応**: `PermissionMode` を `infra/config/index.ts` からエクスポートし、import/export パスを `../../infra/config/index.js` に変更
|
||||||
|
|
||||||
|
### テストコードの deep import(33箇所)
|
||||||
|
- テストコードはモジュール内部を直接テストする性質上、deep import は許容範囲
|
||||||
|
- **今回は修正対象外とする**(機能追加しない制約に基づき、テストの構造変更は行わない)
|
||||||
|
|
||||||
|
## 実装手順
|
||||||
|
|
||||||
|
### Step 1: core/workflow の Public API 修正
|
||||||
|
1. `src/core/workflow/index.ts` を確認し、`PermissionResult` をエクスポートに追加
|
||||||
|
2. `src/infra/claude/types.ts` の import パスを `../../core/workflow/index.js` に変更
|
||||||
|
3. `src/shared/ui/StreamDisplay.ts` の import パスを `../../core/workflow/index.js` に変更
|
||||||
|
|
||||||
|
### Step 2: infra/config の Public API 修正
|
||||||
|
1. `src/infra/config/index.ts` を確認し、`PermissionMode` をエクスポートに追加
|
||||||
|
2. `src/features/config/switchConfig.ts` の import/export パスを `../../infra/config/index.js` に変更
|
||||||
|
|
||||||
|
### Step 3: 最終確認
|
||||||
|
1. `npm run build` でビルド確認
|
||||||
|
2. `npm test` でテスト確認
|
||||||
|
|
||||||
|
## 制約の確認
|
||||||
|
- ✅ 機能追加は行わない(import パスとエクスポートの整理のみ)
|
||||||
|
- ✅ 変更対象は `src/` のみ(`docs/` は変更不要)
|
||||||
@ -73,3 +73,4 @@ Determine the implementation direction:
|
|||||||
**Keep analysis simple.** Overly detailed plans are unnecessary. Provide enough direction for Coder to proceed with implementation.
|
**Keep analysis simple.** Overly detailed plans are unnecessary. Provide enough direction for Coder to proceed with implementation.
|
||||||
|
|
||||||
**Make unclear points explicit.** Don't proceed with guesses, report unclear points.
|
**Make unclear points explicit.** Don't proceed with guesses, report unclear points.
|
||||||
|
**Ask all clarification questions at once.** Do not ask follow-up questions in multiple rounds.
|
||||||
|
|||||||
@ -42,3 +42,4 @@ You are a planning specialist. Analyze tasks and design implementation plans.
|
|||||||
- **Do not plan based on assumptions** — Always read the code to verify
|
- **Do not plan based on assumptions** — Always read the code to verify
|
||||||
- **Be specific** — Specify file names, function names, and change details
|
- **Be specific** — Specify file names, function names, and change details
|
||||||
- **Ask when uncertain** — Do not proceed with ambiguity
|
- **Ask when uncertain** — Do not proceed with ambiguity
|
||||||
|
- **Ask all questions at once** — Avoid multiple rounds of follow-up questions
|
||||||
|
|||||||
@ -3,5 +3,6 @@ You are a task summarizer. Convert the conversation into a concrete task instruc
|
|||||||
Requirements:
|
Requirements:
|
||||||
- Output only the final task instruction (no preamble).
|
- Output only the final task instruction (no preamble).
|
||||||
- Be specific about scope and targets (files/modules) if mentioned.
|
- Be specific about scope and targets (files/modules) if mentioned.
|
||||||
- Preserve constraints and "do not" instructions.
|
- Preserve user-provided constraints and "do not" instructions.
|
||||||
|
- Do NOT include assistant/system operational constraints (tool limits, execution prohibitions).
|
||||||
- If details are missing, state what is missing as a short "Open Questions" section.
|
- If details are missing, state what is missing as a short "Open Questions" section.
|
||||||
|
|||||||
@ -104,9 +104,13 @@ steps:
|
|||||||
- condition: Implementation complete
|
- condition: Implementation complete
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: No implementation (report only)
|
- condition: No implementation (report only)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: Cannot proceed, insufficient info
|
- condition: Cannot proceed, insufficient info
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: User input required
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
instruction_template: |
|
instruction_template: |
|
||||||
Follow the plan from the plan step and implement.
|
Follow the plan from the plan step and implement.
|
||||||
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
||||||
@ -151,7 +155,6 @@ steps:
|
|||||||
- {command and outcome}
|
- {command and outcome}
|
||||||
|
|
||||||
**No-implementation handling (required)**
|
**No-implementation handling (required)**
|
||||||
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
|
|
||||||
|
|
||||||
- name: ai_review
|
- name: ai_review
|
||||||
edit: false
|
edit: false
|
||||||
@ -396,6 +399,17 @@ steps:
|
|||||||
Address the feedback from the reviewers.
|
Address the feedback from the reviewers.
|
||||||
The "Original User Request" is reference information, not the latest instruction.
|
The "Original User Request" is reference information, not the latest instruction.
|
||||||
Review the session conversation history and fix the issues raised by the reviewers.
|
Review the session conversation history and fix the issues raised by the reviewers.
|
||||||
|
|
||||||
|
|
||||||
|
**Required output (include headings)**
|
||||||
|
## Work done
|
||||||
|
- {summary of work performed}
|
||||||
|
## Changes made
|
||||||
|
- {summary of code changes}
|
||||||
|
## Test results
|
||||||
|
- {command and outcome}
|
||||||
|
## Evidence
|
||||||
|
- {key files/grep/diff/log evidence you verified}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
|
|
||||||
- name: supervise
|
- name: supervise
|
||||||
|
|||||||
@ -144,9 +144,13 @@ steps:
|
|||||||
- condition: Implementation is complete
|
- condition: Implementation is complete
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: No implementation (report only)
|
- condition: No implementation (report only)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: Cannot proceed with implementation
|
- condition: Cannot proceed with implementation
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: User input required
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Phase 2: AI Review
|
# Phase 2: AI Review
|
||||||
@ -258,7 +262,6 @@ steps:
|
|||||||
- {command and outcome}
|
- {command and outcome}
|
||||||
|
|
||||||
**No-implementation handling (required)**
|
**No-implementation handling (required)**
|
||||||
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
|
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: AI Reviewer's issues have been fixed
|
- condition: AI Reviewer's issues have been fixed
|
||||||
@ -509,6 +512,17 @@ steps:
|
|||||||
Address the feedback from the reviewers.
|
Address the feedback from the reviewers.
|
||||||
The "Original User Request" is reference information, not the latest instruction.
|
The "Original User Request" is reference information, not the latest instruction.
|
||||||
Review the session conversation history and fix the issues raised by the reviewers.
|
Review the session conversation history and fix the issues raised by the reviewers.
|
||||||
|
|
||||||
|
|
||||||
|
**Required output (include headings)**
|
||||||
|
## Work done
|
||||||
|
- {summary of work performed}
|
||||||
|
## Changes made
|
||||||
|
- {summary of code changes}
|
||||||
|
## Test results
|
||||||
|
- {command and outcome}
|
||||||
|
## Evidence
|
||||||
|
- {key files/grep/diff/log evidence you verified}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@ -624,6 +638,17 @@ steps:
|
|||||||
|
|
||||||
The supervisor has identified issues from a big-picture perspective.
|
The supervisor has identified issues from a big-picture perspective.
|
||||||
Address items in priority order.
|
Address items in priority order.
|
||||||
|
|
||||||
|
|
||||||
|
**Required output (include headings)**
|
||||||
|
## Work done
|
||||||
|
- {summary of work performed}
|
||||||
|
## Changes made
|
||||||
|
- {summary of code changes}
|
||||||
|
## Test results
|
||||||
|
- {command and outcome}
|
||||||
|
## Evidence
|
||||||
|
- {key files/grep/diff/log evidence you verified}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: Supervisor's issues have been fixed
|
- condition: Supervisor's issues have been fixed
|
||||||
|
|||||||
@ -156,9 +156,13 @@ steps:
|
|||||||
- condition: Implementation is complete
|
- condition: Implementation is complete
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: No implementation (report only)
|
- condition: No implementation (report only)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: Cannot proceed with implementation
|
- condition: Cannot proceed with implementation
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: User input required
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Phase 2: AI Review
|
# Phase 2: AI Review
|
||||||
@ -271,7 +275,6 @@ steps:
|
|||||||
- {command and outcome}
|
- {command and outcome}
|
||||||
|
|
||||||
**No-implementation handling (required)**
|
**No-implementation handling (required)**
|
||||||
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
|
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: AI Reviewer's issues have been fixed
|
- condition: AI Reviewer's issues have been fixed
|
||||||
@ -522,6 +525,17 @@ steps:
|
|||||||
Address the feedback from the reviewers.
|
Address the feedback from the reviewers.
|
||||||
The "Original User Request" is reference information, not the latest instruction.
|
The "Original User Request" is reference information, not the latest instruction.
|
||||||
Review the session conversation history and fix the issues raised by the reviewers.
|
Review the session conversation history and fix the issues raised by the reviewers.
|
||||||
|
|
||||||
|
|
||||||
|
**Required output (include headings)**
|
||||||
|
## Work done
|
||||||
|
- {summary of work performed}
|
||||||
|
## Changes made
|
||||||
|
- {summary of code changes}
|
||||||
|
## Test results
|
||||||
|
- {command and outcome}
|
||||||
|
## Evidence
|
||||||
|
- {key files/grep/diff/log evidence you verified}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@ -637,6 +651,17 @@ steps:
|
|||||||
|
|
||||||
The supervisor has identified issues from a big-picture perspective.
|
The supervisor has identified issues from a big-picture perspective.
|
||||||
Address items in priority order.
|
Address items in priority order.
|
||||||
|
|
||||||
|
|
||||||
|
**Required output (include headings)**
|
||||||
|
## Work done
|
||||||
|
- {summary of work performed}
|
||||||
|
## Changes made
|
||||||
|
- {summary of code changes}
|
||||||
|
## Test results
|
||||||
|
- {command and outcome}
|
||||||
|
## Evidence
|
||||||
|
- {key files/grep/diff/log evidence you verified}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: Supervisor's issues have been fixed
|
- condition: Supervisor's issues have been fixed
|
||||||
|
|||||||
@ -100,9 +100,13 @@ steps:
|
|||||||
- condition: Implementation complete
|
- condition: Implementation complete
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: No implementation (report only)
|
- condition: No implementation (report only)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: Cannot proceed, insufficient info
|
- condition: Cannot proceed, insufficient info
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: User input required
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
instruction_template: |
|
instruction_template: |
|
||||||
Follow the plan from the plan step and implement.
|
Follow the plan from the plan step and implement.
|
||||||
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
||||||
@ -147,7 +151,6 @@ steps:
|
|||||||
- {command and outcome}
|
- {command and outcome}
|
||||||
|
|
||||||
**No-implementation handling (required)**
|
**No-implementation handling (required)**
|
||||||
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
|
|
||||||
|
|
||||||
**Required output (include headings)**
|
**Required output (include headings)**
|
||||||
## Work done
|
## Work done
|
||||||
|
|||||||
@ -73,3 +73,4 @@
|
|||||||
**シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。
|
**シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。
|
||||||
|
|
||||||
**不明点は明確にする。** 推測で進めず、不明点を報告する。
|
**不明点は明確にする。** 推測で進めず、不明点を報告する。
|
||||||
|
**確認が必要な場合は質問を一度にまとめる。** 追加の確認質問を繰り返さない。
|
||||||
|
|||||||
@ -42,3 +42,4 @@
|
|||||||
- **推測で計画を立てない** — 必ずコードを読んで確認する
|
- **推測で計画を立てない** — 必ずコードを読んで確認する
|
||||||
- **計画は具体的に** — ファイル名、関数名、変更内容を明示する
|
- **計画は具体的に** — ファイル名、関数名、変更内容を明示する
|
||||||
- **判断に迷ったら質問する** — 曖昧なまま進めない
|
- **判断に迷ったら質問する** — 曖昧なまま進めない
|
||||||
|
- **質問は一度にまとめる** — 追加の確認質問を繰り返さない
|
||||||
|
|||||||
@ -3,5 +3,6 @@
|
|||||||
要件:
|
要件:
|
||||||
- 出力は最終的な指示のみ(前置き不要)
|
- 出力は最終的な指示のみ(前置き不要)
|
||||||
- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く
|
- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く
|
||||||
- 制約や「やらないこと」を保持する
|
- ユーザー由来の制約や「やらないこと」は保持する
|
||||||
|
- アシスタントの運用上の制約(実行禁止/ツール制限など)は指示に含めない
|
||||||
- 情報不足があれば「Open Questions」セクションを短く付ける
|
- 情報不足があれば「Open Questions」セクションを短く付ける
|
||||||
|
|||||||
@ -95,9 +95,13 @@ steps:
|
|||||||
- condition: 実装完了
|
- condition: 実装完了
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: 実装未着手(レポートのみ)
|
- condition: 実装未着手(レポートのみ)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: 判断できない、情報不足
|
- condition: 判断できない、情報不足
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: ユーザー入力が必要
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
instruction_template: |
|
instruction_template: |
|
||||||
planステップで立てた計画に従って実装してください。
|
planステップで立てた計画に従って実装してください。
|
||||||
計画レポート({report:00-plan.md})を参照し、実装を進めてください。
|
計画レポート({report:00-plan.md})を参照し、実装を進めてください。
|
||||||
@ -146,8 +150,6 @@ steps:
|
|||||||
## テスト結果
|
## テスト結果
|
||||||
- {実行コマンドと結果}
|
- {実行コマンドと結果}
|
||||||
|
|
||||||
**実装未着手の扱い(必須)**
|
|
||||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
|
||||||
|
|
||||||
- name: ai_review
|
- name: ai_review
|
||||||
edit: false
|
edit: false
|
||||||
@ -402,6 +404,17 @@ steps:
|
|||||||
instruction_template: |
|
instruction_template: |
|
||||||
レビュアーのフィードバックに対応してください。
|
レビュアーのフィードバックに対応してください。
|
||||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||||
|
|
||||||
|
|
||||||
|
**必須出力(見出しを含める)**
|
||||||
|
## 作業結果
|
||||||
|
- {実施内容の要約}
|
||||||
|
## 変更内容
|
||||||
|
- {変更内容の要約}
|
||||||
|
## テスト結果
|
||||||
|
- {実行コマンドと結果}
|
||||||
|
## 証拠
|
||||||
|
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
|
|
||||||
- name: supervise
|
- name: supervise
|
||||||
|
|||||||
@ -153,9 +153,13 @@ steps:
|
|||||||
- condition: 実装が完了した
|
- condition: 実装が完了した
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: 実装未着手(レポートのみ)
|
- condition: 実装未着手(レポートのみ)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: 実装を進行できない
|
- condition: 実装を進行できない
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: ユーザー入力が必要
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Phase 2: AI Review
|
# Phase 2: AI Review
|
||||||
@ -266,8 +270,6 @@ steps:
|
|||||||
## テスト結果
|
## テスト結果
|
||||||
- {実行コマンドと結果}
|
- {実行コマンドと結果}
|
||||||
|
|
||||||
**実装未着手の扱い(必須)**
|
|
||||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||||
@ -518,6 +520,17 @@ steps:
|
|||||||
レビュアーからのフィードバックに対応してください。
|
レビュアーからのフィードバックに対応してください。
|
||||||
「Original User Request」は参考情報であり、最新の指示ではありません。
|
「Original User Request」は参考情報であり、最新の指示ではありません。
|
||||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||||
|
|
||||||
|
|
||||||
|
**必須出力(見出しを含める)**
|
||||||
|
## 作業結果
|
||||||
|
- {実施内容の要約}
|
||||||
|
## 変更内容
|
||||||
|
- {変更内容の要約}
|
||||||
|
## テスト結果
|
||||||
|
- {実行コマンドと結果}
|
||||||
|
## 証拠
|
||||||
|
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@ -633,6 +646,17 @@ steps:
|
|||||||
|
|
||||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||||
優先度の高い項目から順に対応してください。
|
優先度の高い項目から順に対応してください。
|
||||||
|
|
||||||
|
|
||||||
|
**必須出力(見出しを含める)**
|
||||||
|
## 作業結果
|
||||||
|
- {実施内容の要約}
|
||||||
|
## 変更内容
|
||||||
|
- {変更内容の要約}
|
||||||
|
## テスト結果
|
||||||
|
- {実行コマンドと結果}
|
||||||
|
## 証拠
|
||||||
|
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: 監督者の指摘に対する修正が完了した
|
- condition: 監督者の指摘に対する修正が完了した
|
||||||
|
|||||||
@ -144,9 +144,13 @@ steps:
|
|||||||
- condition: 実装が完了した
|
- condition: 実装が完了した
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: 実装未着手(レポートのみ)
|
- condition: 実装未着手(レポートのみ)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: 実装を進行できない
|
- condition: 実装を進行できない
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: ユーザー入力が必要
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Phase 2: AI Review
|
# Phase 2: AI Review
|
||||||
@ -257,8 +261,6 @@ steps:
|
|||||||
## テスト結果
|
## テスト結果
|
||||||
- {実行コマンドと結果}
|
- {実行コマンドと結果}
|
||||||
|
|
||||||
**実装未着手の扱い(必須)**
|
|
||||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||||
@ -509,6 +511,17 @@ steps:
|
|||||||
レビュアーからのフィードバックに対応してください。
|
レビュアーからのフィードバックに対応してください。
|
||||||
「Original User Request」は参考情報であり、最新の指示ではありません。
|
「Original User Request」は参考情報であり、最新の指示ではありません。
|
||||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||||
|
|
||||||
|
|
||||||
|
**必須出力(見出しを含める)**
|
||||||
|
## 作業結果
|
||||||
|
- {実施内容の要約}
|
||||||
|
## 変更内容
|
||||||
|
- {変更内容の要約}
|
||||||
|
## テスト結果
|
||||||
|
- {実行コマンドと結果}
|
||||||
|
## 証拠
|
||||||
|
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@ -624,6 +637,17 @@ steps:
|
|||||||
|
|
||||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||||
優先度の高い項目から順に対応してください。
|
優先度の高い項目から順に対応してください。
|
||||||
|
|
||||||
|
|
||||||
|
**必須出力(見出しを含める)**
|
||||||
|
## 作業結果
|
||||||
|
- {実施内容の要約}
|
||||||
|
## 変更内容
|
||||||
|
- {変更内容の要約}
|
||||||
|
## テスト結果
|
||||||
|
- {実行コマンドと結果}
|
||||||
|
## 証拠
|
||||||
|
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||||
pass_previous_response: true
|
pass_previous_response: true
|
||||||
rules:
|
rules:
|
||||||
- condition: 監督者の指摘に対する修正が完了した
|
- condition: 監督者の指摘に対する修正が完了した
|
||||||
|
|||||||
@ -138,15 +138,17 @@ steps:
|
|||||||
## テスト結果
|
## テスト結果
|
||||||
- {実行コマンドと結果}
|
- {実行コマンドと結果}
|
||||||
|
|
||||||
**実装未着手の扱い(必須)**
|
|
||||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
|
||||||
rules:
|
rules:
|
||||||
- condition: 実装が完了した
|
- condition: 実装が完了した
|
||||||
next: ai_review
|
next: ai_review
|
||||||
- condition: 実装未着手(レポートのみ)
|
- condition: 実装未着手(レポートのみ)
|
||||||
next: plan
|
next: ai_review
|
||||||
- condition: 実装を進行できない
|
- condition: 実装を進行できない
|
||||||
next: plan
|
next: ai_review
|
||||||
|
- condition: ユーザー入力が必要
|
||||||
|
next: implement
|
||||||
|
requires_user_input: true
|
||||||
|
interactive_only: true
|
||||||
|
|
||||||
- name: ai_review
|
- name: ai_review
|
||||||
edit: false
|
edit: false
|
||||||
|
|||||||
@ -20,7 +20,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
|
|||||||
loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })),
|
loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
promptInput: vi.fn(),
|
promptInput: vi.fn(),
|
||||||
confirm: vi.fn(),
|
confirm: vi.fn(),
|
||||||
selectOption: vi.fn(),
|
selectOption: vi.fn(),
|
||||||
@ -36,7 +36,8 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
blankLine: vi.fn(),
|
blankLine: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/debug.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
@ -69,7 +70,7 @@ vi.mock('../infra/github/issue.js', () => ({
|
|||||||
|
|
||||||
import { interactiveMode } from '../features/interactive/index.js';
|
import { interactiveMode } from '../features/interactive/index.js';
|
||||||
import { getProvider } from '../infra/providers/index.js';
|
import { getProvider } from '../infra/providers/index.js';
|
||||||
import { promptInput, confirm, selectOption } from '../prompt/index.js';
|
import { promptInput, confirm, selectOption } from '../shared/prompt/index.js';
|
||||||
import { summarizeTaskName } from '../infra/task/summarize.js';
|
import { summarizeTaskName } from '../infra/task/summarize.js';
|
||||||
import { listWorkflows } from '../infra/config/loaders/workflowLoader.js';
|
import { listWorkflows } from '../infra/config/loaders/workflowLoader.js';
|
||||||
import { resolveIssueTask } from '../infra/github/issue.js';
|
import { resolveIssueTask } from '../infra/github/issue.js';
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { detectJudgeIndex, buildJudgePrompt } from '../claude/client.js';
|
import { detectJudgeIndex, buildJudgePrompt } from '../infra/claude/client.js';
|
||||||
|
|
||||||
describe('detectJudgeIndex', () => {
|
describe('detectJudgeIndex', () => {
|
||||||
it('should detect [JUDGE:1] as index 0', () => {
|
it('should detect [JUDGE:1] as index 0', () => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
// Mock dependencies before importing the module under test
|
// Mock dependencies before importing the module under test
|
||||||
vi.mock('../prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
confirm: vi.fn(),
|
confirm: vi.fn(),
|
||||||
selectOptionWithDefault: vi.fn(),
|
selectOptionWithDefault: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@ -32,7 +32,8 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
setLogLevel: vi.fn(),
|
setLogLevel: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/debug.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
@ -60,8 +61,8 @@ vi.mock('../infra/config/loaders/workflowLoader.js', () => ({
|
|||||||
listWorkflows: vi.fn(() => []),
|
listWorkflows: vi.fn(() => []),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../constants.js', async (importOriginal) => {
|
vi.mock('../shared/constants.js', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('../constants.js')>();
|
const actual = await importOriginal<typeof import('../shared/constants.js')>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
DEFAULT_WORKFLOW_NAME: 'default',
|
DEFAULT_WORKFLOW_NAME: 'default',
|
||||||
@ -73,11 +74,12 @@ vi.mock('../infra/github/issue.js', () => ({
|
|||||||
resolveIssueTask: vi.fn(),
|
resolveIssueTask: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/updateNotifier.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
checkForUpdates: vi.fn(),
|
checkForUpdates: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { confirm } from '../prompt/index.js';
|
import { confirm } from '../shared/prompt/index.js';
|
||||||
import { createSharedClone } from '../infra/task/clone.js';
|
import { createSharedClone } from '../infra/task/clone.js';
|
||||||
import { summarizeTaskName } from '../infra/task/summarize.js';
|
import { summarizeTaskName } from '../infra/task/summarize.js';
|
||||||
import { info } from '../shared/ui/index.js';
|
import { info } from '../shared/ui/index.js';
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
detectRuleIndex,
|
detectRuleIndex,
|
||||||
isRegexSafe,
|
isRegexSafe,
|
||||||
} from '../claude/client.js';
|
} from '../infra/claude/client.js';
|
||||||
|
|
||||||
describe('isRegexSafe', () => {
|
describe('isRegexSafe', () => {
|
||||||
it('should accept simple patterns', () => {
|
it('should accept simple patterns', () => {
|
||||||
|
|||||||
@ -21,7 +21,8 @@ vi.mock('node:fs', () => ({
|
|||||||
existsSync: vi.fn(),
|
existsSync: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/debug.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
|
|||||||
@ -13,8 +13,6 @@ import {
|
|||||||
loadWorkflow,
|
loadWorkflow,
|
||||||
listWorkflows,
|
listWorkflows,
|
||||||
loadAgentPromptFromPath,
|
loadAgentPromptFromPath,
|
||||||
} from '../infra/config/loaders/loader.js';
|
|
||||||
import {
|
|
||||||
getCurrentWorkflow,
|
getCurrentWorkflow,
|
||||||
setCurrentWorkflow,
|
setCurrentWorkflow,
|
||||||
getProjectConfigDir,
|
getProjectConfigDir,
|
||||||
@ -35,9 +33,9 @@ import {
|
|||||||
getWorktreeSessionPath,
|
getWorktreeSessionPath,
|
||||||
loadWorktreeSessions,
|
loadWorktreeSessions,
|
||||||
updateWorktreeSession,
|
updateWorktreeSession,
|
||||||
} from '../infra/config/paths.js';
|
getLanguage,
|
||||||
import { getLanguage } from '../infra/config/global/globalConfig.js';
|
loadProjectConfig,
|
||||||
import { loadProjectConfig } from '../infra/config/project/projectConfig.js';
|
} from '../infra/config/index.js';
|
||||||
|
|
||||||
describe('getBuiltinWorkflow', () => {
|
describe('getBuiltinWorkflow', () => {
|
||||||
it('should return builtin workflow when it exists in resources', () => {
|
it('should return builtin workflow when it exists in resources', () => {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
debugLog,
|
debugLog,
|
||||||
infoLog,
|
infoLog,
|
||||||
errorLog,
|
errorLog,
|
||||||
} from '../shared/utils/debug.js';
|
} from '../shared/utils/index.js';
|
||||||
import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
|
import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
|
|||||||
@ -28,19 +28,15 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../claude/query-manager.js', () => ({
|
|
||||||
interruptAllQueries: vi.fn().mockReturnValue(0),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// --- Imports (after mocks) ---
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
import { WorkflowEngine } from '../core/workflow/index.js';
|
import { WorkflowEngine } from '../core/workflow/index.js';
|
||||||
import { runAgent } from '../agents/runner.js';
|
import { runAgent } from '../agents/runner.js';
|
||||||
import { interruptAllQueries } from '../claude/query-manager.js';
|
|
||||||
import {
|
import {
|
||||||
makeResponse,
|
makeResponse,
|
||||||
makeStep,
|
makeStep,
|
||||||
@ -128,23 +124,11 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
expect(state.status).toBe('aborted');
|
expect(state.status).toBe('aborted');
|
||||||
expect(abortFn).toHaveBeenCalledOnce();
|
expect(abortFn).toHaveBeenCalledOnce();
|
||||||
expect(abortFn.mock.calls[0][1]).toContain('SIGINT');
|
expect(abortFn.mock.calls[0][1]).toContain('SIGINT');
|
||||||
expect(vi.mocked(interruptAllQueries)).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('abort() calls interruptAllQueries', () => {
|
|
||||||
it('should call interruptAllQueries when abort() is called', () => {
|
|
||||||
const config = makeSimpleConfig();
|
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
|
||||||
|
|
||||||
engine.abort();
|
|
||||||
|
|
||||||
expect(vi.mocked(interruptAllQueries)).toHaveBeenCalledOnce();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('abort() idempotency', () => {
|
describe('abort() idempotency', () => {
|
||||||
it('should only call interruptAllQueries once on multiple abort() calls', () => {
|
it('should remain abort-requested on multiple abort() calls', () => {
|
||||||
const config = makeSimpleConfig();
|
const config = makeSimpleConfig();
|
||||||
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||||
|
|
||||||
@ -152,7 +136,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
|||||||
engine.abort();
|
engine.abort();
|
||||||
engine.abort();
|
engine.abort();
|
||||||
|
|
||||||
expect(vi.mocked(interruptAllQueries)).toHaveBeenCalledOnce();
|
expect(engine.isAbortRequested()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn(),
|
runStatusJudgmentPhase: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { runAgent } from '../agents/runner.js';
|
|||||||
import { detectMatchedRule } from '../core/workflow/index.js';
|
import { detectMatchedRule } from '../core/workflow/index.js';
|
||||||
import type { RuleMatch } from '../core/workflow/index.js';
|
import type { RuleMatch } from '../core/workflow/index.js';
|
||||||
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/workflow/index.js';
|
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/workflow/index.js';
|
||||||
import { generateReportDir } from '../shared/utils/reportDir.js';
|
import { generateReportDir } from '../shared/utils/index.js';
|
||||||
|
|
||||||
// --- Factory functions ---
|
// --- Factory functions ---
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
|
|||||||
expect(phaseCtx.reportDir).not.toBe(unexpectedPath);
|
expect(phaseCtx.reportDir).not.toBe(unexpectedPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass cwd-based reportDir to buildInstruction (used by {report_dir} placeholder)', async () => {
|
it('should pass projectCwd-based reportDir to buildInstruction (used by {report_dir} placeholder)', async () => {
|
||||||
// Given: worktree environment with a step that uses {report_dir} in template
|
// Given: worktree environment with a step that uses {report_dir} in template
|
||||||
const config: WorkflowConfig = {
|
const config: WorkflowConfig = {
|
||||||
name: 'worktree-test',
|
name: 'worktree-test',
|
||||||
@ -162,15 +163,15 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
|
|||||||
// When: run the workflow
|
// When: run the workflow
|
||||||
await engine.run();
|
await engine.run();
|
||||||
|
|
||||||
// Then: the instruction should contain cwd-based reportDir
|
// Then: the instruction should contain projectCwd-based reportDir
|
||||||
const runAgentMock = vi.mocked(runAgent);
|
const runAgentMock = vi.mocked(runAgent);
|
||||||
expect(runAgentMock).toHaveBeenCalled();
|
expect(runAgentMock).toHaveBeenCalled();
|
||||||
const instruction = runAgentMock.mock.calls[0][1] as string;
|
const instruction = runAgentMock.mock.calls[0][1] as string;
|
||||||
|
|
||||||
const expectedPath = join(cloneCwd, '.takt/reports/test-report-dir');
|
const expectedPath = join(projectCwd, '.takt/reports/test-report-dir');
|
||||||
expect(instruction).toContain(expectedPath);
|
expect(instruction).toContain(expectedPath);
|
||||||
// In worktree mode, projectCwd path should NOT appear
|
// In worktree mode, cloneCwd path should NOT appear
|
||||||
expect(instruction).not.toContain(join(projectCwd, '.takt/reports/test-report-dir'));
|
expect(instruction).not.toContain(join(cloneCwd, '.takt/reports/test-report-dir'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use same path in non-worktree mode (cwd === projectCwd)', async () => {
|
it('should use same path in non-worktree mode (cwd === projectCwd)', async () => {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
EXIT_GIT_OPERATION_FAILED,
|
EXIT_GIT_OPERATION_FAILED,
|
||||||
EXIT_PR_CREATION_FAILED,
|
EXIT_PR_CREATION_FAILED,
|
||||||
EXIT_SIGINT,
|
EXIT_SIGINT,
|
||||||
} from '../exitCodes.js';
|
} from '../shared/exitCodes.js';
|
||||||
|
|
||||||
describe('exit codes', () => {
|
describe('exit codes', () => {
|
||||||
it('should have distinct values', () => {
|
it('should have distinct values', () => {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ vi.mock('node:os', async () => {
|
|||||||
|
|
||||||
// Mock the prompt to track if it was called
|
// Mock the prompt to track if it was called
|
||||||
const mockSelectOption = vi.fn().mockResolvedValue('en');
|
const mockSelectOption = vi.fn().mockResolvedValue('en');
|
||||||
vi.mock('../prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
selectOptionWithDefault: mockSelectOption,
|
selectOptionWithDefault: mockSelectOption,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -20,14 +20,14 @@ vi.mock('node:os', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock the prompt to avoid interactive input
|
// Mock the prompt to avoid interactive input
|
||||||
vi.mock('../prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
selectOptionWithDefault: vi.fn().mockResolvedValue('ja'),
|
selectOptionWithDefault: vi.fn().mockResolvedValue('ja'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Import after mocks are set up
|
// Import after mocks are set up
|
||||||
const { needsLanguageSetup } = await import('../infra/config/global/initialization.js');
|
const { needsLanguageSetup } = await import('../infra/config/global/initialization.js');
|
||||||
const { getGlobalConfigPath } = await import('../infra/config/paths.js');
|
const { getGlobalConfigPath } = await import('../infra/config/paths.js');
|
||||||
const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js');
|
const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../infra/resources/index.js');
|
||||||
|
|
||||||
describe('initialization', () => {
|
describe('initialization', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -369,6 +369,20 @@ describe('instruction-builder', () => {
|
|||||||
|
|
||||||
expect(result).toContain('`[AI_REVIEW:1]`');
|
expect(result).toContain('`[AI_REVIEW:1]`');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should omit interactive-only rules when interactive is false', () => {
|
||||||
|
const filteredRules: WorkflowRule[] = [
|
||||||
|
{ condition: 'Clear', next: 'implement' },
|
||||||
|
{ condition: 'User input required', next: 'implement', interactiveOnly: true },
|
||||||
|
{ condition: 'Blocked', next: 'plan' },
|
||||||
|
];
|
||||||
|
const result = generateStatusRulesFromRules('implement', filteredRules, 'en', { interactive: false });
|
||||||
|
|
||||||
|
expect(result).toContain('`[IMPLEMENT:1]`');
|
||||||
|
expect(result).toContain('`[IMPLEMENT:3]`');
|
||||||
|
expect(result).not.toContain('User input required');
|
||||||
|
expect(result).not.toContain('`[IMPLEMENT:2]`');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildInstruction with rules (Phase 1 — status rules injection)', () => {
|
describe('buildInstruction with rules (Phase 1 — status rules injection)', () => {
|
||||||
|
|||||||
@ -12,7 +12,8 @@ vi.mock('../infra/providers/index.js', () => ({
|
|||||||
getProvider: vi.fn(),
|
getProvider: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/debug.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
@ -20,13 +21,15 @@ vi.mock('../shared/utils/debug.js', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../context.js', () => ({
|
vi.mock('../shared/context.js', () => ({
|
||||||
isQuietMode: vi.fn(() => false),
|
isQuietMode: vi.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/config/paths.js', () => ({
|
vi.mock('../infra/config/paths.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
loadAgentSessions: vi.fn(() => ({})),
|
loadAgentSessions: vi.fn(() => ({})),
|
||||||
updateAgentSession: vi.fn(),
|
updateAgentSession: vi.fn(),
|
||||||
|
getProjectConfigDir: vi.fn(() => '/tmp'),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/ui/index.js', () => ({
|
vi.mock('../shared/ui/index.js', () => ({
|
||||||
@ -39,7 +42,7 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
selectOption: vi.fn(),
|
selectOption: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -51,7 +54,7 @@ vi.mock('node:readline', () => ({
|
|||||||
import { createInterface } from 'node:readline';
|
import { createInterface } from 'node:readline';
|
||||||
import { getProvider } from '../infra/providers/index.js';
|
import { getProvider } from '../infra/providers/index.js';
|
||||||
import { interactiveMode } from '../features/interactive/index.js';
|
import { interactiveMode } from '../features/interactive/index.js';
|
||||||
import { selectOption } from '../prompt/index.js';
|
import { selectOption } from '../shared/prompt/index.js';
|
||||||
|
|
||||||
const mockGetProvider = vi.mocked(getProvider);
|
const mockGetProvider = vi.mocked(getProvider);
|
||||||
const mockCreateInterface = vi.mocked(createInterface);
|
const mockCreateInterface = vi.mocked(createInterface);
|
||||||
|
|||||||
@ -12,13 +12,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|||||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||||
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
|
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
|
||||||
|
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
|
|
||||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
@ -31,7 +32,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
}));
|
}));
|
||||||
@ -87,6 +89,14 @@ function createTestEnv(): { dir: string; agentPaths: Record<string, string> } {
|
|||||||
return { dir, agentPaths };
|
return { dir, agentPaths };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEngineOptions(projectCwd: string) {
|
||||||
|
return {
|
||||||
|
projectCwd,
|
||||||
|
detectRuleIndex,
|
||||||
|
callAiJudge,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildWorkflow(agentPaths: Record<string, string>, maxIterations: number): WorkflowConfig {
|
function buildWorkflow(agentPaths: Record<string, string>, maxIterations: number): WorkflowConfig {
|
||||||
return {
|
return {
|
||||||
name: 'it-error',
|
name: 'it-error',
|
||||||
@ -133,7 +143,7 @@ describe('Error Recovery IT: agent blocked response', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 10);
|
const config = buildWorkflow(agentPaths, 10);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -150,7 +160,7 @@ describe('Error Recovery IT: agent blocked response', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 10);
|
const config = buildWorkflow(agentPaths, 10);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -187,7 +197,7 @@ describe('Error Recovery IT: max iterations reached', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 2);
|
const config = buildWorkflow(agentPaths, 2);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,7 +217,7 @@ describe('Error Recovery IT: max iterations reached', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 4);
|
const config = buildWorkflow(agentPaths, 4);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -242,7 +252,7 @@ describe('Error Recovery IT: scenario queue exhaustion', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 10);
|
const config = buildWorkflow(agentPaths, 10);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -279,7 +289,7 @@ describe('Error Recovery IT: step events on error paths', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 3);
|
const config = buildWorkflow(agentPaths, 3);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -300,7 +310,7 @@ describe('Error Recovery IT: step events on error paths', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 10);
|
const config = buildWorkflow(agentPaths, 10);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -347,7 +357,7 @@ describe('Error Recovery IT: programmatic abort', () => {
|
|||||||
|
|
||||||
const config = buildWorkflow(agentPaths, 10);
|
const config = buildWorkflow(agentPaths, 10);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
getScenarioQueue,
|
getScenarioQueue,
|
||||||
resetScenario,
|
resetScenario,
|
||||||
type ScenarioEntry,
|
type ScenarioEntry,
|
||||||
} from '../mock/scenario.js';
|
} from '../infra/mock/index.js';
|
||||||
|
|
||||||
describe('ScenarioQueue', () => {
|
describe('ScenarioQueue', () => {
|
||||||
it('should consume entries in order when no agent specified', () => {
|
it('should consume entries in order when no agent specified', () => {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|||||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
|
|
||||||
@ -31,8 +31,8 @@ const {
|
|||||||
mockPushBranch: vi.fn(),
|
mockPushBranch: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
@ -73,12 +73,14 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/notification.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
notifySuccess: vi.fn(),
|
notifySuccess: vi.fn(),
|
||||||
notifyError: vi.fn(),
|
notifyError: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
createSessionLog: vi.fn().mockReturnValue({
|
createSessionLog: vi.fn().mockReturnValue({
|
||||||
startTime: new Date().toISOString(),
|
startTime: new Date().toISOString(),
|
||||||
@ -122,11 +124,11 @@ vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../context.js', () => ({
|
vi.mock('../shared/context.js', () => ({
|
||||||
isQuietMode: vi.fn().mockReturnValue(true),
|
isQuietMode: vi.fn().mockReturnValue(true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
selectOption: vi.fn().mockResolvedValue('stop'),
|
selectOption: vi.fn().mockResolvedValue('stop'),
|
||||||
promptInput: vi.fn().mockResolvedValue(null),
|
promptInput: vi.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
@ -144,7 +146,7 @@ import {
|
|||||||
EXIT_ISSUE_FETCH_FAILED,
|
EXIT_ISSUE_FETCH_FAILED,
|
||||||
EXIT_WORKFLOW_FAILED,
|
EXIT_WORKFLOW_FAILED,
|
||||||
EXIT_PR_CREATION_FAILED,
|
EXIT_PR_CREATION_FAILED,
|
||||||
} from '../exitCodes.js';
|
} from '../shared/exitCodes.js';
|
||||||
|
|
||||||
// --- Test helpers ---
|
// --- Test helpers ---
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|||||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
|
|
||||||
// Safety net: prevent callAiJudge from calling real Claude CLI.
|
// Safety net: prevent callAiJudge from calling real Claude CLI.
|
||||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
@ -56,12 +56,14 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/notification.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
notifySuccess: vi.fn(),
|
notifySuccess: vi.fn(),
|
||||||
notifyError: vi.fn(),
|
notifyError: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
createSessionLog: vi.fn().mockReturnValue({
|
createSessionLog: vi.fn().mockReturnValue({
|
||||||
startTime: new Date().toISOString(),
|
startTime: new Date().toISOString(),
|
||||||
@ -104,11 +106,11 @@ vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../context.js', () => ({
|
vi.mock('../shared/context.js', () => ({
|
||||||
isQuietMode: vi.fn().mockReturnValue(true),
|
isQuietMode: vi.fn().mockReturnValue(true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../prompt/index.js', () => ({
|
vi.mock('../shared/prompt/index.js', () => ({
|
||||||
selectOption: vi.fn().mockResolvedValue('stop'),
|
selectOption: vi.fn().mockResolvedValue('stop'),
|
||||||
promptInput: vi.fn().mockResolvedValue(null),
|
promptInput: vi.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -21,14 +21,6 @@ import type { WorkflowStep, WorkflowState, WorkflowRule, AgentResponse } from '.
|
|||||||
|
|
||||||
const mockCallAiJudge = vi.fn();
|
const mockCallAiJudge = vi.fn();
|
||||||
|
|
||||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
|
||||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
|
||||||
return {
|
|
||||||
...original,
|
|
||||||
callAiJudge: (...args: unknown[]) => mockCallAiJudge(...args),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
||||||
getLanguage: vi.fn().mockReturnValue('en'),
|
getLanguage: vi.fn().mockReturnValue('en'),
|
||||||
@ -41,6 +33,7 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
|
|||||||
// --- Imports (after mocks) ---
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js';
|
import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js';
|
||||||
|
import { detectRuleIndex } from '../infra/claude/index.js';
|
||||||
import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js';
|
import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js';
|
||||||
|
|
||||||
// --- Test helpers ---
|
// --- Test helpers ---
|
||||||
@ -82,6 +75,8 @@ function makeCtx(stepOutputs?: Map<string, AgentResponse>): RuleEvaluatorContext
|
|||||||
return {
|
return {
|
||||||
state: makeState(stepOutputs),
|
state: makeState(stepOutputs),
|
||||||
cwd: '/tmp/test',
|
cwd: '/tmp/test',
|
||||||
|
detectRuleIndex,
|
||||||
|
callAiJudge: mockCallAiJudge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,13 +13,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|||||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||||
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
|
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
|
||||||
|
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
|
|
||||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
@ -36,7 +37,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...args),
|
runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
}));
|
}));
|
||||||
@ -73,6 +75,14 @@ function createTestEnv(): { dir: string; agentPath: string } {
|
|||||||
return { dir, agentPath };
|
return { dir, agentPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEngineOptions(projectCwd: string) {
|
||||||
|
return {
|
||||||
|
projectCwd,
|
||||||
|
detectRuleIndex,
|
||||||
|
callAiJudge,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function makeStep(
|
function makeStep(
|
||||||
name: string,
|
name: string,
|
||||||
agentPath: string,
|
agentPath: string,
|
||||||
@ -132,7 +142,7 @@ describe('Three-Phase Execution IT: phase1 only (no report, no tag rules)', () =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -184,7 +194,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -213,7 +223,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -265,7 +275,7 @@ describe('Three-Phase Execution IT: phase1 + phase3 (tag rules defined)', () =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -316,7 +326,7 @@ describe('Three-Phase Execution IT: all three phases', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -379,7 +389,7 @@ describe('Three-Phase Execution IT: phase3 tag → rule match', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -13,16 +13,17 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|||||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||||
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
|
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
|
||||||
|
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
|
||||||
|
|
||||||
// --- Mocks (minimal — only infrastructure, not core logic) ---
|
// --- Mocks (minimal — only infrastructure, not core logic) ---
|
||||||
|
|
||||||
// Safety net: prevent callAiJudge from calling real Claude CLI.
|
// Safety net: prevent callAiJudge from calling real Claude CLI.
|
||||||
// Tag-based detection should always match in these tests; if it doesn't,
|
// Tag-based detection should always match in these tests; if it doesn't,
|
||||||
// this mock surfaces the failure immediately instead of timing out.
|
// this mock surfaces the failure immediately instead of timing out.
|
||||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
@ -35,7 +36,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
}));
|
}));
|
||||||
@ -89,6 +91,14 @@ function createTestEnv(): { dir: string; agentPaths: Record<string, string> } {
|
|||||||
return { dir, agentPaths };
|
return { dir, agentPaths };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEngineOptions(projectCwd: string) {
|
||||||
|
return {
|
||||||
|
projectCwd,
|
||||||
|
detectRuleIndex,
|
||||||
|
callAiJudge,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildSimpleWorkflow(agentPaths: Record<string, string>): WorkflowConfig {
|
function buildSimpleWorkflow(agentPaths: Record<string, string>): WorkflowConfig {
|
||||||
return {
|
return {
|
||||||
name: 'it-simple',
|
name: 'it-simple',
|
||||||
@ -168,7 +178,7 @@ describe('Workflow Engine IT: Happy Path', () => {
|
|||||||
|
|
||||||
const config = buildSimpleWorkflow(agentPaths);
|
const config = buildSimpleWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -185,7 +195,7 @@ describe('Workflow Engine IT: Happy Path', () => {
|
|||||||
|
|
||||||
const config = buildSimpleWorkflow(agentPaths);
|
const config = buildSimpleWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Vague task', {
|
const engine = new WorkflowEngine(config, testDir, 'Vague task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -228,7 +238,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
|
|||||||
|
|
||||||
const config = buildLoopWorkflow(agentPaths);
|
const config = buildLoopWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Task needing fix', {
|
const engine = new WorkflowEngine(config, testDir, 'Task needing fix', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -248,7 +258,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
|
|||||||
|
|
||||||
const config = buildLoopWorkflow(agentPaths);
|
const config = buildLoopWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Unfixable task', {
|
const engine = new WorkflowEngine(config, testDir, 'Unfixable task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -286,7 +296,7 @@ describe('Workflow Engine IT: Max Iterations', () => {
|
|||||||
config.maxIterations = 5;
|
config.maxIterations = 5;
|
||||||
|
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -322,7 +332,7 @@ describe('Workflow Engine IT: Step Output Tracking', () => {
|
|||||||
|
|
||||||
const config = buildSimpleWorkflow(agentPaths);
|
const config = buildSimpleWorkflow(agentPaths);
|
||||||
const engine = new WorkflowEngine(config, testDir, 'Track outputs', {
|
const engine = new WorkflowEngine(config, testDir, 'Track outputs', {
|
||||||
projectCwd: testDir,
|
...buildEngineOptions(testDir),
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
|
|||||||
|
|
||||||
// --- Imports (after mocks) ---
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js';
|
import { loadWorkflow } from '../infra/config/index.js';
|
||||||
|
|
||||||
// --- Test helpers ---
|
// --- Test helpers ---
|
||||||
|
|
||||||
|
|||||||
@ -12,12 +12,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|||||||
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||||
|
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
|
||||||
|
|
||||||
// --- Mocks ---
|
// --- Mocks ---
|
||||||
|
|
||||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||||
@ -30,7 +31,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
|||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/reportDir.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
}));
|
}));
|
||||||
@ -48,7 +50,7 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
|
|||||||
// --- Imports (after mocks) ---
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
import { WorkflowEngine } from '../core/workflow/index.js';
|
import { WorkflowEngine } from '../core/workflow/index.js';
|
||||||
import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js';
|
import { loadWorkflow } from '../infra/config/index.js';
|
||||||
import type { WorkflowConfig } from '../core/models/index.js';
|
import type { WorkflowConfig } from '../core/models/index.js';
|
||||||
|
|
||||||
// --- Test helpers ---
|
// --- Test helpers ---
|
||||||
@ -63,6 +65,8 @@ function createEngine(config: WorkflowConfig, dir: string, task: string): Workfl
|
|||||||
return new WorkflowEngine(config, dir, task, {
|
return new WorkflowEngine(config, dir, task, {
|
||||||
projectCwd: dir,
|
projectCwd: dir,
|
||||||
provider: 'mock',
|
provider: 'mock',
|
||||||
|
detectRuleIndex,
|
||||||
|
callAiJudge,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,8 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
}));
|
}));
|
||||||
// Mock debug logger
|
// Mock debug logger
|
||||||
vi.mock('../shared/utils/debug.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
|
|||||||
@ -5,14 +5,14 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import type { SelectOptionItem, KeyInputResult } from '../prompt/index.js';
|
import type { SelectOptionItem, KeyInputResult } from '../shared/prompt/index.js';
|
||||||
import {
|
import {
|
||||||
renderMenu,
|
renderMenu,
|
||||||
countRenderedLines,
|
countRenderedLines,
|
||||||
handleKeyInput,
|
handleKeyInput,
|
||||||
readMultilineFromStream,
|
readMultilineFromStream,
|
||||||
} from '../prompt/index.js';
|
} from '../shared/prompt/index.js';
|
||||||
import { isFullWidth, getDisplayWidth, truncateText } from '../shared/utils/text.js';
|
import { isFullWidth, getDisplayWidth, truncateText } from '../shared/utils/index.js';
|
||||||
|
|
||||||
// Disable chalk colors for predictable test output
|
// Disable chalk colors for predictable test output
|
||||||
chalk.level = 0;
|
chalk.level = 0;
|
||||||
@ -310,7 +310,7 @@ describe('prompt', () => {
|
|||||||
|
|
||||||
describe('selectOption', () => {
|
describe('selectOption', () => {
|
||||||
it('should return null for empty options', async () => {
|
it('should return null for empty options', async () => {
|
||||||
const { selectOption } = await import('../prompt/index.js');
|
const { selectOption } = await import('../shared/prompt/index.js');
|
||||||
const result = await selectOption('Test:', []);
|
const result = await selectOption('Test:', []);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
@ -318,13 +318,13 @@ describe('prompt', () => {
|
|||||||
|
|
||||||
describe('selectOptionWithDefault', () => {
|
describe('selectOptionWithDefault', () => {
|
||||||
it('should return default for empty options', async () => {
|
it('should return default for empty options', async () => {
|
||||||
const { selectOptionWithDefault } = await import('../prompt/index.js');
|
const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
|
||||||
const result = await selectOptionWithDefault('Test:', [], 'fallback');
|
const result = await selectOptionWithDefault('Test:', [], 'fallback');
|
||||||
expect(result).toBe('fallback');
|
expect(result).toBe('fallback');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have return type that allows null (cancel)', async () => {
|
it('should have return type that allows null (cancel)', async () => {
|
||||||
const { selectOptionWithDefault } = await import('../prompt/index.js');
|
const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
|
||||||
// When options are empty, default is returned (not null)
|
// When options are empty, default is returned (not null)
|
||||||
const result: string | null = await selectOptionWithDefault('Test:', [], 'fallback');
|
const result: string | null = await selectOptionWithDefault('Test:', [], 'fallback');
|
||||||
expect(result).toBe('fallback');
|
expect(result).toBe('fallback');
|
||||||
|
|||||||
@ -12,7 +12,8 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
|
|||||||
loadGlobalConfig: vi.fn(),
|
loadGlobalConfig: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/debug.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
|
|||||||
@ -11,20 +11,24 @@ vi.mock('../infra/config/index.js', () => ({
|
|||||||
loadGlobalConfig: vi.fn(() => ({})),
|
loadGlobalConfig: vi.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/task/index.js', () => ({
|
vi.mock('../infra/task/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
TaskRunner: vi.fn(),
|
TaskRunner: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/task/clone.js', () => ({
|
vi.mock('../infra/task/clone.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createSharedClone: vi.fn(),
|
createSharedClone: vi.fn(),
|
||||||
removeClone: vi.fn(),
|
removeClone: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/task/autoCommit.js', () => ({
|
vi.mock('../infra/task/autoCommit.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
autoCommitAndPush: vi.fn(),
|
autoCommitAndPush: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/task/summarize.js', () => ({
|
vi.mock('../infra/task/summarize.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
summarizeTaskName: vi.fn(),
|
summarizeTaskName: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -37,15 +41,13 @@ vi.mock('../shared/ui/index.js', () => ({
|
|||||||
blankLine: vi.fn(),
|
blankLine: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/utils/debug.js', () => ({
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
createLogger: () => ({
|
createLogger: () => ({
|
||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
debug: vi.fn(),
|
debug: vi.fn(),
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../shared/utils/error.js', () => ({
|
|
||||||
getErrorMessage: vi.fn((e) => e.message),
|
getErrorMessage: vi.fn((e) => e.message),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -53,11 +55,11 @@ vi.mock('../features/tasks/execute/workflowExecution.js', () => ({
|
|||||||
executeWorkflow: vi.fn(),
|
executeWorkflow: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../context.js', () => ({
|
vi.mock('../shared/context.js', () => ({
|
||||||
isQuietMode: vi.fn(() => false),
|
isQuietMode: vi.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../constants.js', () => ({
|
vi.mock('../shared/constants.js', () => ({
|
||||||
DEFAULT_WORKFLOW_NAME: 'default',
|
DEFAULT_WORKFLOW_NAME: 'default',
|
||||||
DEFAULT_LANGUAGE: 'en',
|
DEFAULT_LANGUAGE: 'en',
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -24,7 +24,7 @@ vi.mock('node:module', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { checkForUpdates } from '../shared/utils/updateNotifier.js';
|
import { checkForUpdates } from '../shared/utils/index.js';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { loadWorkflow } from '../infra/config/loaders/loader.js';
|
import { loadWorkflow } from '../infra/config/index.js';
|
||||||
|
|
||||||
describe('expert workflow parallel structure', () => {
|
describe('expert workflow parallel structure', () => {
|
||||||
const workflow = loadWorkflow('expert', process.cwd());
|
const workflow = loadWorkflow('expert', process.cwd());
|
||||||
|
|||||||
@ -8,13 +8,11 @@ import {
|
|||||||
callClaudeAgent,
|
callClaudeAgent,
|
||||||
callClaudeSkill,
|
callClaudeSkill,
|
||||||
type ClaudeCallOptions,
|
type ClaudeCallOptions,
|
||||||
} from '../claude/client.js';
|
} from '../infra/claude/index.js';
|
||||||
import { loadCustomAgents, loadAgentPrompt } from '../infra/config/loaders/loader.js';
|
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
||||||
import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
|
|
||||||
import { loadProjectConfig } from '../infra/config/project/projectConfig.js';
|
|
||||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
|
||||||
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
|
||||||
import { createLogger } from '../shared/utils/debug.js';
|
import { createLogger } from '../shared/utils/index.js';
|
||||||
import type { RunAgentOptions } from './types.js';
|
import type { RunAgentOptions } from './types.js';
|
||||||
|
|
||||||
// Re-export for backward compatibility
|
// Re-export for backward compatibility
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* Type definitions for agent execution
|
* Type definitions for agent execution
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/types.js';
|
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/index.js';
|
||||||
import type { PermissionMode } from '../core/models/index.js';
|
import type { PermissionMode } from '../core/models/index.js';
|
||||||
|
|
||||||
export type { StreamCallback };
|
export type { StreamCallback };
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, config).
|
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, config).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/paths.js';
|
import { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/index.js';
|
||||||
import { success } from '../../shared/ui/index.js';
|
import { success } from '../../shared/ui/index.js';
|
||||||
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
|
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
|
||||||
import { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/index.js';
|
import { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/index.js';
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type { Command } from 'commander';
|
|||||||
import type { TaskExecutionOptions } from '../../features/tasks/index.js';
|
import type { TaskExecutionOptions } from '../../features/tasks/index.js';
|
||||||
import type { ProviderType } from '../../infra/providers/index.js';
|
import type { ProviderType } from '../../infra/providers/index.js';
|
||||||
import { error } from '../../shared/ui/index.js';
|
import { error } from '../../shared/ui/index.js';
|
||||||
import { isIssueReference } from '../../infra/github/issue.js';
|
import { isIssueReference } from '../../infra/github/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve --provider and --model options into TaskExecutionOptions.
|
* Resolve --provider and --model options into TaskExecutionOptions.
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
* Import order matters: program setup → commands → routing → parse.
|
* Import order matters: program setup → commands → routing → parse.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { checkForUpdates } from '../../shared/utils/updateNotifier.js';
|
import { checkForUpdates } from '../../shared/utils/index.js';
|
||||||
|
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,9 @@ import {
|
|||||||
getEffectiveDebugConfig,
|
getEffectiveDebugConfig,
|
||||||
isVerboseMode,
|
isVerboseMode,
|
||||||
} from '../../infra/config/index.js';
|
} from '../../infra/config/index.js';
|
||||||
import { setQuietMode } from '../../context.js';
|
import { setQuietMode } from '../../shared/context.js';
|
||||||
import { setLogLevel } from '../../shared/ui/index.js';
|
import { setLogLevel } from '../../shared/ui/index.js';
|
||||||
import { initDebugLogger, createLogger, setVerboseConsole } from '../../shared/utils/debug.js';
|
import { initDebugLogger, createLogger, setVerboseConsole } from '../../shared/utils/index.js';
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const { version: cliVersion } = require('../../../package.json') as { version: string };
|
const { version: cliVersion } = require('../../../package.json') as { version: string };
|
||||||
|
|||||||
@ -6,12 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { info, error } from '../../shared/ui/index.js';
|
import { info, error } from '../../shared/ui/index.js';
|
||||||
import { getErrorMessage } from '../../shared/utils/error.js';
|
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { resolveIssueTask, isIssueReference } from '../../infra/github/issue.js';
|
import { resolveIssueTask, isIssueReference } from '../../infra/github/index.js';
|
||||||
import { selectAndExecuteTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
import { selectAndExecuteTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||||
import { executePipeline } from '../../features/pipeline/index.js';
|
import { executePipeline } from '../../features/pipeline/index.js';
|
||||||
import { interactiveMode } from '../../features/interactive/index.js';
|
import { interactiveMode } from '../../features/interactive/index.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../../constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../../shared/constants.js';
|
||||||
import { program, resolvedCwd, pipelineMode } from './program.js';
|
import { program, resolvedCwd, pipelineMode } from './program.js';
|
||||||
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
||||||
|
|
||||||
@ -94,5 +94,6 @@ program
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectOptions.interactiveUserInput = true;
|
||||||
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
|
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import { DEFAULT_LANGUAGE } from '../../constants.js';
|
import { DEFAULT_LANGUAGE } from '../../shared/constants.js';
|
||||||
|
|
||||||
/** Agent model schema (opus, sonnet, haiku) */
|
/** Agent model schema (opus, sonnet, haiku) */
|
||||||
export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet');
|
export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet');
|
||||||
@ -107,6 +107,10 @@ export const WorkflowRuleSchema = z.object({
|
|||||||
next: z.string().min(1).optional(),
|
next: z.string().min(1).optional(),
|
||||||
/** Template for additional AI output */
|
/** Template for additional AI output */
|
||||||
appendix: z.string().optional(),
|
appendix: z.string().optional(),
|
||||||
|
/** Require user input before continuing (interactive mode only) */
|
||||||
|
requires_user_input: z.boolean().optional(),
|
||||||
|
/** Rule applies only in interactive mode */
|
||||||
|
interactive_only: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Sub-step schema for parallel execution (agent is required) */
|
/** Sub-step schema for parallel execution (agent is required) */
|
||||||
|
|||||||
@ -13,6 +13,10 @@ export interface WorkflowRule {
|
|||||||
next?: string;
|
next?: string;
|
||||||
/** Template for additional AI output */
|
/** Template for additional AI output */
|
||||||
appendix?: string;
|
appendix?: string;
|
||||||
|
/** Require user input before continuing (interactive mode only) */
|
||||||
|
requiresUserInput?: boolean;
|
||||||
|
/** Rule applies only in interactive mode */
|
||||||
|
interactiveOnly?: boolean;
|
||||||
/** Whether this condition uses ai() expression (set by loader) */
|
/** Whether this condition uses ai() expression (set by loader) */
|
||||||
isAiCondition?: boolean;
|
isAiCondition?: boolean;
|
||||||
/** The condition text inside ai("...") for AI judge evaluation (set by loader) */
|
/** The condition text inside ai("...") for AI judge evaluation (set by loader) */
|
||||||
|
|||||||
@ -58,6 +58,8 @@ export class OptionsBuilder {
|
|||||||
): RunAgentOptions {
|
): RunAgentOptions {
|
||||||
return {
|
return {
|
||||||
...this.buildBaseOptions(step),
|
...this.buildBaseOptions(step),
|
||||||
|
// Do not pass permission mode in report/status phases.
|
||||||
|
permissionMode: undefined,
|
||||||
sessionId,
|
sessionId,
|
||||||
allowedTools: overrides.allowedTools,
|
allowedTools: overrides.allowedTools,
|
||||||
maxTurns: overrides.maxTurns,
|
maxTurns: overrides.maxTurns,
|
||||||
@ -73,6 +75,7 @@ export class OptionsBuilder {
|
|||||||
cwd: this.getCwd(),
|
cwd: this.getCwd(),
|
||||||
reportDir: join(this.getProjectCwd(), this.getReportDir()),
|
reportDir: join(this.getProjectCwd(), this.getReportDir()),
|
||||||
language: this.getLanguage(),
|
language: this.getLanguage(),
|
||||||
|
interactive: this.engineOptions.interactive,
|
||||||
getSessionId: (agent: string) => state.agentSessions.get(agent),
|
getSessionId: (agent: string) => state.agentSessions.get(agent),
|
||||||
buildResumeOptions: this.buildResumeOptions.bind(this),
|
buildResumeOptions: this.buildResumeOptions.bind(this),
|
||||||
updateAgentSession,
|
updateAgentSession,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { ParallelLogger } from './parallel-logger.js';
|
|||||||
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
|
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
|
||||||
import { detectMatchedRule } from '../evaluation/index.js';
|
import { detectMatchedRule } from '../evaluation/index.js';
|
||||||
import { incrementStepIteration } from './state-manager.js';
|
import { incrementStepIteration } from './state-manager.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
import type { OptionsBuilder } from './OptionsBuilder.js';
|
import type { OptionsBuilder } from './OptionsBuilder.js';
|
||||||
import type { StepExecutor } from './StepExecutor.js';
|
import type { StepExecutor } from './StepExecutor.js';
|
||||||
import type { WorkflowEngineOptions } from '../types.js';
|
import type { WorkflowEngineOptions } from '../types.js';
|
||||||
@ -28,6 +28,13 @@ export interface ParallelRunnerDeps {
|
|||||||
readonly engineOptions: WorkflowEngineOptions;
|
readonly engineOptions: WorkflowEngineOptions;
|
||||||
readonly getCwd: () => string;
|
readonly getCwd: () => string;
|
||||||
readonly getReportDir: () => string;
|
readonly getReportDir: () => string;
|
||||||
|
readonly getInteractive: () => boolean;
|
||||||
|
readonly detectRuleIndex: (content: string, stepName: string) => number;
|
||||||
|
readonly callAiJudge: (
|
||||||
|
agentOutput: string,
|
||||||
|
conditions: Array<{ index: number; text: string }>,
|
||||||
|
options: { cwd: string }
|
||||||
|
) => Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ParallelRunner {
|
export class ParallelRunner {
|
||||||
@ -63,7 +70,13 @@ export class ParallelRunner {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const phaseCtx = this.deps.optionsBuilder.buildPhaseRunnerContext(state, updateAgentSession);
|
const phaseCtx = this.deps.optionsBuilder.buildPhaseRunnerContext(state, updateAgentSession);
|
||||||
const ruleCtx = { state, cwd: this.deps.getCwd() };
|
const ruleCtx = {
|
||||||
|
state,
|
||||||
|
cwd: this.deps.getCwd(),
|
||||||
|
interactive: this.deps.getInteractive(),
|
||||||
|
detectRuleIndex: this.deps.detectRuleIndex,
|
||||||
|
callAiJudge: this.deps.callAiJudge,
|
||||||
|
};
|
||||||
|
|
||||||
// Run all sub-steps concurrently
|
// Run all sub-steps concurrently
|
||||||
const subResults = await Promise.all(
|
const subResults = await Promise.all(
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { InstructionBuilder, isReportObjectConfig } from '../instruction/Instruc
|
|||||||
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
|
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
|
||||||
import { detectMatchedRule } from '../evaluation/index.js';
|
import { detectMatchedRule } from '../evaluation/index.js';
|
||||||
import { incrementStepIteration, getPreviousOutput } from './state-manager.js';
|
import { incrementStepIteration, getPreviousOutput } from './state-manager.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
import type { OptionsBuilder } from './OptionsBuilder.js';
|
import type { OptionsBuilder } from './OptionsBuilder.js';
|
||||||
|
|
||||||
const log = createLogger('step-executor');
|
const log = createLogger('step-executor');
|
||||||
@ -30,6 +30,13 @@ export interface StepExecutorDeps {
|
|||||||
readonly getProjectCwd: () => string;
|
readonly getProjectCwd: () => string;
|
||||||
readonly getReportDir: () => string;
|
readonly getReportDir: () => string;
|
||||||
readonly getLanguage: () => Language | undefined;
|
readonly getLanguage: () => Language | undefined;
|
||||||
|
readonly getInteractive: () => boolean;
|
||||||
|
readonly detectRuleIndex: (content: string, stepName: string) => number;
|
||||||
|
readonly callAiJudge: (
|
||||||
|
agentOutput: string,
|
||||||
|
conditions: Array<{ index: number; text: string }>,
|
||||||
|
options: { cwd: string }
|
||||||
|
) => Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StepExecutor {
|
export class StepExecutor {
|
||||||
@ -54,8 +61,9 @@ export class StepExecutor {
|
|||||||
projectCwd: this.deps.getProjectCwd(),
|
projectCwd: this.deps.getProjectCwd(),
|
||||||
userInputs: state.userInputs,
|
userInputs: state.userInputs,
|
||||||
previousOutput: getPreviousOutput(state),
|
previousOutput: getPreviousOutput(state),
|
||||||
reportDir: join(this.deps.getCwd(), this.deps.getReportDir()),
|
reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()),
|
||||||
language: this.deps.getLanguage(),
|
language: this.deps.getLanguage(),
|
||||||
|
interactive: this.deps.getInteractive(),
|
||||||
}).build();
|
}).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +114,9 @@ export class StepExecutor {
|
|||||||
const match = await detectMatchedRule(step, response.content, tagContent, {
|
const match = await detectMatchedRule(step, response.content, tagContent, {
|
||||||
state,
|
state,
|
||||||
cwd: this.deps.getCwd(),
|
cwd: this.deps.getCwd(),
|
||||||
|
interactive: this.deps.getInteractive(),
|
||||||
|
detectRuleIndex: this.deps.detectRuleIndex,
|
||||||
|
callAiJudge: this.deps.callAiJudge,
|
||||||
});
|
});
|
||||||
if (match) {
|
if (match) {
|
||||||
log.debug('Rule matched', { step: step.name, ruleIndex: match.index, method: match.method });
|
log.debug('Rule matched', { step: step.name, ruleIndex: match.index, method: match.method });
|
||||||
|
|||||||
@ -25,10 +25,7 @@ import {
|
|||||||
addUserInput as addUserInputToState,
|
addUserInput as addUserInputToState,
|
||||||
incrementStepIteration,
|
incrementStepIteration,
|
||||||
} from './state-manager.js';
|
} from './state-manager.js';
|
||||||
import { generateReportDir } from '../../../shared/utils/reportDir.js';
|
import { generateReportDir, getErrorMessage, createLogger } from '../../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
|
||||||
import { interruptAllQueries } from '../../../claude/query-manager.js';
|
|
||||||
import { OptionsBuilder } from './OptionsBuilder.js';
|
import { OptionsBuilder } from './OptionsBuilder.js';
|
||||||
import { StepExecutor } from './StepExecutor.js';
|
import { StepExecutor } from './StepExecutor.js';
|
||||||
import { ParallelRunner } from './ParallelRunner.js';
|
import { ParallelRunner } from './ParallelRunner.js';
|
||||||
@ -61,6 +58,12 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
private readonly optionsBuilder: OptionsBuilder;
|
private readonly optionsBuilder: OptionsBuilder;
|
||||||
private readonly stepExecutor: StepExecutor;
|
private readonly stepExecutor: StepExecutor;
|
||||||
private readonly parallelRunner: ParallelRunner;
|
private readonly parallelRunner: ParallelRunner;
|
||||||
|
private readonly detectRuleIndex: (content: string, stepName: string) => number;
|
||||||
|
private readonly callAiJudge: (
|
||||||
|
agentOutput: string,
|
||||||
|
conditions: Array<{ index: number; text: string }>,
|
||||||
|
options: { cwd: string }
|
||||||
|
) => Promise<number>;
|
||||||
|
|
||||||
constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions) {
|
constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions) {
|
||||||
super();
|
super();
|
||||||
@ -74,6 +77,12 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
this.ensureReportDirExists();
|
this.ensureReportDirExists();
|
||||||
this.validateConfig();
|
this.validateConfig();
|
||||||
this.state = createInitialState(config, options);
|
this.state = createInitialState(config, options);
|
||||||
|
this.detectRuleIndex = options.detectRuleIndex ?? (() => {
|
||||||
|
throw new Error('detectRuleIndex is required for rule evaluation');
|
||||||
|
});
|
||||||
|
this.callAiJudge = options.callAiJudge ?? (async () => {
|
||||||
|
throw new Error('callAiJudge is required for rule evaluation');
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize composed collaborators
|
// Initialize composed collaborators
|
||||||
this.optionsBuilder = new OptionsBuilder(
|
this.optionsBuilder = new OptionsBuilder(
|
||||||
@ -91,6 +100,9 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
getProjectCwd: () => this.projectCwd,
|
getProjectCwd: () => this.projectCwd,
|
||||||
getReportDir: () => this.reportDir,
|
getReportDir: () => this.reportDir,
|
||||||
getLanguage: () => this.options.language,
|
getLanguage: () => this.options.language,
|
||||||
|
getInteractive: () => this.options.interactive === true,
|
||||||
|
detectRuleIndex: this.detectRuleIndex,
|
||||||
|
callAiJudge: this.callAiJudge,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.parallelRunner = new ParallelRunner({
|
this.parallelRunner = new ParallelRunner({
|
||||||
@ -99,6 +111,9 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
engineOptions: this.options,
|
engineOptions: this.options,
|
||||||
getCwd: () => this.cwd,
|
getCwd: () => this.cwd,
|
||||||
getReportDir: () => this.reportDir,
|
getReportDir: () => this.reportDir,
|
||||||
|
getInteractive: () => this.options.interactive === true,
|
||||||
|
detectRuleIndex: this.detectRuleIndex,
|
||||||
|
callAiJudge: this.callAiJudge,
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug('WorkflowEngine initialized', {
|
log.debug('WorkflowEngine initialized', {
|
||||||
@ -183,7 +198,6 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
if (this.abortRequested) return;
|
if (this.abortRequested) return;
|
||||||
this.abortRequested = true;
|
this.abortRequested = true;
|
||||||
log.info('Abort requested');
|
log.info('Abort requested');
|
||||||
interruptAllQueries();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if abort has been requested */
|
/** Check if abort has been requested */
|
||||||
@ -345,6 +359,31 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
nextStep,
|
nextStep,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.matchedRuleIndex != null && step.rules) {
|
||||||
|
const matchedRule = step.rules[response.matchedRuleIndex];
|
||||||
|
if (matchedRule?.requiresUserInput) {
|
||||||
|
if (!this.options.onUserInput) {
|
||||||
|
this.state.status = 'aborted';
|
||||||
|
this.emit('workflow:abort', this.state, 'User input required but no handler is configured');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const userInput = await this.options.onUserInput({
|
||||||
|
step,
|
||||||
|
response,
|
||||||
|
prompt: response.content,
|
||||||
|
});
|
||||||
|
if (userInput === null) {
|
||||||
|
this.state.status = 'aborted';
|
||||||
|
this.emit('workflow:abort', this.state, 'User input cancelled');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.addUserInput(userInput);
|
||||||
|
this.emit('step:user_input', step, userInput);
|
||||||
|
this.state.currentStep = step.name;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (nextStep === COMPLETE_STEP) {
|
if (nextStep === COMPLETE_STEP) {
|
||||||
this.state.status = 'completed';
|
this.state.status = 'completed';
|
||||||
this.emit('workflow:complete', this.state);
|
this.emit('workflow:complete', this.state);
|
||||||
@ -403,6 +442,29 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
const nextStep = this.resolveNextStep(step, response);
|
const nextStep = this.resolveNextStep(step, response);
|
||||||
const isComplete = nextStep === COMPLETE_STEP || nextStep === ABORT_STEP;
|
const isComplete = nextStep === COMPLETE_STEP || nextStep === ABORT_STEP;
|
||||||
|
|
||||||
|
if (response.matchedRuleIndex != null && step.rules) {
|
||||||
|
const matchedRule = step.rules[response.matchedRuleIndex];
|
||||||
|
if (matchedRule?.requiresUserInput) {
|
||||||
|
if (!this.options.onUserInput) {
|
||||||
|
this.state.status = 'aborted';
|
||||||
|
return { response, nextStep: ABORT_STEP, isComplete: true, loopDetected: loopCheck.isLoop };
|
||||||
|
}
|
||||||
|
const userInput = await this.options.onUserInput({
|
||||||
|
step,
|
||||||
|
response,
|
||||||
|
prompt: response.content,
|
||||||
|
});
|
||||||
|
if (userInput === null) {
|
||||||
|
this.state.status = 'aborted';
|
||||||
|
return { response, nextStep: ABORT_STEP, isComplete: true, loopDetected: loopCheck.isLoop };
|
||||||
|
}
|
||||||
|
this.addUserInput(userInput);
|
||||||
|
this.emit('step:user_input', step, userInput);
|
||||||
|
this.state.currentStep = step.name;
|
||||||
|
return { response, nextStep: step.name, isComplete: false, loopDetected: loopCheck.isLoop };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isComplete) {
|
if (!isComplete) {
|
||||||
this.state.currentStep = nextStep;
|
this.state.currentStep = nextStep;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WorkflowStep, WorkflowState } from '../../models/types.js';
|
import type { WorkflowStep, WorkflowState } from '../../models/types.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
|
|
||||||
const log = createLogger('aggregate-evaluator');
|
const log = createLogger('aggregate-evaluator');
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import type {
|
|||||||
WorkflowState,
|
WorkflowState,
|
||||||
RuleMatchMethod,
|
RuleMatchMethod,
|
||||||
} from '../../models/types.js';
|
} from '../../models/types.js';
|
||||||
import { detectRuleIndex, callAiJudge } from '../../../claude/client.js';
|
import type { AiJudgeCaller, RuleIndexDetector } from '../types.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
import { AggregateEvaluator } from './AggregateEvaluator.js';
|
import { AggregateEvaluator } from './AggregateEvaluator.js';
|
||||||
|
|
||||||
const log = createLogger('rule-evaluator');
|
const log = createLogger('rule-evaluator');
|
||||||
@ -27,6 +27,12 @@ export interface RuleEvaluatorContext {
|
|||||||
state: WorkflowState;
|
state: WorkflowState;
|
||||||
/** Working directory (for AI judge calls) */
|
/** Working directory (for AI judge calls) */
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
/** Whether interactive-only rules are enabled */
|
||||||
|
interactive?: boolean;
|
||||||
|
/** Rule tag index detector */
|
||||||
|
detectRuleIndex: RuleIndexDetector;
|
||||||
|
/** AI judge caller */
|
||||||
|
callAiJudge: AiJudgeCaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +56,7 @@ export class RuleEvaluator {
|
|||||||
|
|
||||||
async evaluate(agentContent: string, tagContent: string): Promise<RuleMatch | undefined> {
|
async evaluate(agentContent: string, tagContent: string): Promise<RuleMatch | undefined> {
|
||||||
if (!this.step.rules || this.step.rules.length === 0) return undefined;
|
if (!this.step.rules || this.step.rules.length === 0) return undefined;
|
||||||
|
const interactiveEnabled = this.ctx.interactive === true;
|
||||||
|
|
||||||
// 1. Aggregate conditions (all/any) — only meaningful for parallel parent steps
|
// 1. Aggregate conditions (all/any) — only meaningful for parallel parent steps
|
||||||
const aggEvaluator = new AggregateEvaluator(this.step, this.ctx.state);
|
const aggEvaluator = new AggregateEvaluator(this.step, this.ctx.state);
|
||||||
@ -60,17 +67,27 @@ export class RuleEvaluator {
|
|||||||
|
|
||||||
// 2. Tag detection from Phase 3 output
|
// 2. Tag detection from Phase 3 output
|
||||||
if (tagContent) {
|
if (tagContent) {
|
||||||
const ruleIndex = detectRuleIndex(tagContent, this.step.name);
|
const ruleIndex = this.ctx.detectRuleIndex(tagContent, this.step.name);
|
||||||
if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) {
|
if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) {
|
||||||
return { index: ruleIndex, method: 'phase3_tag' };
|
const rule = this.step.rules[ruleIndex];
|
||||||
|
if (rule?.interactiveOnly && !interactiveEnabled) {
|
||||||
|
// Skip interactive-only rule in non-interactive mode
|
||||||
|
} else {
|
||||||
|
return { index: ruleIndex, method: 'phase3_tag' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Tag detection from Phase 1 output (fallback)
|
// 3. Tag detection from Phase 1 output (fallback)
|
||||||
if (agentContent) {
|
if (agentContent) {
|
||||||
const ruleIndex = detectRuleIndex(agentContent, this.step.name);
|
const ruleIndex = this.ctx.detectRuleIndex(agentContent, this.step.name);
|
||||||
if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) {
|
if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) {
|
||||||
return { index: ruleIndex, method: 'phase1_tag' };
|
const rule = this.step.rules[ruleIndex];
|
||||||
|
if (rule?.interactiveOnly && !interactiveEnabled) {
|
||||||
|
// Skip interactive-only rule in non-interactive mode
|
||||||
|
} else {
|
||||||
|
return { index: ruleIndex, method: 'phase1_tag' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +116,9 @@ export class RuleEvaluator {
|
|||||||
const aiConditions: { index: number; text: string }[] = [];
|
const aiConditions: { index: number; text: string }[] = [];
|
||||||
for (let i = 0; i < this.step.rules.length; i++) {
|
for (let i = 0; i < this.step.rules.length; i++) {
|
||||||
const rule = this.step.rules[i]!;
|
const rule = this.step.rules[i]!;
|
||||||
|
if (rule.interactiveOnly && this.ctx.interactive !== true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (rule.isAiCondition && rule.aiConditionText) {
|
if (rule.isAiCondition && rule.aiConditionText) {
|
||||||
aiConditions.push({ index: i, text: rule.aiConditionText });
|
aiConditions.push({ index: i, text: rule.aiConditionText });
|
||||||
}
|
}
|
||||||
@ -112,7 +132,7 @@ export class RuleEvaluator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const judgeConditions = aiConditions.map((c, i) => ({ index: i, text: c.text }));
|
const judgeConditions = aiConditions.map((c, i) => ({ index: i, text: c.text }));
|
||||||
const judgeResult = await callAiJudge(agentOutput, judgeConditions, { cwd: this.ctx.cwd });
|
const judgeResult = await this.ctx.callAiJudge(agentOutput, judgeConditions, { cwd: this.ctx.cwd });
|
||||||
|
|
||||||
if (judgeResult >= 0 && judgeResult < aiConditions.length) {
|
if (judgeResult >= 0 && judgeResult < aiConditions.length) {
|
||||||
const matched = aiConditions[judgeResult]!;
|
const matched = aiConditions[judgeResult]!;
|
||||||
@ -136,14 +156,17 @@ export class RuleEvaluator {
|
|||||||
private async evaluateAllConditionsViaAiJudge(agentOutput: string): Promise<number> {
|
private async evaluateAllConditionsViaAiJudge(agentOutput: string): Promise<number> {
|
||||||
if (!this.step.rules || this.step.rules.length === 0) return -1;
|
if (!this.step.rules || this.step.rules.length === 0) return -1;
|
||||||
|
|
||||||
const conditions = this.step.rules.map((rule, i) => ({ index: i, text: rule.condition }));
|
const conditions = this.step.rules
|
||||||
|
.map((rule, i) => ({ index: i, text: rule.condition, interactiveOnly: rule.interactiveOnly }))
|
||||||
|
.filter((rule) => this.ctx.interactive === true || !rule.interactiveOnly)
|
||||||
|
.map((rule) => ({ index: rule.index, text: rule.text }));
|
||||||
|
|
||||||
log.debug('Evaluating all conditions via AI judge (final fallback)', {
|
log.debug('Evaluating all conditions via AI judge (final fallback)', {
|
||||||
step: this.step.name,
|
step: this.step.name,
|
||||||
conditionCount: conditions.length,
|
conditionCount: conditions.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const judgeResult = await callAiJudge(agentOutput, conditions, { cwd: this.ctx.cwd });
|
const judgeResult = await this.ctx.callAiJudge(agentOutput, conditions, { cwd: this.ctx.cwd });
|
||||||
|
|
||||||
if (judgeResult >= 0 && judgeResult < conditions.length) {
|
if (judgeResult >= 0 && judgeResult < conditions.length) {
|
||||||
log.debug('AI judge (fallback) matched condition', {
|
log.debug('AI judge (fallback) matched condition', {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export type {
|
|||||||
StreamEvent,
|
StreamEvent,
|
||||||
StreamCallback,
|
StreamCallback,
|
||||||
PermissionHandler,
|
PermissionHandler,
|
||||||
|
PermissionResult,
|
||||||
AskUserQuestionHandler,
|
AskUserQuestionHandler,
|
||||||
ProviderType,
|
ProviderType,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|||||||
@ -136,7 +136,12 @@ export class InstructionBuilder {
|
|||||||
|
|
||||||
// 7. Status Output Rules (for tag-based detection in Phase 1)
|
// 7. Status Output Rules (for tag-based detection in Phase 1)
|
||||||
if (hasTagBasedRules(this.step)) {
|
if (hasTagBasedRules(this.step)) {
|
||||||
const statusRulesPrompt = generateStatusRulesFromRules(this.step.name, this.step.rules!, language);
|
const statusRulesPrompt = generateStatusRulesFromRules(
|
||||||
|
this.step.name,
|
||||||
|
this.step.rules!,
|
||||||
|
language,
|
||||||
|
{ interactive: this.context.interactive },
|
||||||
|
);
|
||||||
sections.push(statusRulesPrompt);
|
sections.push(statusRulesPrompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,13 +22,17 @@ const REPORT_PHASE_STRINGS = {
|
|||||||
noSourceEdit: '**Do NOT modify project source files.** Only output report files.',
|
noSourceEdit: '**Do NOT modify project source files.** Only output report files.',
|
||||||
reportDirOnly: '**Use only the Report Directory files shown above.** Do not search or open reports outside that directory.',
|
reportDirOnly: '**Use only the Report Directory files shown above.** Do not search or open reports outside that directory.',
|
||||||
instructionBody: 'Output the results of your previous work as a report.',
|
instructionBody: 'Output the results of your previous work as a report.',
|
||||||
reportJsonFormat: 'Output a JSON object mapping each report file name to its content.',
|
reportJsonFormat: 'JSON format is optional. If you use JSON, map report file names to content (file name key only).',
|
||||||
|
reportPlainAllowed: 'You may output plain text. If there are multiple report files, the same content will be written to each file.',
|
||||||
|
reportOnlyOutput: 'Output only the report content (no status tags, no commentary).',
|
||||||
},
|
},
|
||||||
ja: {
|
ja: {
|
||||||
noSourceEdit: '**プロジェクトのソースファイルを変更しないでください。** レポートファイルのみ出力してください。',
|
noSourceEdit: '**プロジェクトのソースファイルを変更しないでください。** レポートファイルのみ出力してください。',
|
||||||
reportDirOnly: '**上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。',
|
reportDirOnly: '**上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。',
|
||||||
instructionBody: '前のステップの作業結果をレポートとして出力してください。',
|
instructionBody: '前のステップの作業結果をレポートとして出力してください。',
|
||||||
reportJsonFormat: 'レポートファイル名→内容のJSONオブジェクトで出力してください。',
|
reportJsonFormat: 'JSON形式は任意です。JSONを使う場合は「レポートファイル名→内容」のオブジェクトにしてください(キーはファイル名のみ)。',
|
||||||
|
reportPlainAllowed: '本文のみの出力も可です。複数ファイルの場合は同じ内容が各ファイルに書き込まれます。',
|
||||||
|
reportOnlyOutput: 'レポート本文のみを出力してください(ステータスタグやコメントは禁止)。',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -103,6 +107,8 @@ export class ReportInstructionBuilder {
|
|||||||
r.instructionBody,
|
r.instructionBody,
|
||||||
r.reportJsonFormat,
|
r.reportJsonFormat,
|
||||||
];
|
];
|
||||||
|
instrParts.push(r.reportPlainAllowed);
|
||||||
|
instrParts.push(r.reportOnlyOutput);
|
||||||
|
|
||||||
// Report output instruction (auto-generated or explicit order)
|
// Report output instruction (auto-generated or explicit order)
|
||||||
const reportContext: InstructionContext = {
|
const reportContext: InstructionContext = {
|
||||||
|
|||||||
@ -28,6 +28,8 @@ const STATUS_JUDGMENT_STRINGS = {
|
|||||||
export interface StatusJudgmentContext {
|
export interface StatusJudgmentContext {
|
||||||
/** Language */
|
/** Language */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
|
/** Whether interactive-only rules are enabled */
|
||||||
|
interactive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +54,12 @@ export class StatusJudgmentBuilder {
|
|||||||
sections.push(s.header);
|
sections.push(s.header);
|
||||||
|
|
||||||
// Status rules (criteria table + output format)
|
// Status rules (criteria table + output format)
|
||||||
const generatedPrompt = generateStatusRulesFromRules(this.step.name, this.step.rules, language);
|
const generatedPrompt = generateStatusRulesFromRules(
|
||||||
|
this.step.name,
|
||||||
|
this.step.rules,
|
||||||
|
language,
|
||||||
|
{ interactive: this.context.interactive },
|
||||||
|
);
|
||||||
sections.push(generatedPrompt);
|
sections.push(generatedPrompt);
|
||||||
|
|
||||||
return sections.join('\n\n');
|
return sections.join('\n\n');
|
||||||
|
|||||||
@ -31,6 +31,8 @@ export interface InstructionContext {
|
|||||||
reportDir?: string;
|
reportDir?: string;
|
||||||
/** Language for metadata rendering. Defaults to 'en'. */
|
/** Language for metadata rendering. Defaults to 'en'. */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
|
/** Whether interactive-only rules are enabled */
|
||||||
|
interactive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Execution environment metadata prepended to agent instructions */
|
/** Execution environment metadata prepended to agent instructions */
|
||||||
|
|||||||
@ -47,9 +47,14 @@ export function generateStatusRulesFromRules(
|
|||||||
stepName: string,
|
stepName: string,
|
||||||
rules: WorkflowRule[],
|
rules: WorkflowRule[],
|
||||||
language: Language,
|
language: Language,
|
||||||
|
options?: { interactive?: boolean },
|
||||||
): string {
|
): string {
|
||||||
const tag = stepName.toUpperCase();
|
const tag = stepName.toUpperCase();
|
||||||
const strings = RULES_PROMPT_STRINGS[language];
|
const strings = RULES_PROMPT_STRINGS[language];
|
||||||
|
const interactiveEnabled = options?.interactive;
|
||||||
|
const visibleRules = rules
|
||||||
|
.map((rule, index) => ({ rule, index }))
|
||||||
|
.filter(({ rule }) => interactiveEnabled !== false || !rule.interactiveOnly);
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
@ -58,8 +63,8 @@ export function generateStatusRulesFromRules(
|
|||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`);
|
lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`);
|
||||||
lines.push('|---|------|------|');
|
lines.push('|---|------|------|');
|
||||||
for (const [i, rule] of rules.entries()) {
|
for (const { rule, index } of visibleRules) {
|
||||||
lines.push(`| ${i + 1} | ${rule.condition} | \`[${tag}:${i + 1}]\` |`);
|
lines.push(`| ${index + 1} | ${rule.condition} | \`[${tag}:${index + 1}]\` |`);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
@ -68,18 +73,18 @@ export function generateStatusRulesFromRules(
|
|||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(strings.outputInstruction);
|
lines.push(strings.outputInstruction);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
for (const [i, rule] of rules.entries()) {
|
for (const { rule, index } of visibleRules) {
|
||||||
lines.push(`- \`[${tag}:${i + 1}]\` — ${rule.condition}`);
|
lines.push(`- \`[${tag}:${index + 1}]\` — ${rule.condition}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appendix templates (if any rules have appendix)
|
// Appendix templates (if any rules have appendix)
|
||||||
const rulesWithAppendix = rules.filter((r) => r.appendix);
|
const rulesWithAppendix = visibleRules.filter(({ rule }) => rule.appendix);
|
||||||
if (rulesWithAppendix.length > 0) {
|
if (rulesWithAppendix.length > 0) {
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(strings.appendixHeading);
|
lines.push(strings.appendixHeading);
|
||||||
for (const [i, rule] of rules.entries()) {
|
for (const { rule, index } of visibleRules) {
|
||||||
if (!rule.appendix) continue;
|
if (!rule.appendix) continue;
|
||||||
const tagStr = `[${tag}:${i + 1}]`;
|
const tagStr = `[${tag}:${index + 1}]`;
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(strings.appendixInstruction.replace('{tag}', tagStr));
|
lines.push(strings.appendixInstruction.replace('{tag}', tagStr));
|
||||||
lines.push('```');
|
lines.push('```');
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder
|
|||||||
import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js';
|
import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js';
|
||||||
import { hasTagBasedRules } from './evaluation/rule-utils.js';
|
import { hasTagBasedRules } from './evaluation/rule-utils.js';
|
||||||
import { isReportObjectConfig } from './instruction/InstructionBuilder.js';
|
import { isReportObjectConfig } from './instruction/InstructionBuilder.js';
|
||||||
import { createLogger } from '../../shared/utils/debug.js';
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
|
|
||||||
const log = createLogger('phase-runner');
|
const log = createLogger('phase-runner');
|
||||||
|
|
||||||
@ -24,6 +24,8 @@ export interface PhaseRunnerContext {
|
|||||||
reportDir: string;
|
reportDir: string;
|
||||||
/** Language for instructions */
|
/** Language for instructions */
|
||||||
language?: Language;
|
language?: Language;
|
||||||
|
/** Whether interactive-only rules are enabled */
|
||||||
|
interactive?: boolean;
|
||||||
/** Get agent session ID */
|
/** Get agent session ID */
|
||||||
getSessionId: (agent: string) => string | undefined;
|
getSessionId: (agent: string) => string | undefined;
|
||||||
/** Build resume options for a step */
|
/** Build resume options for a step */
|
||||||
@ -76,6 +78,7 @@ function getReportFiles(report: WorkflowStep['report']): string[] {
|
|||||||
|
|
||||||
function resolveReportOutputs(
|
function resolveReportOutputs(
|
||||||
report: WorkflowStep['report'],
|
report: WorkflowStep['report'],
|
||||||
|
reportDir: string,
|
||||||
content: string,
|
content: string,
|
||||||
): Map<string, string> {
|
): Map<string, string> {
|
||||||
if (!report) return new Map();
|
if (!report) return new Map();
|
||||||
@ -83,12 +86,17 @@ function resolveReportOutputs(
|
|||||||
const files = getReportFiles(report);
|
const files = getReportFiles(report);
|
||||||
const json = parseReportJson(content);
|
const json = parseReportJson(content);
|
||||||
if (!json) {
|
if (!json) {
|
||||||
throw new Error('Report output must be a JSON object mapping report file names to content.');
|
const raw = content;
|
||||||
|
if (!raw || raw.trim().length === 0) {
|
||||||
|
throw new Error('Report output is empty.');
|
||||||
|
}
|
||||||
|
return new Map(files.map((file) => [file, raw]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputs = new Map<string, string>();
|
const outputs = new Map<string, string>();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const value = json[file];
|
const absolutePath = resolve(reportDir, file);
|
||||||
|
const value = json[file] ?? json[absolutePath];
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
throw new Error(`Report output missing content for file: ${file}`);
|
throw new Error(`Report output missing content for file: ${file}`);
|
||||||
}
|
}
|
||||||
@ -142,7 +150,7 @@ export async function runReportPhase(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const reportResponse = await runAgent(step.agent, reportInstruction, reportOptions);
|
const reportResponse = await runAgent(step.agent, reportInstruction, reportOptions);
|
||||||
const outputs = resolveReportOutputs(step.report, reportResponse.content);
|
const outputs = resolveReportOutputs(step.report, ctx.reportDir, reportResponse.content);
|
||||||
for (const [fileName, content] of outputs.entries()) {
|
for (const [fileName, content] of outputs.entries()) {
|
||||||
writeReportFile(ctx.reportDir, fileName, content);
|
writeReportFile(ctx.reportDir, fileName, content);
|
||||||
}
|
}
|
||||||
@ -171,6 +179,7 @@ export async function runStatusJudgmentPhase(
|
|||||||
|
|
||||||
const judgmentInstruction = new StatusJudgmentBuilder(step, {
|
const judgmentInstruction = new StatusJudgmentBuilder(step, {
|
||||||
language: ctx.language,
|
language: ctx.language,
|
||||||
|
interactive: ctx.interactive,
|
||||||
}).build();
|
}).build();
|
||||||
|
|
||||||
const judgmentOptions = ctx.buildResumeOptions(step, sessionId, {
|
const judgmentOptions = ctx.buildResumeOptions(step, sessionId, {
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
* used by the workflow execution engine.
|
* used by the workflow execution engine.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js';
|
import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js';
|
||||||
import type { PermissionResult } from '../../claude/types.js';
|
|
||||||
|
|
||||||
export type ProviderType = 'claude' | 'codex' | 'mock';
|
export type ProviderType = 'claude' | 'codex' | 'mock';
|
||||||
|
|
||||||
@ -66,11 +66,13 @@ export type StreamCallback = (event: StreamEvent) => void;
|
|||||||
export interface PermissionRequest {
|
export interface PermissionRequest {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
input: Record<string, unknown>;
|
input: Record<string, unknown>;
|
||||||
suggestions?: Array<Record<string, unknown>>;
|
suggestions?: PermissionUpdate[];
|
||||||
blockedPath?: string;
|
blockedPath?: string;
|
||||||
decisionReason?: string;
|
decisionReason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { PermissionResult, PermissionUpdate };
|
||||||
|
|
||||||
export type PermissionHandler = (request: PermissionRequest) => Promise<PermissionResult>;
|
export type PermissionHandler = (request: PermissionRequest) => Promise<PermissionResult>;
|
||||||
|
|
||||||
export interface AskUserQuestionInput {
|
export interface AskUserQuestionInput {
|
||||||
@ -89,6 +91,19 @@ export type AskUserQuestionHandler = (
|
|||||||
input: AskUserQuestionInput
|
input: AskUserQuestionInput
|
||||||
) => Promise<Record<string, string>>;
|
) => Promise<Record<string, string>>;
|
||||||
|
|
||||||
|
export type RuleIndexDetector = (content: string, stepName: string) => number;
|
||||||
|
|
||||||
|
export interface AiJudgeCondition {
|
||||||
|
index: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AiJudgeCaller = (
|
||||||
|
agentOutput: string,
|
||||||
|
conditions: AiJudgeCondition[],
|
||||||
|
options: { cwd: string }
|
||||||
|
) => Promise<number>;
|
||||||
|
|
||||||
/** Events emitted by workflow engine */
|
/** Events emitted by workflow engine */
|
||||||
export interface WorkflowEvents {
|
export interface WorkflowEvents {
|
||||||
'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void;
|
'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void;
|
||||||
@ -157,6 +172,12 @@ export interface WorkflowEngineOptions {
|
|||||||
language?: Language;
|
language?: Language;
|
||||||
provider?: ProviderType;
|
provider?: ProviderType;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** Enable interactive-only rules and user-input transitions */
|
||||||
|
interactive?: boolean;
|
||||||
|
/** Rule tag index detector (required for rules evaluation) */
|
||||||
|
detectRuleIndex?: RuleIndexDetector;
|
||||||
|
/** AI judge caller (required for rules evaluation) */
|
||||||
|
callAiJudge?: AiJudgeCaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loop detection result */
|
/** Loop detection result */
|
||||||
|
|||||||
@ -7,8 +7,13 @@
|
|||||||
|
|
||||||
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||||
import { join, dirname } from 'node:path';
|
import { join, dirname } from 'node:path';
|
||||||
import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../../infra/config/paths.js';
|
import {
|
||||||
import { getLanguage } from '../../infra/config/global/globalConfig.js';
|
getGlobalWorkflowsDir,
|
||||||
|
getGlobalAgentsDir,
|
||||||
|
getBuiltinWorkflowsDir,
|
||||||
|
getBuiltinAgentsDir,
|
||||||
|
getLanguage,
|
||||||
|
} from '../../infra/config/index.js';
|
||||||
import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js';
|
import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -7,15 +7,15 @@
|
|||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { info, success } from '../../shared/ui/index.js';
|
import { info, success } from '../../shared/ui/index.js';
|
||||||
import { selectOption } from '../../prompt/index.js';
|
import { selectOption } from '../../shared/prompt/index.js';
|
||||||
import {
|
import {
|
||||||
loadProjectConfig,
|
loadProjectConfig,
|
||||||
updateProjectConfig,
|
updateProjectConfig,
|
||||||
type PermissionMode,
|
} from '../../infra/config/index.js';
|
||||||
} from '../../infra/config/project/projectConfig.js';
|
import type { PermissionMode } from '../../infra/config/index.js';
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
export type { PermissionMode } from '../../infra/config/project/projectConfig.js';
|
export type { PermissionMode } from '../../infra/config/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get permission mode options for selection
|
* Get permission mode options for selection
|
||||||
|
|||||||
@ -2,10 +2,9 @@
|
|||||||
* Workflow switching command
|
* Workflow switching command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { listWorkflows, loadWorkflow } from '../../infra/config/loaders/workflowLoader.js';
|
import { listWorkflows, loadWorkflow, getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/index.js';
|
||||||
import { getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/paths.js';
|
|
||||||
import { info, success, error } from '../../shared/ui/index.js';
|
import { info, success, error } from '../../shared/ui/index.js';
|
||||||
import { selectOption } from '../../prompt/index.js';
|
import { selectOption } from '../../shared/prompt/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all available workflow options
|
* Get all available workflow options
|
||||||
|
|||||||
@ -15,14 +15,12 @@ import { existsSync, readFileSync } from 'node:fs';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import type { Language } from '../../core/models/index.js';
|
import type { Language } from '../../core/models/index.js';
|
||||||
import { loadGlobalConfig } from '../../infra/config/global/globalConfig.js';
|
import { loadGlobalConfig, loadAgentSessions, updateAgentSession } from '../../infra/config/index.js';
|
||||||
import { isQuietMode } from '../../context.js';
|
import { isQuietMode } from '../../shared/context.js';
|
||||||
import { loadAgentSessions, updateAgentSession } from '../../infra/config/paths.js';
|
|
||||||
import { getProvider, type ProviderType } from '../../infra/providers/index.js';
|
import { getProvider, type ProviderType } from '../../infra/providers/index.js';
|
||||||
import { selectOption } from '../../prompt/index.js';
|
import { selectOption } from '../../shared/prompt/index.js';
|
||||||
import { getLanguageResourcesDir } from '../../resources/index.js';
|
import { getLanguageResourcesDir } from '../../infra/resources/index.js';
|
||||||
import { createLogger } from '../../shared/utils/debug.js';
|
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../../shared/utils/error.js';
|
|
||||||
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
|
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
|
||||||
const log = createLogger('interactive');
|
const log = createLogger('interactive');
|
||||||
|
|
||||||
@ -363,14 +361,18 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle slash commands
|
// Handle slash commands
|
||||||
if (trimmed === '/go') {
|
if (trimmed.startsWith('/go')) {
|
||||||
const summaryPrompt = buildSummaryPrompt(
|
const userNote = trimmed.slice(3).trim();
|
||||||
|
let summaryPrompt = buildSummaryPrompt(
|
||||||
history,
|
history,
|
||||||
!!sessionId,
|
!!sessionId,
|
||||||
prompts.summaryPrompt,
|
prompts.summaryPrompt,
|
||||||
prompts.noTranscript,
|
prompts.noTranscript,
|
||||||
prompts.conversationLabel,
|
prompts.conversationLabel,
|
||||||
);
|
);
|
||||||
|
if (summaryPrompt && userNote) {
|
||||||
|
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
|
||||||
|
}
|
||||||
if (!summaryPrompt) {
|
if (!summaryPrompt) {
|
||||||
info(prompts.ui.noConversation);
|
info(prompts.ui.noConversation);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -10,22 +10,27 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { fetchIssue, formatIssueAsTask, checkGhCli } from '../../infra/github/issue.js';
|
import {
|
||||||
import type { GitHubIssue } from '../../infra/github/types.js';
|
fetchIssue,
|
||||||
import { createPullRequest, pushBranch, buildPrBody } from '../../infra/github/pr.js';
|
formatIssueAsTask,
|
||||||
import { stageAndCommit } from '../../infra/task/git.js';
|
checkGhCli,
|
||||||
|
createPullRequest,
|
||||||
|
pushBranch,
|
||||||
|
buildPrBody,
|
||||||
|
type GitHubIssue,
|
||||||
|
} from '../../infra/github/index.js';
|
||||||
|
import { stageAndCommit } from '../../infra/task/index.js';
|
||||||
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
|
||||||
import { loadGlobalConfig } from '../../infra/config/global/globalConfig.js';
|
import { loadGlobalConfig } from '../../infra/config/index.js';
|
||||||
import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
|
import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
|
||||||
import { createLogger } from '../../shared/utils/debug.js';
|
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../../shared/utils/error.js';
|
|
||||||
import type { PipelineConfig } from '../../core/models/index.js';
|
import type { PipelineConfig } from '../../core/models/index.js';
|
||||||
import {
|
import {
|
||||||
EXIT_ISSUE_FETCH_FAILED,
|
EXIT_ISSUE_FETCH_FAILED,
|
||||||
EXIT_WORKFLOW_FAILED,
|
EXIT_WORKFLOW_FAILED,
|
||||||
EXIT_GIT_OPERATION_FAILED,
|
EXIT_GIT_OPERATION_FAILED,
|
||||||
EXIT_PR_CREATION_FAILED,
|
EXIT_PR_CREATION_FAILED,
|
||||||
} from '../../exitCodes.js';
|
} from '../../shared/exitCodes.js';
|
||||||
|
|
||||||
export type { PipelineExecutionOptions };
|
export type { PipelineExecutionOptions };
|
||||||
|
|
||||||
|
|||||||
@ -8,18 +8,14 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { stringify as stringifyYaml } from 'yaml';
|
import { stringify as stringifyYaml } from 'yaml';
|
||||||
import { promptInput, confirm, selectOption } from '../../../prompt/index.js';
|
import { promptInput, confirm, selectOption } from '../../../shared/prompt/index.js';
|
||||||
import { success, info } from '../../../shared/ui/index.js';
|
import { success, info } from '../../../shared/ui/index.js';
|
||||||
import { summarizeTaskName } from '../../../infra/task/summarize.js';
|
import { summarizeTaskName, type TaskFileData } from '../../../infra/task/index.js';
|
||||||
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js';
|
import { loadGlobalConfig, listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
|
||||||
import { getProvider, type ProviderType } from '../../../infra/providers/index.js';
|
import { getProvider, type ProviderType } from '../../../infra/providers/index.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/index.js';
|
||||||
import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js';
|
|
||||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
|
||||||
import { interactiveMode } from '../../interactive/index.js';
|
import { interactiveMode } from '../../interactive/index.js';
|
||||||
import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/issue.js';
|
|
||||||
import type { TaskFileData } from '../../../infra/task/schema.js';
|
|
||||||
|
|
||||||
const log = createLogger('add-task');
|
const log = createLogger('add-task');
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,13 @@
|
|||||||
* mixing CLI parsing with business logic.
|
* mixing CLI parsing with business logic.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
import { getCurrentWorkflow, listWorkflows, isWorkflowPath } from '../../../infra/config/index.js';
|
||||||
import { listWorkflows, isWorkflowPath } from '../../../infra/config/loaders/workflowLoader.js';
|
import { selectOptionWithDefault, confirm } from '../../../shared/prompt/index.js';
|
||||||
import { selectOptionWithDefault, confirm } from '../../../prompt/index.js';
|
import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
|
||||||
import { createSharedClone } from '../../../infra/task/clone.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||||
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js';
|
|
||||||
import { summarizeTaskName } from '../../../infra/task/summarize.js';
|
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
|
|
||||||
import { info, error, success } from '../../../shared/ui/index.js';
|
import { info, error, success } from '../../../shared/ui/index.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
import { createPullRequest, buildPrBody } from '../../../infra/github/pr.js';
|
import { createPullRequest, buildPrBody } from '../../../infra/github/index.js';
|
||||||
import { executeTask } from './taskExecution.js';
|
import { executeTask } from './taskExecution.js';
|
||||||
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
||||||
|
|
||||||
@ -136,6 +133,7 @@ export async function selectAndExecuteTask(
|
|||||||
workflowIdentifier,
|
workflowIdentifier,
|
||||||
projectCwd: cwd,
|
projectCwd: cwd,
|
||||||
agentOverrides,
|
agentOverrides,
|
||||||
|
interactiveUserInput: options?.interactiveUserInput === true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (taskSuccess && isWorktree) {
|
if (taskSuccess && isWorktree) {
|
||||||
|
|||||||
@ -2,8 +2,7 @@
|
|||||||
* Session management helpers for agent execution
|
* Session management helpers for agent execution
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadAgentSessions, updateAgentSession } from '../../../infra/config/paths.js';
|
import { loadAgentSessions, updateAgentSession, loadGlobalConfig } from '../../../infra/config/index.js';
|
||||||
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js';
|
|
||||||
import type { AgentResponse } from '../../../core/models/index.js';
|
import type { AgentResponse } from '../../../core/models/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3,10 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../../infra/config/index.js';
|
import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../../infra/config/index.js';
|
||||||
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
import { TaskRunner, type TaskInfo, createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
|
||||||
import { createSharedClone } from '../../../infra/task/clone.js';
|
|
||||||
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js';
|
|
||||||
import { summarizeTaskName } from '../../../infra/task/summarize.js';
|
|
||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
info,
|
info,
|
||||||
@ -15,10 +12,9 @@ import {
|
|||||||
status,
|
status,
|
||||||
blankLine,
|
blankLine,
|
||||||
} from '../../../shared/ui/index.js';
|
} from '../../../shared/ui/index.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
|
||||||
import { executeWorkflow } from './workflowExecution.js';
|
import { executeWorkflow } from './workflowExecution.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||||
import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js';
|
import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js';
|
||||||
|
|
||||||
export type { TaskExecutionOptions, ExecuteTaskOptions };
|
export type { TaskExecutionOptions, ExecuteTaskOptions };
|
||||||
@ -29,7 +25,7 @@ const log = createLogger('task');
|
|||||||
* Execute a single task with workflow.
|
* Execute a single task with workflow.
|
||||||
*/
|
*/
|
||||||
export async function executeTask(options: ExecuteTaskOptions): Promise<boolean> {
|
export async function executeTask(options: ExecuteTaskOptions): Promise<boolean> {
|
||||||
const { task, cwd, workflowIdentifier, projectCwd, agentOverrides } = options;
|
const { task, cwd, workflowIdentifier, projectCwd, agentOverrides, interactiveUserInput } = options;
|
||||||
const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, projectCwd);
|
const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, projectCwd);
|
||||||
|
|
||||||
if (!workflowConfig) {
|
if (!workflowConfig) {
|
||||||
@ -54,6 +50,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<boolean>
|
|||||||
language: globalConfig.language,
|
language: globalConfig.language,
|
||||||
provider: agentOverrides?.provider,
|
provider: agentOverrides?.provider,
|
||||||
model: agentOverrides?.model,
|
model: agentOverrides?.model,
|
||||||
|
interactiveUserInput,
|
||||||
});
|
});
|
||||||
return result.success;
|
return result.success;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,8 @@ export interface WorkflowExecutionOptions {
|
|||||||
language?: Language;
|
language?: Language;
|
||||||
provider?: ProviderType;
|
provider?: ProviderType;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** Enable interactive user input during step transitions */
|
||||||
|
interactiveUserInput?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskExecutionOptions {
|
export interface TaskExecutionOptions {
|
||||||
@ -39,6 +41,8 @@ export interface ExecuteTaskOptions {
|
|||||||
projectCwd: string;
|
projectCwd: string;
|
||||||
/** Agent provider/model overrides */
|
/** Agent provider/model overrides */
|
||||||
agentOverrides?: TaskExecutionOptions;
|
agentOverrides?: TaskExecutionOptions;
|
||||||
|
/** Enable interactive user input during step transitions */
|
||||||
|
interactiveUserInput?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PipelineExecutionOptions {
|
export interface PipelineExecutionOptions {
|
||||||
@ -73,4 +77,6 @@ export interface SelectAndExecuteOptions {
|
|||||||
repo?: string;
|
repo?: string;
|
||||||
workflow?: string;
|
workflow?: string;
|
||||||
createWorktree?: boolean | undefined;
|
createWorktree?: boolean | undefined;
|
||||||
|
/** Enable interactive user input during step transitions */
|
||||||
|
interactiveUserInput?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { WorkflowEngine, type IterationLimitRequest } from '../../../core/workflow/index.js';
|
import { WorkflowEngine, type IterationLimitRequest, type UserInputRequest } from '../../../core/workflow/index.js';
|
||||||
import type { WorkflowConfig } from '../../../core/models/index.js';
|
import type { WorkflowConfig } from '../../../core/models/index.js';
|
||||||
import type { WorkflowExecutionResult, WorkflowExecutionOptions } from './types.js';
|
import type { WorkflowExecutionResult, WorkflowExecutionOptions } from './types.js';
|
||||||
|
import { callAiJudge, detectRuleIndex, interruptAllQueries } from '../../../infra/claude/index.js';
|
||||||
|
|
||||||
export type { WorkflowExecutionResult, WorkflowExecutionOptions };
|
export type { WorkflowExecutionResult, WorkflowExecutionOptions };
|
||||||
|
|
||||||
@ -14,9 +15,9 @@ import {
|
|||||||
updateAgentSession,
|
updateAgentSession,
|
||||||
loadWorktreeSessions,
|
loadWorktreeSessions,
|
||||||
updateWorktreeSession,
|
updateWorktreeSession,
|
||||||
} from '../../../infra/config/paths.js';
|
loadGlobalConfig,
|
||||||
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js';
|
} from '../../../infra/config/index.js';
|
||||||
import { isQuietMode } from '../../../context.js';
|
import { isQuietMode } from '../../../shared/context.js';
|
||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
info,
|
info,
|
||||||
@ -38,11 +39,10 @@ import {
|
|||||||
type NdjsonStepComplete,
|
type NdjsonStepComplete,
|
||||||
type NdjsonWorkflowComplete,
|
type NdjsonWorkflowComplete,
|
||||||
type NdjsonWorkflowAbort,
|
type NdjsonWorkflowAbort,
|
||||||
} from '../../../infra/fs/session.js';
|
} from '../../../infra/fs/index.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger, notifySuccess, notifyError } from '../../../shared/utils/index.js';
|
||||||
import { notifySuccess, notifyError } from '../../../shared/utils/notification.js';
|
import { selectOption, promptInput } from '../../../shared/prompt/index.js';
|
||||||
import { selectOption, promptInput } from '../../../prompt/index.js';
|
import { EXIT_SIGINT } from '../../../shared/exitCodes.js';
|
||||||
import { EXIT_SIGINT } from '../../../exitCodes.js';
|
|
||||||
|
|
||||||
const log = createLogger('workflow');
|
const log = createLogger('workflow');
|
||||||
|
|
||||||
@ -75,6 +75,7 @@ export async function executeWorkflow(
|
|||||||
): Promise<WorkflowExecutionResult> {
|
): Promise<WorkflowExecutionResult> {
|
||||||
const {
|
const {
|
||||||
headerPrefix = 'Running Workflow:',
|
headerPrefix = 'Running Workflow:',
|
||||||
|
interactiveUserInput = false,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// projectCwd is where .takt/ lives (project root, not the clone)
|
// projectCwd is where .takt/ lives (project root, not the clone)
|
||||||
@ -164,8 +165,22 @@ export async function executeWorkflow(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUserInput = interactiveUserInput
|
||||||
|
? async (request: UserInputRequest): Promise<string | null> => {
|
||||||
|
if (displayRef.current) {
|
||||||
|
displayRef.current.flush();
|
||||||
|
displayRef.current = null;
|
||||||
|
}
|
||||||
|
blankLine();
|
||||||
|
info(request.prompt.trim());
|
||||||
|
const input = await promptInput('追加の指示を入力してください(空で中止)');
|
||||||
|
return input && input.trim() ? input.trim() : null;
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const engine = new WorkflowEngine(workflowConfig, cwd, task, {
|
const engine = new WorkflowEngine(workflowConfig, cwd, task, {
|
||||||
onStream: streamHandler,
|
onStream: streamHandler,
|
||||||
|
onUserInput,
|
||||||
initialSessions: savedSessions,
|
initialSessions: savedSessions,
|
||||||
onSessionUpdate: sessionUpdateHandler,
|
onSessionUpdate: sessionUpdateHandler,
|
||||||
onIterationLimit: iterationLimitHandler,
|
onIterationLimit: iterationLimitHandler,
|
||||||
@ -173,6 +188,9 @@ export async function executeWorkflow(
|
|||||||
language: options.language,
|
language: options.language,
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
model: options.model,
|
model: options.model,
|
||||||
|
interactive: interactiveUserInput,
|
||||||
|
detectRuleIndex,
|
||||||
|
callAiJudge,
|
||||||
});
|
});
|
||||||
|
|
||||||
let abortReason: string | undefined;
|
let abortReason: string | undefined;
|
||||||
@ -288,6 +306,7 @@ export async function executeWorkflow(
|
|||||||
});
|
});
|
||||||
|
|
||||||
engine.on('workflow:abort', (state, reason) => {
|
engine.on('workflow:abort', (state, reason) => {
|
||||||
|
interruptAllQueries();
|
||||||
log.error('Workflow aborted', { reason, iterations: state.iteration });
|
log.error('Workflow aborted', { reason, iterations: state.iteration });
|
||||||
if (displayRef.current) {
|
if (displayRef.current) {
|
||||||
displayRef.current.flush();
|
displayRef.current.flush();
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import {
|
|||||||
detectDefaultBranch,
|
detectDefaultBranch,
|
||||||
listTaktBranches,
|
listTaktBranches,
|
||||||
buildListItems,
|
buildListItems,
|
||||||
} from '../../../infra/task/branchList.js';
|
} from '../../../infra/task/index.js';
|
||||||
import { selectOption, confirm } from '../../../prompt/index.js';
|
import { selectOption, confirm } from '../../../shared/prompt/index.js';
|
||||||
import { info } from '../../../shared/ui/index.js';
|
import { info } from '../../../shared/ui/index.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger } from '../../../shared/utils/index.js';
|
||||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||||
import {
|
import {
|
||||||
type ListAction,
|
type ListAction,
|
||||||
|
|||||||
@ -12,21 +12,19 @@ import {
|
|||||||
removeClone,
|
removeClone,
|
||||||
removeCloneMeta,
|
removeCloneMeta,
|
||||||
cleanupOrphanedClone,
|
cleanupOrphanedClone,
|
||||||
} from '../../../infra/task/clone.js';
|
} from '../../../infra/task/index.js';
|
||||||
import {
|
import {
|
||||||
detectDefaultBranch,
|
detectDefaultBranch,
|
||||||
type BranchListItem,
|
type BranchListItem,
|
||||||
} from '../../../infra/task/branchList.js';
|
autoCommitAndPush,
|
||||||
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js';
|
} from '../../../infra/task/index.js';
|
||||||
import { selectOption, promptInput } from '../../../prompt/index.js';
|
import { selectOption, promptInput } from '../../../shared/prompt/index.js';
|
||||||
import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js';
|
import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js';
|
||||||
import { createLogger } from '../../../shared/utils/debug.js';
|
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
|
||||||
import { executeTask } from '../execute/taskExecution.js';
|
import { executeTask } from '../execute/taskExecution.js';
|
||||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||||
import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js';
|
import { listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
|
||||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
|
|
||||||
|
|
||||||
const log = createLogger('list-tasks');
|
const log = createLogger('list-tasks');
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,8 @@
|
|||||||
* Stays resident until Ctrl+C (SIGINT).
|
* Stays resident until Ctrl+C (SIGINT).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
import { TaskRunner, type TaskInfo, TaskWatcher } from '../../../infra/task/index.js';
|
||||||
import { TaskWatcher } from '../../../infra/task/watcher.js';
|
import { getCurrentWorkflow } from '../../../infra/config/index.js';
|
||||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
|
||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
info,
|
info,
|
||||||
@ -16,7 +15,7 @@ import {
|
|||||||
blankLine,
|
blankLine,
|
||||||
} from '../../../shared/ui/index.js';
|
} from '../../../shared/ui/index.js';
|
||||||
import { executeAndCompleteTask } from '../execute/taskExecution.js';
|
import { executeAndCompleteTask } from '../execute/taskExecution.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
47
src/index.ts
47
src/index.ts
@ -7,8 +7,39 @@
|
|||||||
// Models
|
// Models
|
||||||
export * from './core/models/index.js';
|
export * from './core/models/index.js';
|
||||||
|
|
||||||
// Configuration
|
// Configuration (PermissionMode excluded to avoid name conflict with core/models PermissionMode)
|
||||||
export * from './infra/config/index.js';
|
export * from './infra/config/paths.js';
|
||||||
|
export * from './infra/config/loaders/index.js';
|
||||||
|
export * from './infra/config/global/index.js';
|
||||||
|
export {
|
||||||
|
loadProjectConfig,
|
||||||
|
saveProjectConfig,
|
||||||
|
updateProjectConfig,
|
||||||
|
getCurrentWorkflow,
|
||||||
|
setCurrentWorkflow,
|
||||||
|
isVerboseMode,
|
||||||
|
type ProjectPermissionMode,
|
||||||
|
type ProjectLocalConfig,
|
||||||
|
writeFileAtomic,
|
||||||
|
getInputHistoryPath,
|
||||||
|
MAX_INPUT_HISTORY,
|
||||||
|
loadInputHistory,
|
||||||
|
saveInputHistory,
|
||||||
|
addToInputHistory,
|
||||||
|
type AgentSessionData,
|
||||||
|
getAgentSessionsPath,
|
||||||
|
loadAgentSessions,
|
||||||
|
saveAgentSessions,
|
||||||
|
updateAgentSession,
|
||||||
|
clearAgentSessions,
|
||||||
|
getWorktreeSessionsDir,
|
||||||
|
encodeWorktreePath,
|
||||||
|
getWorktreeSessionPath,
|
||||||
|
loadWorktreeSessions,
|
||||||
|
updateWorktreeSession,
|
||||||
|
getClaudeProjectSessionsDir,
|
||||||
|
clearClaudeProjectSessions,
|
||||||
|
} from './infra/config/project/index.js';
|
||||||
|
|
||||||
// Claude integration
|
// Claude integration
|
||||||
export {
|
export {
|
||||||
@ -40,7 +71,7 @@ export {
|
|||||||
detectJudgeIndex,
|
detectJudgeIndex,
|
||||||
buildJudgePrompt,
|
buildJudgePrompt,
|
||||||
isRegexSafe,
|
isRegexSafe,
|
||||||
} from './claude/index.js';
|
} from './infra/claude/index.js';
|
||||||
export type {
|
export type {
|
||||||
StreamEvent,
|
StreamEvent,
|
||||||
StreamCallback,
|
StreamCallback,
|
||||||
@ -60,10 +91,10 @@ export type {
|
|||||||
ThinkingEventData,
|
ThinkingEventData,
|
||||||
ResultEventData,
|
ResultEventData,
|
||||||
ErrorEventData,
|
ErrorEventData,
|
||||||
} from './claude/index.js';
|
} from './infra/claude/index.js';
|
||||||
|
|
||||||
// Codex integration
|
// Codex integration
|
||||||
export * from './codex/index.js';
|
export * from './infra/codex/index.js';
|
||||||
|
|
||||||
// Agent execution
|
// Agent execution
|
||||||
export * from './agents/index.js';
|
export * from './agents/index.js';
|
||||||
@ -117,6 +148,10 @@ export type {
|
|||||||
// Utilities
|
// Utilities
|
||||||
export * from './shared/utils/index.js';
|
export * from './shared/utils/index.js';
|
||||||
export * from './shared/ui/index.js';
|
export * from './shared/ui/index.js';
|
||||||
|
export * from './shared/prompt/index.js';
|
||||||
|
export * from './shared/constants.js';
|
||||||
|
export * from './shared/context.js';
|
||||||
|
export * from './shared/exitCodes.js';
|
||||||
|
|
||||||
// Resources (embedded prompts and templates)
|
// Resources (embedded prompts and templates)
|
||||||
export * from './resources/index.js';
|
export * from './infra/resources/index.js';
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
import { executeClaudeCli } from './process.js';
|
import { executeClaudeCli } from './process.js';
|
||||||
import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js';
|
import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js';
|
||||||
import type { AgentResponse, Status } from '../core/models/index.js';
|
import type { AgentResponse, Status } from '../../core/models/index.js';
|
||||||
import { createLogger } from '../shared/utils/debug.js';
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
|
|
||||||
// Re-export for backward compatibility
|
// Re-export for backward compatibility
|
||||||
export type { ClaudeCallOptions } from './types.js';
|
export type { ClaudeCallOptions } from './types.js';
|
||||||
@ -11,8 +11,7 @@ import {
|
|||||||
type SDKResultMessage,
|
type SDKResultMessage,
|
||||||
type SDKAssistantMessage,
|
type SDKAssistantMessage,
|
||||||
} from '@anthropic-ai/claude-agent-sdk';
|
} from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { createLogger } from '../shared/utils/debug.js';
|
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../shared/utils/error.js';
|
|
||||||
import {
|
import {
|
||||||
generateQueryId,
|
generateQueryId,
|
||||||
registerQuery,
|
registerQuery,
|
||||||
@ -16,7 +16,7 @@ import type {
|
|||||||
PreToolUseHookInput,
|
PreToolUseHookInput,
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
} from '@anthropic-ai/claude-agent-sdk';
|
} from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { createLogger } from '../shared/utils/debug.js';
|
import { createLogger } from '../../shared/utils/index.js';
|
||||||
import type {
|
import type {
|
||||||
PermissionHandler,
|
PermissionHandler,
|
||||||
AskUserQuestionInput,
|
AskUserQuestionInput,
|
||||||
@ -5,8 +5,9 @@
|
|||||||
* used throughout the Claude integration layer.
|
* used throughout the Claude integration layer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PermissionResult, PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk';
|
import type { PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import type { PermissionMode } from '../core/models/index.js';
|
import type { PermissionMode } from '../../core/models/index.js';
|
||||||
|
import type { PermissionResult } from '../../core/workflow/index.js';
|
||||||
|
|
||||||
// Re-export PermissionResult for convenience
|
// Re-export PermissionResult for convenience
|
||||||
export type { PermissionResult, PermissionUpdate };
|
export type { PermissionResult, PermissionUpdate };
|
||||||
@ -5,7 +5,7 @@
|
|||||||
* used throughout the takt codebase.
|
* used throughout the takt codebase.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { StreamCallback } from '../claude/types.js';
|
import type { StreamCallback } from '../claude/index.js';
|
||||||
|
|
||||||
export type CodexEvent = {
|
export type CodexEvent = {
|
||||||
type: string;
|
type: string;
|
||||||
@ -5,9 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Codex } from '@openai/codex-sdk';
|
import { Codex } from '@openai/codex-sdk';
|
||||||
import type { AgentResponse } from '../core/models/index.js';
|
import type { AgentResponse } from '../../core/models/index.js';
|
||||||
import { createLogger } from '../shared/utils/debug.js';
|
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { getErrorMessage } from '../shared/utils/error.js';
|
|
||||||
import type { CodexCallOptions } from './types.js';
|
import type { CodexCallOptions } from './types.js';
|
||||||
import {
|
import {
|
||||||
type CodexEvent,
|
type CodexEvent,
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user