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.
|
||||
|
||||
**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
|
||||
- **Be specific** — Specify file names, function names, and change details
|
||||
- **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:
|
||||
- Output only the final task instruction (no preamble).
|
||||
- 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.
|
||||
|
||||
@ -104,9 +104,13 @@ steps:
|
||||
- condition: Implementation complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- 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: |
|
||||
Follow the plan from the plan step and implement.
|
||||
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
||||
@ -151,7 +155,6 @@ steps:
|
||||
- {command and outcome}
|
||||
|
||||
**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
|
||||
edit: false
|
||||
@ -396,6 +399,17 @@ steps:
|
||||
Address the feedback from the reviewers.
|
||||
The "Original User Request" is reference information, not the latest instruction.
|
||||
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
|
||||
|
||||
- name: supervise
|
||||
|
||||
@ -144,9 +144,13 @@ steps:
|
||||
- condition: Implementation is complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- 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
|
||||
@ -258,7 +262,6 @@ steps:
|
||||
- {command and outcome}
|
||||
|
||||
**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
|
||||
rules:
|
||||
- condition: AI Reviewer's issues have been fixed
|
||||
@ -509,6 +512,17 @@ steps:
|
||||
Address the feedback from the reviewers.
|
||||
The "Original User Request" is reference information, not the latest instruction.
|
||||
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
|
||||
|
||||
# ===========================================
|
||||
@ -624,6 +638,17 @@ steps:
|
||||
|
||||
The supervisor has identified issues from a big-picture perspective.
|
||||
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
|
||||
rules:
|
||||
- condition: Supervisor's issues have been fixed
|
||||
|
||||
@ -156,9 +156,13 @@ steps:
|
||||
- condition: Implementation is complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- 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
|
||||
@ -271,7 +275,6 @@ steps:
|
||||
- {command and outcome}
|
||||
|
||||
**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
|
||||
rules:
|
||||
- condition: AI Reviewer's issues have been fixed
|
||||
@ -522,6 +525,17 @@ steps:
|
||||
Address the feedback from the reviewers.
|
||||
The "Original User Request" is reference information, not the latest instruction.
|
||||
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
|
||||
|
||||
# ===========================================
|
||||
@ -637,6 +651,17 @@ steps:
|
||||
|
||||
The supervisor has identified issues from a big-picture perspective.
|
||||
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
|
||||
rules:
|
||||
- condition: Supervisor's issues have been fixed
|
||||
|
||||
@ -100,9 +100,13 @@ steps:
|
||||
- condition: Implementation complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- 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: |
|
||||
Follow the plan from the plan step and implement.
|
||||
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
||||
@ -147,7 +151,6 @@ steps:
|
||||
- {command and outcome}
|
||||
|
||||
**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)**
|
||||
## Work done
|
||||
|
||||
@ -73,3 +73,4 @@
|
||||
**シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。
|
||||
|
||||
**不明点は明確にする。** 推測で進めず、不明点を報告する。
|
||||
**確認が必要な場合は質問を一度にまとめる。** 追加の確認質問を繰り返さない。
|
||||
|
||||
@ -42,3 +42,4 @@
|
||||
- **推測で計画を立てない** — 必ずコードを読んで確認する
|
||||
- **計画は具体的に** — ファイル名、関数名、変更内容を明示する
|
||||
- **判断に迷ったら質問する** — 曖昧なまま進めない
|
||||
- **質問は一度にまとめる** — 追加の確認質問を繰り返さない
|
||||
|
||||
@ -3,5 +3,6 @@
|
||||
要件:
|
||||
- 出力は最終的な指示のみ(前置き不要)
|
||||
- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く
|
||||
- 制約や「やらないこと」を保持する
|
||||
- ユーザー由来の制約や「やらないこと」は保持する
|
||||
- アシスタントの運用上の制約(実行禁止/ツール制限など)は指示に含めない
|
||||
- 情報不足があれば「Open Questions」セクションを短く付ける
|
||||
|
||||
@ -95,9 +95,13 @@ steps:
|
||||
- condition: 実装完了
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: 判断できない、情報不足
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
planステップで立てた計画に従って実装してください。
|
||||
計画レポート({report:00-plan.md})を参照し、実装を進めてください。
|
||||
@ -146,8 +150,6 @@ steps:
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
|
||||
**実装未着手の扱い(必須)**
|
||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
||||
|
||||
- name: ai_review
|
||||
edit: false
|
||||
@ -402,6 +404,17 @@ steps:
|
||||
instruction_template: |
|
||||
レビュアーのフィードバックに対応してください。
|
||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
pass_previous_response: true
|
||||
|
||||
- name: supervise
|
||||
|
||||
@ -153,9 +153,13 @@ steps:
|
||||
- condition: 実装が完了した
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: 実装を進行できない
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
|
||||
# ===========================================
|
||||
# Phase 2: AI Review
|
||||
@ -266,8 +270,6 @@ steps:
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
|
||||
**実装未着手の扱い(必須)**
|
||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
||||
pass_previous_response: true
|
||||
rules:
|
||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||
@ -518,6 +520,17 @@ steps:
|
||||
レビュアーからのフィードバックに対応してください。
|
||||
「Original User Request」は参考情報であり、最新の指示ではありません。
|
||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
pass_previous_response: true
|
||||
|
||||
# ===========================================
|
||||
@ -633,6 +646,17 @@ steps:
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
pass_previous_response: true
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
|
||||
@ -144,9 +144,13 @@ steps:
|
||||
- condition: 実装が完了した
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: 実装を進行できない
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
|
||||
# ===========================================
|
||||
# Phase 2: AI Review
|
||||
@ -257,8 +261,6 @@ steps:
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
|
||||
**実装未着手の扱い(必須)**
|
||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
||||
pass_previous_response: true
|
||||
rules:
|
||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||
@ -509,6 +511,17 @@ steps:
|
||||
レビュアーからのフィードバックに対応してください。
|
||||
「Original User Request」は参考情報であり、最新の指示ではありません。
|
||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
pass_previous_response: true
|
||||
|
||||
# ===========================================
|
||||
@ -624,6 +637,17 @@ steps:
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
pass_previous_response: true
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
|
||||
@ -138,15 +138,17 @@ steps:
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
|
||||
**実装未着手の扱い(必須)**
|
||||
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
|
||||
rules:
|
||||
- condition: 実装が完了した
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: 実装を進行できない
|
||||
next: plan
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
|
||||
- name: ai_review
|
||||
edit: false
|
||||
|
||||
@ -20,7 +20,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })),
|
||||
}));
|
||||
|
||||
vi.mock('../prompt/index.js', () => ({
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
promptInput: vi.fn(),
|
||||
confirm: vi.fn(),
|
||||
selectOption: vi.fn(),
|
||||
@ -36,7 +36,8 @@ vi.mock('../shared/ui/index.js', () => ({
|
||||
blankLine: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
@ -69,7 +70,7 @@ vi.mock('../infra/github/issue.js', () => ({
|
||||
|
||||
import { interactiveMode } from '../features/interactive/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 { listWorkflows } from '../infra/config/loaders/workflowLoader.js';
|
||||
import { resolveIssueTask } from '../infra/github/issue.js';
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { detectJudgeIndex, buildJudgePrompt } from '../claude/client.js';
|
||||
import { detectJudgeIndex, buildJudgePrompt } from '../infra/claude/client.js';
|
||||
|
||||
describe('detectJudgeIndex', () => {
|
||||
it('should detect [JUDGE:1] as index 0', () => {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock dependencies before importing the module under test
|
||||
vi.mock('../prompt/index.js', () => ({
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
confirm: vi.fn(),
|
||||
selectOptionWithDefault: vi.fn(),
|
||||
}));
|
||||
@ -32,7 +32,8 @@ vi.mock('../shared/ui/index.js', () => ({
|
||||
setLogLevel: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
@ -60,8 +61,8 @@ vi.mock('../infra/config/loaders/workflowLoader.js', () => ({
|
||||
listWorkflows: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.mock('../constants.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../constants.js')>();
|
||||
vi.mock('../shared/constants.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../shared/constants.js')>();
|
||||
return {
|
||||
...actual,
|
||||
DEFAULT_WORKFLOW_NAME: 'default',
|
||||
@ -73,11 +74,12 @@ vi.mock('../infra/github/issue.js', () => ({
|
||||
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(),
|
||||
}));
|
||||
|
||||
import { confirm } from '../prompt/index.js';
|
||||
import { confirm } from '../shared/prompt/index.js';
|
||||
import { createSharedClone } from '../infra/task/clone.js';
|
||||
import { summarizeTaskName } from '../infra/task/summarize.js';
|
||||
import { info } from '../shared/ui/index.js';
|
||||
|
||||
@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
detectRuleIndex,
|
||||
isRegexSafe,
|
||||
} from '../claude/client.js';
|
||||
} from '../infra/claude/client.js';
|
||||
|
||||
describe('isRegexSafe', () => {
|
||||
it('should accept simple patterns', () => {
|
||||
|
||||
@ -21,7 +21,8 @@ vi.mock('node:fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
|
||||
@ -13,8 +13,6 @@ import {
|
||||
loadWorkflow,
|
||||
listWorkflows,
|
||||
loadAgentPromptFromPath,
|
||||
} from '../infra/config/loaders/loader.js';
|
||||
import {
|
||||
getCurrentWorkflow,
|
||||
setCurrentWorkflow,
|
||||
getProjectConfigDir,
|
||||
@ -35,9 +33,9 @@ import {
|
||||
getWorktreeSessionPath,
|
||||
loadWorktreeSessions,
|
||||
updateWorktreeSession,
|
||||
} from '../infra/config/paths.js';
|
||||
import { getLanguage } from '../infra/config/global/globalConfig.js';
|
||||
import { loadProjectConfig } from '../infra/config/project/projectConfig.js';
|
||||
getLanguage,
|
||||
loadProjectConfig,
|
||||
} from '../infra/config/index.js';
|
||||
|
||||
describe('getBuiltinWorkflow', () => {
|
||||
it('should return builtin workflow when it exists in resources', () => {
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
debugLog,
|
||||
infoLog,
|
||||
errorLog,
|
||||
} from '../shared/utils/debug.js';
|
||||
} from '../shared/utils/index.js';
|
||||
import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
@ -28,19 +28,15 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
}));
|
||||
|
||||
vi.mock('../claude/query-manager.js', () => ({
|
||||
interruptAllQueries: vi.fn().mockReturnValue(0),
|
||||
}));
|
||||
|
||||
// --- Imports (after mocks) ---
|
||||
|
||||
import { WorkflowEngine } from '../core/workflow/index.js';
|
||||
import { runAgent } from '../agents/runner.js';
|
||||
import { interruptAllQueries } from '../claude/query-manager.js';
|
||||
import {
|
||||
makeResponse,
|
||||
makeStep,
|
||||
@ -128,23 +124,11 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
||||
expect(state.status).toBe('aborted');
|
||||
expect(abortFn).toHaveBeenCalledOnce();
|
||||
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', () => {
|
||||
it('should only call interruptAllQueries once on multiple abort() calls', () => {
|
||||
it('should remain abort-requested on multiple abort() calls', () => {
|
||||
const config = makeSimpleConfig();
|
||||
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
@ -152,7 +136,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
|
||||
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(),
|
||||
}));
|
||||
|
||||
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'),
|
||||
}));
|
||||
|
||||
|
||||
@ -26,7 +26,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
}));
|
||||
|
||||
|
||||
@ -27,7 +27,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
}));
|
||||
|
||||
|
||||
@ -31,7 +31,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
}));
|
||||
|
||||
|
||||
@ -26,7 +26,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
}));
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import { runAgent } from '../agents/runner.js';
|
||||
import { detectMatchedRule } from '../core/workflow/index.js';
|
||||
import type { RuleMatch } 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 ---
|
||||
|
||||
|
||||
@ -27,7 +27,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
}));
|
||||
|
||||
@ -130,7 +131,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
|
||||
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
|
||||
const config: WorkflowConfig = {
|
||||
name: 'worktree-test',
|
||||
@ -162,15 +163,15 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
|
||||
// When: run the workflow
|
||||
await engine.run();
|
||||
|
||||
// Then: the instruction should contain cwd-based reportDir
|
||||
// Then: the instruction should contain projectCwd-based reportDir
|
||||
const runAgentMock = vi.mocked(runAgent);
|
||||
expect(runAgentMock).toHaveBeenCalled();
|
||||
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);
|
||||
// In worktree mode, projectCwd path should NOT appear
|
||||
expect(instruction).not.toContain(join(projectCwd, '.takt/reports/test-report-dir'));
|
||||
// In worktree mode, cloneCwd path should NOT appear
|
||||
expect(instruction).not.toContain(join(cloneCwd, '.takt/reports/test-report-dir'));
|
||||
});
|
||||
|
||||
it('should use same path in non-worktree mode (cwd === projectCwd)', async () => {
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
EXIT_GIT_OPERATION_FAILED,
|
||||
EXIT_PR_CREATION_FAILED,
|
||||
EXIT_SIGINT,
|
||||
} from '../exitCodes.js';
|
||||
} from '../shared/exitCodes.js';
|
||||
|
||||
describe('exit codes', () => {
|
||||
it('should have distinct values', () => {
|
||||
|
||||
@ -20,7 +20,7 @@ vi.mock('node:os', async () => {
|
||||
|
||||
// Mock the prompt to track if it was called
|
||||
const mockSelectOption = vi.fn().mockResolvedValue('en');
|
||||
vi.mock('../prompt/index.js', () => ({
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
selectOptionWithDefault: mockSelectOption,
|
||||
}));
|
||||
|
||||
|
||||
@ -20,14 +20,14 @@ vi.mock('node:os', async () => {
|
||||
});
|
||||
|
||||
// Mock the prompt to avoid interactive input
|
||||
vi.mock('../prompt/index.js', () => ({
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
selectOptionWithDefault: vi.fn().mockResolvedValue('ja'),
|
||||
}));
|
||||
|
||||
// Import after mocks are set up
|
||||
const { needsLanguageSetup } = await import('../infra/config/global/initialization.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', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@ -369,6 +369,20 @@ describe('instruction-builder', () => {
|
||||
|
||||
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)', () => {
|
||||
|
||||
@ -12,7 +12,8 @@ vi.mock('../infra/providers/index.js', () => ({
|
||||
getProvider: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: 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),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/paths.js', () => ({
|
||||
vi.mock('../infra/config/paths.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
loadAgentSessions: vi.fn(() => ({})),
|
||||
updateAgentSession: vi.fn(),
|
||||
getProjectConfigDir: vi.fn(() => '/tmp'),
|
||||
}));
|
||||
|
||||
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(),
|
||||
}));
|
||||
|
||||
@ -51,7 +54,7 @@ vi.mock('node:readline', () => ({
|
||||
import { createInterface } from 'node:readline';
|
||||
import { getProvider } from '../infra/providers/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 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 { join } from 'node:path';
|
||||
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 { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
|
||||
|
||||
// --- Mocks ---
|
||||
|
||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
||||
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||
return {
|
||||
...original,
|
||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||
@ -31,7 +32,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
}));
|
||||
@ -87,6 +89,14 @@ function createTestEnv(): { dir: string; agentPaths: Record<string, string> } {
|
||||
return { dir, agentPaths };
|
||||
}
|
||||
|
||||
function buildEngineOptions(projectCwd: string) {
|
||||
return {
|
||||
projectCwd,
|
||||
detectRuleIndex,
|
||||
callAiJudge,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWorkflow(agentPaths: Record<string, string>, maxIterations: number): WorkflowConfig {
|
||||
return {
|
||||
name: 'it-error',
|
||||
@ -133,7 +143,7 @@ describe('Error Recovery IT: agent blocked response', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 10);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -150,7 +160,7 @@ describe('Error Recovery IT: agent blocked response', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 10);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -187,7 +197,7 @@ describe('Error Recovery IT: max iterations reached', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 2);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -207,7 +217,7 @@ describe('Error Recovery IT: max iterations reached', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 4);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -242,7 +252,7 @@ describe('Error Recovery IT: scenario queue exhaustion', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 10);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -279,7 +289,7 @@ describe('Error Recovery IT: step events on error paths', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 3);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -300,7 +310,7 @@ describe('Error Recovery IT: step events on error paths', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 10);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -347,7 +357,7 @@ describe('Error Recovery IT: programmatic abort', () => {
|
||||
|
||||
const config = buildWorkflow(agentPaths, 10);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
getScenarioQueue,
|
||||
resetScenario,
|
||||
type ScenarioEntry,
|
||||
} from '../mock/scenario.js';
|
||||
} from '../infra/mock/index.js';
|
||||
|
||||
describe('ScenarioQueue', () => {
|
||||
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 { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
||||
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||
|
||||
// --- Mocks ---
|
||||
|
||||
@ -31,8 +31,8 @@ const {
|
||||
mockPushBranch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
||||
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||
return {
|
||||
...original,
|
||||
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(),
|
||||
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'),
|
||||
createSessionLog: vi.fn().mockReturnValue({
|
||||
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),
|
||||
}));
|
||||
|
||||
vi.mock('../prompt/index.js', () => ({
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
selectOption: vi.fn().mockResolvedValue('stop'),
|
||||
promptInput: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
@ -144,7 +146,7 @@ import {
|
||||
EXIT_ISSUE_FETCH_FAILED,
|
||||
EXIT_WORKFLOW_FAILED,
|
||||
EXIT_PR_CREATION_FAILED,
|
||||
} from '../exitCodes.js';
|
||||
} from '../shared/exitCodes.js';
|
||||
|
||||
// --- Test helpers ---
|
||||
|
||||
|
||||
@ -12,13 +12,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { setMockScenario, resetScenario } from '../mock/scenario.js';
|
||||
import { setMockScenario, resetScenario } from '../infra/mock/index.js';
|
||||
|
||||
// --- Mocks ---
|
||||
|
||||
// Safety net: prevent callAiJudge from calling real Claude CLI.
|
||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
||||
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||
return {
|
||||
...original,
|
||||
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(),
|
||||
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'),
|
||||
createSessionLog: vi.fn().mockReturnValue({
|
||||
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),
|
||||
}));
|
||||
|
||||
vi.mock('../prompt/index.js', () => ({
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
selectOption: vi.fn().mockResolvedValue('stop'),
|
||||
promptInput: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
@ -21,14 +21,6 @@ import type { WorkflowStep, WorkflowState, WorkflowRule, AgentResponse } from '.
|
||||
|
||||
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', () => ({
|
||||
loadGlobalConfig: vi.fn().mockReturnValue({}),
|
||||
getLanguage: vi.fn().mockReturnValue('en'),
|
||||
@ -41,6 +33,7 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
|
||||
// --- Imports (after mocks) ---
|
||||
|
||||
import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js';
|
||||
import { detectRuleIndex } from '../infra/claude/index.js';
|
||||
import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js';
|
||||
|
||||
// --- Test helpers ---
|
||||
@ -82,6 +75,8 @@ function makeCtx(stepOutputs?: Map<string, AgentResponse>): RuleEvaluatorContext
|
||||
return {
|
||||
state: makeState(stepOutputs),
|
||||
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 { join } from 'node:path';
|
||||
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 { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
|
||||
|
||||
// --- Mocks ---
|
||||
|
||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
||||
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||
return {
|
||||
...original,
|
||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||
@ -36,7 +37,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
}));
|
||||
@ -73,6 +75,14 @@ function createTestEnv(): { dir: string; agentPath: string } {
|
||||
return { dir, agentPath };
|
||||
}
|
||||
|
||||
function buildEngineOptions(projectCwd: string) {
|
||||
return {
|
||||
projectCwd,
|
||||
detectRuleIndex,
|
||||
callAiJudge,
|
||||
};
|
||||
}
|
||||
|
||||
function makeStep(
|
||||
name: 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', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -184,7 +194,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
|
||||
};
|
||||
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -213,7 +223,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
|
||||
};
|
||||
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -265,7 +275,7 @@ describe('Three-Phase Execution IT: phase1 + phase3 (tag rules defined)', () =>
|
||||
};
|
||||
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -316,7 +326,7 @@ describe('Three-Phase Execution IT: all three phases', () => {
|
||||
};
|
||||
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -379,7 +389,7 @@ describe('Three-Phase Execution IT: phase3 tag → rule match', () => {
|
||||
};
|
||||
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
|
||||
@ -13,16 +13,17 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
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 { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
|
||||
|
||||
// --- Mocks (minimal — only infrastructure, not core logic) ---
|
||||
|
||||
// Safety net: prevent callAiJudge from calling real Claude CLI.
|
||||
// Tag-based detection should always match in these tests; if it doesn't,
|
||||
// this mock surfaces the failure immediately instead of timing out.
|
||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
||||
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||
return {
|
||||
...original,
|
||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||
@ -35,7 +36,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
}));
|
||||
@ -89,6 +91,14 @@ function createTestEnv(): { dir: string; agentPaths: Record<string, string> } {
|
||||
return { dir, agentPaths };
|
||||
}
|
||||
|
||||
function buildEngineOptions(projectCwd: string) {
|
||||
return {
|
||||
projectCwd,
|
||||
detectRuleIndex,
|
||||
callAiJudge,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSimpleWorkflow(agentPaths: Record<string, string>): WorkflowConfig {
|
||||
return {
|
||||
name: 'it-simple',
|
||||
@ -168,7 +178,7 @@ describe('Workflow Engine IT: Happy Path', () => {
|
||||
|
||||
const config = buildSimpleWorkflow(agentPaths);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Test task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -185,7 +195,7 @@ describe('Workflow Engine IT: Happy Path', () => {
|
||||
|
||||
const config = buildSimpleWorkflow(agentPaths);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Vague task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -228,7 +238,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
|
||||
|
||||
const config = buildLoopWorkflow(agentPaths);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Task needing fix', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -248,7 +258,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
|
||||
|
||||
const config = buildLoopWorkflow(agentPaths);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Unfixable task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -286,7 +296,7 @@ describe('Workflow Engine IT: Max Iterations', () => {
|
||||
config.maxIterations = 5;
|
||||
|
||||
const engine = new WorkflowEngine(config, testDir, 'Looping task', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
@ -322,7 +332,7 @@ describe('Workflow Engine IT: Step Output Tracking', () => {
|
||||
|
||||
const config = buildSimpleWorkflow(agentPaths);
|
||||
const engine = new WorkflowEngine(config, testDir, 'Track outputs', {
|
||||
projectCwd: testDir,
|
||||
...buildEngineOptions(testDir),
|
||||
provider: 'mock',
|
||||
});
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
|
||||
// --- Imports (after mocks) ---
|
||||
|
||||
import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js';
|
||||
import { loadWorkflow } from '../infra/config/index.js';
|
||||
|
||||
// --- Test helpers ---
|
||||
|
||||
|
||||
@ -12,12 +12,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
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 ---
|
||||
|
||||
vi.mock('../claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../claude/client.js')>();
|
||||
vi.mock('../infra/claude/client.js', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('../infra/claude/client.js')>();
|
||||
return {
|
||||
...original,
|
||||
callAiJudge: vi.fn().mockResolvedValue(-1),
|
||||
@ -30,7 +31,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
|
||||
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'),
|
||||
generateSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
}));
|
||||
@ -48,7 +50,7 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
|
||||
// --- Imports (after mocks) ---
|
||||
|
||||
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';
|
||||
|
||||
// --- Test helpers ---
|
||||
@ -63,6 +65,8 @@ function createEngine(config: WorkflowConfig, dir: string, task: string): Workfl
|
||||
return new WorkflowEngine(config, dir, task, {
|
||||
projectCwd: dir,
|
||||
provider: 'mock',
|
||||
detectRuleIndex,
|
||||
callAiJudge,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -56,7 +56,8 @@ vi.mock('../shared/ui/index.js', () => ({
|
||||
debug: vi.fn(),
|
||||
}));
|
||||
// Mock debug logger
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
|
||||
@ -5,14 +5,14 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Readable } from 'node:stream';
|
||||
import chalk from 'chalk';
|
||||
import type { SelectOptionItem, KeyInputResult } from '../prompt/index.js';
|
||||
import type { SelectOptionItem, KeyInputResult } from '../shared/prompt/index.js';
|
||||
import {
|
||||
renderMenu,
|
||||
countRenderedLines,
|
||||
handleKeyInput,
|
||||
readMultilineFromStream,
|
||||
} from '../prompt/index.js';
|
||||
import { isFullWidth, getDisplayWidth, truncateText } from '../shared/utils/text.js';
|
||||
} from '../shared/prompt/index.js';
|
||||
import { isFullWidth, getDisplayWidth, truncateText } from '../shared/utils/index.js';
|
||||
|
||||
// Disable chalk colors for predictable test output
|
||||
chalk.level = 0;
|
||||
@ -310,7 +310,7 @@ describe('prompt', () => {
|
||||
|
||||
describe('selectOption', () => {
|
||||
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:', []);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
@ -318,13 +318,13 @@ describe('prompt', () => {
|
||||
|
||||
describe('selectOptionWithDefault', () => {
|
||||
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');
|
||||
expect(result).toBe('fallback');
|
||||
});
|
||||
|
||||
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)
|
||||
const result: string | null = await selectOptionWithDefault('Test:', [], 'fallback');
|
||||
expect(result).toBe('fallback');
|
||||
|
||||
@ -12,7 +12,8 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
loadGlobalConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
|
||||
@ -11,20 +11,24 @@ vi.mock('../infra/config/index.js', () => ({
|
||||
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(),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/task/clone.js', () => ({
|
||||
vi.mock('../infra/task/clone.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createSharedClone: 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(),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/task/summarize.js', () => ({
|
||||
vi.mock('../infra/task/summarize.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
summarizeTaskName: vi.fn(),
|
||||
}));
|
||||
|
||||
@ -37,15 +41,13 @@ vi.mock('../shared/ui/index.js', () => ({
|
||||
blankLine: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/debug.js', () => ({
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/error.js', () => ({
|
||||
getErrorMessage: vi.fn((e) => e.message),
|
||||
}));
|
||||
|
||||
@ -53,11 +55,11 @@ vi.mock('../features/tasks/execute/workflowExecution.js', () => ({
|
||||
executeWorkflow: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../context.js', () => ({
|
||||
vi.mock('../shared/context.js', () => ({
|
||||
isQuietMode: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock('../constants.js', () => ({
|
||||
vi.mock('../shared/constants.js', () => ({
|
||||
DEFAULT_WORKFLOW_NAME: 'default',
|
||||
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(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
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', () => {
|
||||
const workflow = loadWorkflow('expert', process.cwd());
|
||||
|
||||
@ -8,13 +8,11 @@ import {
|
||||
callClaudeAgent,
|
||||
callClaudeSkill,
|
||||
type ClaudeCallOptions,
|
||||
} from '../claude/client.js';
|
||||
import { loadCustomAgents, loadAgentPrompt } from '../infra/config/loaders/loader.js';
|
||||
import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
|
||||
import { loadProjectConfig } from '../infra/config/project/projectConfig.js';
|
||||
} from '../infra/claude/index.js';
|
||||
import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
|
||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/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';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* 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';
|
||||
|
||||
export type { StreamCallback };
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* 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 { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/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 { ProviderType } from '../../infra/providers/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.
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
* Import order matters: program setup → commands → routing → parse.
|
||||
*/
|
||||
|
||||
import { checkForUpdates } from '../../shared/utils/updateNotifier.js';
|
||||
import { checkForUpdates } from '../../shared/utils/index.js';
|
||||
|
||||
checkForUpdates();
|
||||
|
||||
|
||||
@ -15,9 +15,9 @@ import {
|
||||
getEffectiveDebugConfig,
|
||||
isVerboseMode,
|
||||
} from '../../infra/config/index.js';
|
||||
import { setQuietMode } from '../../context.js';
|
||||
import { setQuietMode } from '../../shared/context.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 { version: cliVersion } = require('../../../package.json') as { version: string };
|
||||
|
||||
@ -6,12 +6,12 @@
|
||||
*/
|
||||
|
||||
import { info, error } from '../../shared/ui/index.js';
|
||||
import { getErrorMessage } from '../../shared/utils/error.js';
|
||||
import { resolveIssueTask, isIssueReference } from '../../infra/github/issue.js';
|
||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||
import { resolveIssueTask, isIssueReference } from '../../infra/github/index.js';
|
||||
import { selectAndExecuteTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||
import { executePipeline } from '../../features/pipeline/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 { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
|
||||
|
||||
@ -94,5 +94,6 @@ program
|
||||
return;
|
||||
}
|
||||
|
||||
selectOptions.interactiveUserInput = true;
|
||||
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod/v4';
|
||||
import { DEFAULT_LANGUAGE } from '../../constants.js';
|
||||
import { DEFAULT_LANGUAGE } from '../../shared/constants.js';
|
||||
|
||||
/** Agent model schema (opus, sonnet, haiku) */
|
||||
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(),
|
||||
/** Template for additional AI output */
|
||||
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) */
|
||||
|
||||
@ -13,6 +13,10 @@ export interface WorkflowRule {
|
||||
next?: string;
|
||||
/** Template for additional AI output */
|
||||
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) */
|
||||
isAiCondition?: boolean;
|
||||
/** The condition text inside ai("...") for AI judge evaluation (set by loader) */
|
||||
|
||||
@ -58,6 +58,8 @@ export class OptionsBuilder {
|
||||
): RunAgentOptions {
|
||||
return {
|
||||
...this.buildBaseOptions(step),
|
||||
// Do not pass permission mode in report/status phases.
|
||||
permissionMode: undefined,
|
||||
sessionId,
|
||||
allowedTools: overrides.allowedTools,
|
||||
maxTurns: overrides.maxTurns,
|
||||
@ -73,6 +75,7 @@ export class OptionsBuilder {
|
||||
cwd: this.getCwd(),
|
||||
reportDir: join(this.getProjectCwd(), this.getReportDir()),
|
||||
language: this.getLanguage(),
|
||||
interactive: this.engineOptions.interactive,
|
||||
getSessionId: (agent: string) => state.agentSessions.get(agent),
|
||||
buildResumeOptions: this.buildResumeOptions.bind(this),
|
||||
updateAgentSession,
|
||||
|
||||
@ -15,7 +15,7 @@ import { ParallelLogger } from './parallel-logger.js';
|
||||
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
|
||||
import { detectMatchedRule } from '../evaluation/index.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 { StepExecutor } from './StepExecutor.js';
|
||||
import type { WorkflowEngineOptions } from '../types.js';
|
||||
@ -28,6 +28,13 @@ export interface ParallelRunnerDeps {
|
||||
readonly engineOptions: WorkflowEngineOptions;
|
||||
readonly getCwd: () => 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 {
|
||||
@ -63,7 +70,13 @@ export class ParallelRunner {
|
||||
: undefined;
|
||||
|
||||
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
|
||||
const subResults = await Promise.all(
|
||||
|
||||
@ -19,7 +19,7 @@ import { InstructionBuilder, isReportObjectConfig } from '../instruction/Instruc
|
||||
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
|
||||
import { detectMatchedRule } from '../evaluation/index.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';
|
||||
|
||||
const log = createLogger('step-executor');
|
||||
@ -30,6 +30,13 @@ export interface StepExecutorDeps {
|
||||
readonly getProjectCwd: () => string;
|
||||
readonly getReportDir: () => string;
|
||||
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 {
|
||||
@ -54,8 +61,9 @@ export class StepExecutor {
|
||||
projectCwd: this.deps.getProjectCwd(),
|
||||
userInputs: state.userInputs,
|
||||
previousOutput: getPreviousOutput(state),
|
||||
reportDir: join(this.deps.getCwd(), this.deps.getReportDir()),
|
||||
reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()),
|
||||
language: this.deps.getLanguage(),
|
||||
interactive: this.deps.getInteractive(),
|
||||
}).build();
|
||||
}
|
||||
|
||||
@ -106,6 +114,9 @@ export class StepExecutor {
|
||||
const match = await detectMatchedRule(step, response.content, tagContent, {
|
||||
state,
|
||||
cwd: this.deps.getCwd(),
|
||||
interactive: this.deps.getInteractive(),
|
||||
detectRuleIndex: this.deps.detectRuleIndex,
|
||||
callAiJudge: this.deps.callAiJudge,
|
||||
});
|
||||
if (match) {
|
||||
log.debug('Rule matched', { step: step.name, ruleIndex: match.index, method: match.method });
|
||||
|
||||
@ -25,10 +25,7 @@ import {
|
||||
addUserInput as addUserInputToState,
|
||||
incrementStepIteration,
|
||||
} from './state-manager.js';
|
||||
import { generateReportDir } from '../../../shared/utils/reportDir.js';
|
||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
||||
import { createLogger } from '../../../shared/utils/debug.js';
|
||||
import { interruptAllQueries } from '../../../claude/query-manager.js';
|
||||
import { generateReportDir, getErrorMessage, createLogger } from '../../../shared/utils/index.js';
|
||||
import { OptionsBuilder } from './OptionsBuilder.js';
|
||||
import { StepExecutor } from './StepExecutor.js';
|
||||
import { ParallelRunner } from './ParallelRunner.js';
|
||||
@ -61,6 +58,12 @@ export class WorkflowEngine extends EventEmitter {
|
||||
private readonly optionsBuilder: OptionsBuilder;
|
||||
private readonly stepExecutor: StepExecutor;
|
||||
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) {
|
||||
super();
|
||||
@ -74,6 +77,12 @@ export class WorkflowEngine extends EventEmitter {
|
||||
this.ensureReportDirExists();
|
||||
this.validateConfig();
|
||||
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
|
||||
this.optionsBuilder = new OptionsBuilder(
|
||||
@ -91,6 +100,9 @@ export class WorkflowEngine extends EventEmitter {
|
||||
getProjectCwd: () => this.projectCwd,
|
||||
getReportDir: () => this.reportDir,
|
||||
getLanguage: () => this.options.language,
|
||||
getInteractive: () => this.options.interactive === true,
|
||||
detectRuleIndex: this.detectRuleIndex,
|
||||
callAiJudge: this.callAiJudge,
|
||||
});
|
||||
|
||||
this.parallelRunner = new ParallelRunner({
|
||||
@ -99,6 +111,9 @@ export class WorkflowEngine extends EventEmitter {
|
||||
engineOptions: this.options,
|
||||
getCwd: () => this.cwd,
|
||||
getReportDir: () => this.reportDir,
|
||||
getInteractive: () => this.options.interactive === true,
|
||||
detectRuleIndex: this.detectRuleIndex,
|
||||
callAiJudge: this.callAiJudge,
|
||||
});
|
||||
|
||||
log.debug('WorkflowEngine initialized', {
|
||||
@ -183,7 +198,6 @@ export class WorkflowEngine extends EventEmitter {
|
||||
if (this.abortRequested) return;
|
||||
this.abortRequested = true;
|
||||
log.info('Abort requested');
|
||||
interruptAllQueries();
|
||||
}
|
||||
|
||||
/** Check if abort has been requested */
|
||||
@ -345,6 +359,31 @@ export class WorkflowEngine extends EventEmitter {
|
||||
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) {
|
||||
this.state.status = 'completed';
|
||||
this.emit('workflow:complete', this.state);
|
||||
@ -403,6 +442,29 @@ export class WorkflowEngine extends EventEmitter {
|
||||
const nextStep = this.resolveNextStep(step, response);
|
||||
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) {
|
||||
this.state.currentStep = nextStep;
|
||||
} else {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
|
||||
@ -11,8 +11,8 @@ import type {
|
||||
WorkflowState,
|
||||
RuleMatchMethod,
|
||||
} from '../../models/types.js';
|
||||
import { detectRuleIndex, callAiJudge } from '../../../claude/client.js';
|
||||
import { createLogger } from '../../../shared/utils/debug.js';
|
||||
import type { AiJudgeCaller, RuleIndexDetector } from '../types.js';
|
||||
import { createLogger } from '../../../shared/utils/index.js';
|
||||
import { AggregateEvaluator } from './AggregateEvaluator.js';
|
||||
|
||||
const log = createLogger('rule-evaluator');
|
||||
@ -27,6 +27,12 @@ export interface RuleEvaluatorContext {
|
||||
state: WorkflowState;
|
||||
/** Working directory (for AI judge calls) */
|
||||
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> {
|
||||
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
|
||||
const aggEvaluator = new AggregateEvaluator(this.step, this.ctx.state);
|
||||
@ -60,17 +67,27 @@ export class RuleEvaluator {
|
||||
|
||||
// 2. Tag detection from Phase 3 output
|
||||
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) {
|
||||
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)
|
||||
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) {
|
||||
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 }[] = [];
|
||||
for (let i = 0; i < this.step.rules.length; i++) {
|
||||
const rule = this.step.rules[i]!;
|
||||
if (rule.interactiveOnly && this.ctx.interactive !== true) {
|
||||
continue;
|
||||
}
|
||||
if (rule.isAiCondition && 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 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) {
|
||||
const matched = aiConditions[judgeResult]!;
|
||||
@ -136,14 +156,17 @@ export class RuleEvaluator {
|
||||
private async evaluateAllConditionsViaAiJudge(agentOutput: string): Promise<number> {
|
||||
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)', {
|
||||
step: this.step.name,
|
||||
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) {
|
||||
log.debug('AI judge (fallback) matched condition', {
|
||||
|
||||
@ -23,6 +23,7 @@ export type {
|
||||
StreamEvent,
|
||||
StreamCallback,
|
||||
PermissionHandler,
|
||||
PermissionResult,
|
||||
AskUserQuestionHandler,
|
||||
ProviderType,
|
||||
} from './types.js';
|
||||
|
||||
@ -136,7 +136,12 @@ export class InstructionBuilder {
|
||||
|
||||
// 7. Status Output Rules (for tag-based detection in Phase 1)
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -22,13 +22,17 @@ const REPORT_PHASE_STRINGS = {
|
||||
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.',
|
||||
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: {
|
||||
noSourceEdit: '**プロジェクトのソースファイルを変更しないでください。** レポートファイルのみ出力してください。',
|
||||
reportDirOnly: '**上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。',
|
||||
instructionBody: '前のステップの作業結果をレポートとして出力してください。',
|
||||
reportJsonFormat: 'レポートファイル名→内容のJSONオブジェクトで出力してください。',
|
||||
reportJsonFormat: 'JSON形式は任意です。JSONを使う場合は「レポートファイル名→内容」のオブジェクトにしてください(キーはファイル名のみ)。',
|
||||
reportPlainAllowed: '本文のみの出力も可です。複数ファイルの場合は同じ内容が各ファイルに書き込まれます。',
|
||||
reportOnlyOutput: 'レポート本文のみを出力してください(ステータスタグやコメントは禁止)。',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@ -103,6 +107,8 @@ export class ReportInstructionBuilder {
|
||||
r.instructionBody,
|
||||
r.reportJsonFormat,
|
||||
];
|
||||
instrParts.push(r.reportPlainAllowed);
|
||||
instrParts.push(r.reportOnlyOutput);
|
||||
|
||||
// Report output instruction (auto-generated or explicit order)
|
||||
const reportContext: InstructionContext = {
|
||||
|
||||
@ -28,6 +28,8 @@ const STATUS_JUDGMENT_STRINGS = {
|
||||
export interface StatusJudgmentContext {
|
||||
/** Language */
|
||||
language?: Language;
|
||||
/** Whether interactive-only rules are enabled */
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +54,12 @@ export class StatusJudgmentBuilder {
|
||||
sections.push(s.header);
|
||||
|
||||
// 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);
|
||||
|
||||
return sections.join('\n\n');
|
||||
|
||||
@ -31,6 +31,8 @@ export interface InstructionContext {
|
||||
reportDir?: string;
|
||||
/** Language for metadata rendering. Defaults to 'en'. */
|
||||
language?: Language;
|
||||
/** Whether interactive-only rules are enabled */
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
/** Execution environment metadata prepended to agent instructions */
|
||||
|
||||
@ -47,9 +47,14 @@ export function generateStatusRulesFromRules(
|
||||
stepName: string,
|
||||
rules: WorkflowRule[],
|
||||
language: Language,
|
||||
options?: { interactive?: boolean },
|
||||
): string {
|
||||
const tag = stepName.toUpperCase();
|
||||
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[] = [];
|
||||
|
||||
@ -58,8 +63,8 @@ export function generateStatusRulesFromRules(
|
||||
lines.push('');
|
||||
lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`);
|
||||
lines.push('|---|------|------|');
|
||||
for (const [i, rule] of rules.entries()) {
|
||||
lines.push(`| ${i + 1} | ${rule.condition} | \`[${tag}:${i + 1}]\` |`);
|
||||
for (const { rule, index } of visibleRules) {
|
||||
lines.push(`| ${index + 1} | ${rule.condition} | \`[${tag}:${index + 1}]\` |`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
@ -68,18 +73,18 @@ export function generateStatusRulesFromRules(
|
||||
lines.push('');
|
||||
lines.push(strings.outputInstruction);
|
||||
lines.push('');
|
||||
for (const [i, rule] of rules.entries()) {
|
||||
lines.push(`- \`[${tag}:${i + 1}]\` — ${rule.condition}`);
|
||||
for (const { rule, index } of visibleRules) {
|
||||
lines.push(`- \`[${tag}:${index + 1}]\` — ${rule.condition}`);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
lines.push('');
|
||||
lines.push(strings.appendixHeading);
|
||||
for (const [i, rule] of rules.entries()) {
|
||||
for (const { rule, index } of visibleRules) {
|
||||
if (!rule.appendix) continue;
|
||||
const tagStr = `[${tag}:${i + 1}]`;
|
||||
const tagStr = `[${tag}:${index + 1}]`;
|
||||
lines.push('');
|
||||
lines.push(strings.appendixInstruction.replace('{tag}', tagStr));
|
||||
lines.push('```');
|
||||
|
||||
@ -13,7 +13,7 @@ import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder
|
||||
import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js';
|
||||
import { hasTagBasedRules } from './evaluation/rule-utils.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');
|
||||
|
||||
@ -24,6 +24,8 @@ export interface PhaseRunnerContext {
|
||||
reportDir: string;
|
||||
/** Language for instructions */
|
||||
language?: Language;
|
||||
/** Whether interactive-only rules are enabled */
|
||||
interactive?: boolean;
|
||||
/** Get agent session ID */
|
||||
getSessionId: (agent: string) => string | undefined;
|
||||
/** Build resume options for a step */
|
||||
@ -76,6 +78,7 @@ function getReportFiles(report: WorkflowStep['report']): string[] {
|
||||
|
||||
function resolveReportOutputs(
|
||||
report: WorkflowStep['report'],
|
||||
reportDir: string,
|
||||
content: string,
|
||||
): Map<string, string> {
|
||||
if (!report) return new Map();
|
||||
@ -83,12 +86,17 @@ function resolveReportOutputs(
|
||||
const files = getReportFiles(report);
|
||||
const json = parseReportJson(content);
|
||||
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>();
|
||||
for (const file of files) {
|
||||
const value = json[file];
|
||||
const absolutePath = resolve(reportDir, file);
|
||||
const value = json[file] ?? json[absolutePath];
|
||||
if (typeof value !== 'string') {
|
||||
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 outputs = resolveReportOutputs(step.report, reportResponse.content);
|
||||
const outputs = resolveReportOutputs(step.report, ctx.reportDir, reportResponse.content);
|
||||
for (const [fileName, content] of outputs.entries()) {
|
||||
writeReportFile(ctx.reportDir, fileName, content);
|
||||
}
|
||||
@ -171,6 +179,7 @@ export async function runStatusJudgmentPhase(
|
||||
|
||||
const judgmentInstruction = new StatusJudgmentBuilder(step, {
|
||||
language: ctx.language,
|
||||
interactive: ctx.interactive,
|
||||
}).build();
|
||||
|
||||
const judgmentOptions = ctx.buildResumeOptions(step, sessionId, {
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
* 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 { PermissionResult } from '../../claude/types.js';
|
||||
|
||||
export type ProviderType = 'claude' | 'codex' | 'mock';
|
||||
|
||||
@ -66,11 +66,13 @@ export type StreamCallback = (event: StreamEvent) => void;
|
||||
export interface PermissionRequest {
|
||||
toolName: string;
|
||||
input: Record<string, unknown>;
|
||||
suggestions?: Array<Record<string, unknown>>;
|
||||
suggestions?: PermissionUpdate[];
|
||||
blockedPath?: string;
|
||||
decisionReason?: string;
|
||||
}
|
||||
|
||||
export type { PermissionResult, PermissionUpdate };
|
||||
|
||||
export type PermissionHandler = (request: PermissionRequest) => Promise<PermissionResult>;
|
||||
|
||||
export interface AskUserQuestionInput {
|
||||
@ -89,6 +91,19 @@ export type AskUserQuestionHandler = (
|
||||
input: AskUserQuestionInput
|
||||
) => 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 */
|
||||
export interface WorkflowEvents {
|
||||
'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void;
|
||||
@ -157,6 +172,12 @@ export interface WorkflowEngineOptions {
|
||||
language?: Language;
|
||||
provider?: ProviderType;
|
||||
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 */
|
||||
|
||||
@ -7,8 +7,13 @@
|
||||
|
||||
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../../infra/config/paths.js';
|
||||
import { getLanguage } from '../../infra/config/global/globalConfig.js';
|
||||
import {
|
||||
getGlobalWorkflowsDir,
|
||||
getGlobalAgentsDir,
|
||||
getBuiltinWorkflowsDir,
|
||||
getBuiltinAgentsDir,
|
||||
getLanguage,
|
||||
} from '../../infra/config/index.js';
|
||||
import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js';
|
||||
|
||||
/**
|
||||
|
||||
@ -7,15 +7,15 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { info, success } from '../../shared/ui/index.js';
|
||||
import { selectOption } from '../../prompt/index.js';
|
||||
import { selectOption } from '../../shared/prompt/index.js';
|
||||
import {
|
||||
loadProjectConfig,
|
||||
updateProjectConfig,
|
||||
type PermissionMode,
|
||||
} from '../../infra/config/project/projectConfig.js';
|
||||
} from '../../infra/config/index.js';
|
||||
import type { PermissionMode } from '../../infra/config/index.js';
|
||||
|
||||
// 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
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
* Workflow switching command
|
||||
*/
|
||||
|
||||
import { listWorkflows, loadWorkflow } from '../../infra/config/loaders/workflowLoader.js';
|
||||
import { getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/paths.js';
|
||||
import { listWorkflows, loadWorkflow, getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/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
|
||||
|
||||
@ -15,14 +15,12 @@ import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import chalk from 'chalk';
|
||||
import type { Language } from '../../core/models/index.js';
|
||||
import { loadGlobalConfig } from '../../infra/config/global/globalConfig.js';
|
||||
import { isQuietMode } from '../../context.js';
|
||||
import { loadAgentSessions, updateAgentSession } from '../../infra/config/paths.js';
|
||||
import { loadGlobalConfig, loadAgentSessions, updateAgentSession } from '../../infra/config/index.js';
|
||||
import { isQuietMode } from '../../shared/context.js';
|
||||
import { getProvider, type ProviderType } from '../../infra/providers/index.js';
|
||||
import { selectOption } from '../../prompt/index.js';
|
||||
import { getLanguageResourcesDir } from '../../resources/index.js';
|
||||
import { createLogger } from '../../shared/utils/debug.js';
|
||||
import { getErrorMessage } from '../../shared/utils/error.js';
|
||||
import { selectOption } from '../../shared/prompt/index.js';
|
||||
import { getLanguageResourcesDir } from '../../infra/resources/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
|
||||
const log = createLogger('interactive');
|
||||
|
||||
@ -363,14 +361,18 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
||||
}
|
||||
|
||||
// Handle slash commands
|
||||
if (trimmed === '/go') {
|
||||
const summaryPrompt = buildSummaryPrompt(
|
||||
if (trimmed.startsWith('/go')) {
|
||||
const userNote = trimmed.slice(3).trim();
|
||||
let summaryPrompt = buildSummaryPrompt(
|
||||
history,
|
||||
!!sessionId,
|
||||
prompts.summaryPrompt,
|
||||
prompts.noTranscript,
|
||||
prompts.conversationLabel,
|
||||
);
|
||||
if (summaryPrompt && userNote) {
|
||||
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
|
||||
}
|
||||
if (!summaryPrompt) {
|
||||
info(prompts.ui.noConversation);
|
||||
continue;
|
||||
|
||||
@ -10,22 +10,27 @@
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fetchIssue, formatIssueAsTask, checkGhCli } from '../../infra/github/issue.js';
|
||||
import type { GitHubIssue } from '../../infra/github/types.js';
|
||||
import { createPullRequest, pushBranch, buildPrBody } from '../../infra/github/pr.js';
|
||||
import { stageAndCommit } from '../../infra/task/git.js';
|
||||
import {
|
||||
fetchIssue,
|
||||
formatIssueAsTask,
|
||||
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 { 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 { createLogger } from '../../shared/utils/debug.js';
|
||||
import { getErrorMessage } from '../../shared/utils/error.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import type { PipelineConfig } from '../../core/models/index.js';
|
||||
import {
|
||||
EXIT_ISSUE_FETCH_FAILED,
|
||||
EXIT_WORKFLOW_FAILED,
|
||||
EXIT_GIT_OPERATION_FAILED,
|
||||
EXIT_PR_CREATION_FAILED,
|
||||
} from '../../exitCodes.js';
|
||||
} from '../../shared/exitCodes.js';
|
||||
|
||||
export type { PipelineExecutionOptions };
|
||||
|
||||
|
||||
@ -8,18 +8,14 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
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 { summarizeTaskName } from '../../../infra/task/summarize.js';
|
||||
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js';
|
||||
import { summarizeTaskName, type TaskFileData } from '../../../infra/task/index.js';
|
||||
import { loadGlobalConfig, listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
|
||||
import { getProvider, type ProviderType } from '../../../infra/providers/index.js';
|
||||
import { createLogger } from '../../../shared/utils/debug.js';
|
||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
||||
import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js';
|
||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/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');
|
||||
|
||||
|
||||
@ -6,16 +6,13 @@
|
||||
* mixing CLI parsing with business logic.
|
||||
*/
|
||||
|
||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
||||
import { listWorkflows, isWorkflowPath } from '../../../infra/config/loaders/workflowLoader.js';
|
||||
import { selectOptionWithDefault, confirm } from '../../../prompt/index.js';
|
||||
import { createSharedClone } from '../../../infra/task/clone.js';
|
||||
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js';
|
||||
import { summarizeTaskName } from '../../../infra/task/summarize.js';
|
||||
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
|
||||
import { getCurrentWorkflow, listWorkflows, isWorkflowPath } from '../../../infra/config/index.js';
|
||||
import { selectOptionWithDefault, confirm } from '../../../shared/prompt/index.js';
|
||||
import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
|
||||
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||
import { info, error, success } from '../../../shared/ui/index.js';
|
||||
import { createLogger } from '../../../shared/utils/debug.js';
|
||||
import { createPullRequest, buildPrBody } from '../../../infra/github/pr.js';
|
||||
import { createLogger } from '../../../shared/utils/index.js';
|
||||
import { createPullRequest, buildPrBody } from '../../../infra/github/index.js';
|
||||
import { executeTask } from './taskExecution.js';
|
||||
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
||||
|
||||
@ -136,6 +133,7 @@ export async function selectAndExecuteTask(
|
||||
workflowIdentifier,
|
||||
projectCwd: cwd,
|
||||
agentOverrides,
|
||||
interactiveUserInput: options?.interactiveUserInput === true,
|
||||
});
|
||||
|
||||
if (taskSuccess && isWorktree) {
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
* Session management helpers for agent execution
|
||||
*/
|
||||
|
||||
import { loadAgentSessions, updateAgentSession } from '../../../infra/config/paths.js';
|
||||
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js';
|
||||
import { loadAgentSessions, updateAgentSession, loadGlobalConfig } from '../../../infra/config/index.js';
|
||||
import type { AgentResponse } from '../../../core/models/index.js';
|
||||
|
||||
/**
|
||||
|
||||
@ -3,10 +3,7 @@
|
||||
*/
|
||||
|
||||
import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../../infra/config/index.js';
|
||||
import { TaskRunner, type TaskInfo } 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 { TaskRunner, type TaskInfo, createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
|
||||
import {
|
||||
header,
|
||||
info,
|
||||
@ -15,10 +12,9 @@ import {
|
||||
status,
|
||||
blankLine,
|
||||
} from '../../../shared/ui/index.js';
|
||||
import { createLogger } from '../../../shared/utils/debug.js';
|
||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.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';
|
||||
|
||||
export type { TaskExecutionOptions, ExecuteTaskOptions };
|
||||
@ -29,7 +25,7 @@ const log = createLogger('task');
|
||||
* Execute a single task with workflow.
|
||||
*/
|
||||
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);
|
||||
|
||||
if (!workflowConfig) {
|
||||
@ -54,6 +50,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<boolean>
|
||||
language: globalConfig.language,
|
||||
provider: agentOverrides?.provider,
|
||||
model: agentOverrides?.model,
|
||||
interactiveUserInput,
|
||||
});
|
||||
return result.success;
|
||||
}
|
||||
|
||||
@ -21,6 +21,8 @@ export interface WorkflowExecutionOptions {
|
||||
language?: Language;
|
||||
provider?: ProviderType;
|
||||
model?: string;
|
||||
/** Enable interactive user input during step transitions */
|
||||
interactiveUserInput?: boolean;
|
||||
}
|
||||
|
||||
export interface TaskExecutionOptions {
|
||||
@ -39,6 +41,8 @@ export interface ExecuteTaskOptions {
|
||||
projectCwd: string;
|
||||
/** Agent provider/model overrides */
|
||||
agentOverrides?: TaskExecutionOptions;
|
||||
/** Enable interactive user input during step transitions */
|
||||
interactiveUserInput?: boolean;
|
||||
}
|
||||
|
||||
export interface PipelineExecutionOptions {
|
||||
@ -73,4 +77,6 @@ export interface SelectAndExecuteOptions {
|
||||
repo?: string;
|
||||
workflow?: string;
|
||||
createWorktree?: boolean | undefined;
|
||||
/** Enable interactive user input during step transitions */
|
||||
interactiveUserInput?: boolean;
|
||||
}
|
||||
|
||||
@ -3,9 +3,10 @@
|
||||
*/
|
||||
|
||||
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 { WorkflowExecutionResult, WorkflowExecutionOptions } from './types.js';
|
||||
import { callAiJudge, detectRuleIndex, interruptAllQueries } from '../../../infra/claude/index.js';
|
||||
|
||||
export type { WorkflowExecutionResult, WorkflowExecutionOptions };
|
||||
|
||||
@ -14,9 +15,9 @@ import {
|
||||
updateAgentSession,
|
||||
loadWorktreeSessions,
|
||||
updateWorktreeSession,
|
||||
} from '../../../infra/config/paths.js';
|
||||
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js';
|
||||
import { isQuietMode } from '../../../context.js';
|
||||
loadGlobalConfig,
|
||||
} from '../../../infra/config/index.js';
|
||||
import { isQuietMode } from '../../../shared/context.js';
|
||||
import {
|
||||
header,
|
||||
info,
|
||||
@ -38,11 +39,10 @@ import {
|
||||
type NdjsonStepComplete,
|
||||
type NdjsonWorkflowComplete,
|
||||
type NdjsonWorkflowAbort,
|
||||
} from '../../../infra/fs/session.js';
|
||||
import { createLogger } from '../../../shared/utils/debug.js';
|
||||
import { notifySuccess, notifyError } from '../../../shared/utils/notification.js';
|
||||
import { selectOption, promptInput } from '../../../prompt/index.js';
|
||||
import { EXIT_SIGINT } from '../../../exitCodes.js';
|
||||
} from '../../../infra/fs/index.js';
|
||||
import { createLogger, notifySuccess, notifyError } from '../../../shared/utils/index.js';
|
||||
import { selectOption, promptInput } from '../../../shared/prompt/index.js';
|
||||
import { EXIT_SIGINT } from '../../../shared/exitCodes.js';
|
||||
|
||||
const log = createLogger('workflow');
|
||||
|
||||
@ -75,6 +75,7 @@ export async function executeWorkflow(
|
||||
): Promise<WorkflowExecutionResult> {
|
||||
const {
|
||||
headerPrefix = 'Running Workflow:',
|
||||
interactiveUserInput = false,
|
||||
} = options;
|
||||
|
||||
// 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, {
|
||||
onStream: streamHandler,
|
||||
onUserInput,
|
||||
initialSessions: savedSessions,
|
||||
onSessionUpdate: sessionUpdateHandler,
|
||||
onIterationLimit: iterationLimitHandler,
|
||||
@ -173,6 +188,9 @@ export async function executeWorkflow(
|
||||
language: options.language,
|
||||
provider: options.provider,
|
||||
model: options.model,
|
||||
interactive: interactiveUserInput,
|
||||
detectRuleIndex,
|
||||
callAiJudge,
|
||||
});
|
||||
|
||||
let abortReason: string | undefined;
|
||||
@ -288,6 +306,7 @@ export async function executeWorkflow(
|
||||
});
|
||||
|
||||
engine.on('workflow:abort', (state, reason) => {
|
||||
interruptAllQueries();
|
||||
log.error('Workflow aborted', { reason, iterations: state.iteration });
|
||||
if (displayRef.current) {
|
||||
displayRef.current.flush();
|
||||
|
||||
@ -9,10 +9,10 @@ import {
|
||||
detectDefaultBranch,
|
||||
listTaktBranches,
|
||||
buildListItems,
|
||||
} from '../../../infra/task/branchList.js';
|
||||
import { selectOption, confirm } from '../../../prompt/index.js';
|
||||
} from '../../../infra/task/index.js';
|
||||
import { selectOption, confirm } from '../../../shared/prompt/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 ListAction,
|
||||
|
||||
@ -12,21 +12,19 @@ import {
|
||||
removeClone,
|
||||
removeCloneMeta,
|
||||
cleanupOrphanedClone,
|
||||
} from '../../../infra/task/clone.js';
|
||||
} from '../../../infra/task/index.js';
|
||||
import {
|
||||
detectDefaultBranch,
|
||||
type BranchListItem,
|
||||
} from '../../../infra/task/branchList.js';
|
||||
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js';
|
||||
import { selectOption, promptInput } from '../../../prompt/index.js';
|
||||
autoCommitAndPush,
|
||||
} from '../../../infra/task/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 { createLogger } from '../../../shared/utils/debug.js';
|
||||
import { getErrorMessage } from '../../../shared/utils/error.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
import { executeTask } from '../execute/taskExecution.js';
|
||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||
import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js';
|
||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
||||
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
|
||||
import { listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
|
||||
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||
|
||||
const log = createLogger('list-tasks');
|
||||
|
||||
|
||||
@ -5,9 +5,8 @@
|
||||
* Stays resident until Ctrl+C (SIGINT).
|
||||
*/
|
||||
|
||||
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js';
|
||||
import { TaskWatcher } from '../../../infra/task/watcher.js';
|
||||
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
|
||||
import { TaskRunner, type TaskInfo, TaskWatcher } from '../../../infra/task/index.js';
|
||||
import { getCurrentWorkflow } from '../../../infra/config/index.js';
|
||||
import {
|
||||
header,
|
||||
info,
|
||||
@ -16,7 +15,7 @@ import {
|
||||
blankLine,
|
||||
} from '../../../shared/ui/index.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';
|
||||
|
||||
/**
|
||||
|
||||
47
src/index.ts
47
src/index.ts
@ -7,8 +7,39 @@
|
||||
// Models
|
||||
export * from './core/models/index.js';
|
||||
|
||||
// Configuration
|
||||
export * from './infra/config/index.js';
|
||||
// Configuration (PermissionMode excluded to avoid name conflict with core/models PermissionMode)
|
||||
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
|
||||
export {
|
||||
@ -40,7 +71,7 @@ export {
|
||||
detectJudgeIndex,
|
||||
buildJudgePrompt,
|
||||
isRegexSafe,
|
||||
} from './claude/index.js';
|
||||
} from './infra/claude/index.js';
|
||||
export type {
|
||||
StreamEvent,
|
||||
StreamCallback,
|
||||
@ -60,10 +91,10 @@ export type {
|
||||
ThinkingEventData,
|
||||
ResultEventData,
|
||||
ErrorEventData,
|
||||
} from './claude/index.js';
|
||||
} from './infra/claude/index.js';
|
||||
|
||||
// Codex integration
|
||||
export * from './codex/index.js';
|
||||
export * from './infra/codex/index.js';
|
||||
|
||||
// Agent execution
|
||||
export * from './agents/index.js';
|
||||
@ -117,6 +148,10 @@ export type {
|
||||
// Utilities
|
||||
export * from './shared/utils/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)
|
||||
export * from './resources/index.js';
|
||||
export * from './infra/resources/index.js';
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
import { executeClaudeCli } from './process.js';
|
||||
import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js';
|
||||
import type { AgentResponse, Status } from '../core/models/index.js';
|
||||
import { createLogger } from '../shared/utils/debug.js';
|
||||
import type { AgentResponse, Status } from '../../core/models/index.js';
|
||||
import { createLogger } from '../../shared/utils/index.js';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export type { ClaudeCallOptions } from './types.js';
|
||||
@ -11,8 +11,7 @@ import {
|
||||
type SDKResultMessage,
|
||||
type SDKAssistantMessage,
|
||||
} from '@anthropic-ai/claude-agent-sdk';
|
||||
import { createLogger } from '../shared/utils/debug.js';
|
||||
import { getErrorMessage } from '../shared/utils/error.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import {
|
||||
generateQueryId,
|
||||
registerQuery,
|
||||
@ -16,7 +16,7 @@ import type {
|
||||
PreToolUseHookInput,
|
||||
PermissionMode,
|
||||
} from '@anthropic-ai/claude-agent-sdk';
|
||||
import { createLogger } from '../shared/utils/debug.js';
|
||||
import { createLogger } from '../../shared/utils/index.js';
|
||||
import type {
|
||||
PermissionHandler,
|
||||
AskUserQuestionInput,
|
||||
@ -5,8 +5,9 @@
|
||||
* used throughout the Claude integration layer.
|
||||
*/
|
||||
|
||||
import type { PermissionResult, PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { PermissionMode } from '../core/models/index.js';
|
||||
import type { PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { PermissionMode } from '../../core/models/index.js';
|
||||
import type { PermissionResult } from '../../core/workflow/index.js';
|
||||
|
||||
// Re-export PermissionResult for convenience
|
||||
export type { PermissionResult, PermissionUpdate };
|
||||
@ -5,7 +5,7 @@
|
||||
* used throughout the takt codebase.
|
||||
*/
|
||||
|
||||
import type { StreamCallback } from '../claude/types.js';
|
||||
import type { StreamCallback } from '../claude/index.js';
|
||||
|
||||
export type CodexEvent = {
|
||||
type: string;
|
||||
@ -5,9 +5,8 @@
|
||||
*/
|
||||
|
||||
import { Codex } from '@openai/codex-sdk';
|
||||
import type { AgentResponse } from '../core/models/index.js';
|
||||
import { createLogger } from '../shared/utils/debug.js';
|
||||
import { getErrorMessage } from '../shared/utils/error.js';
|
||||
import type { AgentResponse } from '../../core/models/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import type { CodexCallOptions } from './types.js';
|
||||
import {
|
||||
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