This commit is contained in:
nrslib 2026-02-02 21:52:40 +09:00
parent 7d8ba10abb
commit b944349d8f
137 changed files with 1270 additions and 361 deletions

View 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 APIindex.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
View 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 import33箇所
- テストコードはモジュール内部を直接テストする性質上、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/` は変更不要)

View File

@ -73,3 +73,4 @@ Determine the implementation direction:
**Keep analysis simple.** Overly detailed plans are unnecessary. Provide enough direction for Coder to proceed with implementation. **Keep analysis simple.** Overly detailed plans are unnecessary. Provide enough direction for Coder to proceed with implementation.
**Make unclear points explicit.** Don't proceed with guesses, report unclear points. **Make unclear points explicit.** Don't proceed with guesses, report unclear points.
**Ask all clarification questions at once.** Do not ask follow-up questions in multiple rounds.

View File

@ -42,3 +42,4 @@ You are a planning specialist. Analyze tasks and design implementation plans.
- **Do not plan based on assumptions** — Always read the code to verify - **Do not plan based on assumptions** — Always read the code to verify
- **Be specific** — Specify file names, function names, and change details - **Be specific** — Specify file names, function names, and change details
- **Ask when uncertain** — Do not proceed with ambiguity - **Ask when uncertain** — Do not proceed with ambiguity
- **Ask all questions at once** — Avoid multiple rounds of follow-up questions

View File

@ -3,5 +3,6 @@ You are a task summarizer. Convert the conversation into a concrete task instruc
Requirements: Requirements:
- Output only the final task instruction (no preamble). - Output only the final task instruction (no preamble).
- Be specific about scope and targets (files/modules) if mentioned. - Be specific about scope and targets (files/modules) if mentioned.
- Preserve constraints and "do not" instructions. - Preserve user-provided constraints and "do not" instructions.
- Do NOT include assistant/system operational constraints (tool limits, execution prohibitions).
- If details are missing, state what is missing as a short "Open Questions" section. - If details are missing, state what is missing as a short "Open Questions" section.

View File

@ -104,9 +104,13 @@ steps:
- condition: Implementation complete - condition: Implementation complete
next: ai_review next: ai_review
- condition: No implementation (report only) - condition: No implementation (report only)
next: plan next: ai_review
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: plan next: ai_review
- condition: User input required
next: implement
requires_user_input: true
interactive_only: true
instruction_template: | instruction_template: |
Follow the plan from the plan step and implement. Follow the plan from the plan step and implement.
Refer to the plan report ({report:00-plan.md}) and proceed with implementation. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
@ -151,7 +155,6 @@ steps:
- {command and outcome} - {command and outcome}
**No-implementation handling (required)** **No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
- name: ai_review - name: ai_review
edit: false edit: false
@ -396,6 +399,17 @@ steps:
Address the feedback from the reviewers. Address the feedback from the reviewers.
The "Original User Request" is reference information, not the latest instruction. The "Original User Request" is reference information, not the latest instruction.
Review the session conversation history and fix the issues raised by the reviewers. Review the session conversation history and fix the issues raised by the reviewers.
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
## Evidence
- {key files/grep/diff/log evidence you verified}
pass_previous_response: true pass_previous_response: true
- name: supervise - name: supervise

View File

@ -144,9 +144,13 @@ steps:
- condition: Implementation is complete - condition: Implementation is complete
next: ai_review next: ai_review
- condition: No implementation (report only) - condition: No implementation (report only)
next: plan next: ai_review
- condition: Cannot proceed with implementation - condition: Cannot proceed with implementation
next: plan next: ai_review
- condition: User input required
next: implement
requires_user_input: true
interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Phase 2: AI Review
@ -258,7 +262,6 @@ steps:
- {command and outcome} - {command and outcome}
**No-implementation handling (required)** **No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewer's issues have been fixed - condition: AI Reviewer's issues have been fixed
@ -509,6 +512,17 @@ steps:
Address the feedback from the reviewers. Address the feedback from the reviewers.
The "Original User Request" is reference information, not the latest instruction. The "Original User Request" is reference information, not the latest instruction.
Review the session conversation history and fix the issues raised by the reviewers. Review the session conversation history and fix the issues raised by the reviewers.
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
## Evidence
- {key files/grep/diff/log evidence you verified}
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
@ -624,6 +638,17 @@ steps:
The supervisor has identified issues from a big-picture perspective. The supervisor has identified issues from a big-picture perspective.
Address items in priority order. Address items in priority order.
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
## Evidence
- {key files/grep/diff/log evidence you verified}
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: Supervisor's issues have been fixed - condition: Supervisor's issues have been fixed

View File

@ -156,9 +156,13 @@ steps:
- condition: Implementation is complete - condition: Implementation is complete
next: ai_review next: ai_review
- condition: No implementation (report only) - condition: No implementation (report only)
next: plan next: ai_review
- condition: Cannot proceed with implementation - condition: Cannot proceed with implementation
next: plan next: ai_review
- condition: User input required
next: implement
requires_user_input: true
interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Phase 2: AI Review
@ -271,7 +275,6 @@ steps:
- {command and outcome} - {command and outcome}
**No-implementation handling (required)** **No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewer's issues have been fixed - condition: AI Reviewer's issues have been fixed
@ -522,6 +525,17 @@ steps:
Address the feedback from the reviewers. Address the feedback from the reviewers.
The "Original User Request" is reference information, not the latest instruction. The "Original User Request" is reference information, not the latest instruction.
Review the session conversation history and fix the issues raised by the reviewers. Review the session conversation history and fix the issues raised by the reviewers.
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
## Evidence
- {key files/grep/diff/log evidence you verified}
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
@ -637,6 +651,17 @@ steps:
The supervisor has identified issues from a big-picture perspective. The supervisor has identified issues from a big-picture perspective.
Address items in priority order. Address items in priority order.
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
## Evidence
- {key files/grep/diff/log evidence you verified}
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: Supervisor's issues have been fixed - condition: Supervisor's issues have been fixed

View File

@ -100,9 +100,13 @@ steps:
- condition: Implementation complete - condition: Implementation complete
next: ai_review next: ai_review
- condition: No implementation (report only) - condition: No implementation (report only)
next: plan next: ai_review
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: plan next: ai_review
- condition: User input required
next: implement
requires_user_input: true
interactive_only: true
instruction_template: | instruction_template: |
Follow the plan from the plan step and implement. Follow the plan from the plan step and implement.
Refer to the plan report ({report:00-plan.md}) and proceed with implementation. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
@ -147,7 +151,6 @@ steps:
- {command and outcome} - {command and outcome}
**No-implementation handling (required)** **No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
**Required output (include headings)** **Required output (include headings)**
## Work done ## Work done

View File

@ -73,3 +73,4 @@
**シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。 **シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。
**不明点は明確にする。** 推測で進めず、不明点を報告する。 **不明点は明確にする。** 推測で進めず、不明点を報告する。
**確認が必要な場合は質問を一度にまとめる。** 追加の確認質問を繰り返さない。

View File

@ -42,3 +42,4 @@
- **推測で計画を立てない** — 必ずコードを読んで確認する - **推測で計画を立てない** — 必ずコードを読んで確認する
- **計画は具体的に** — ファイル名、関数名、変更内容を明示する - **計画は具体的に** — ファイル名、関数名、変更内容を明示する
- **判断に迷ったら質問する** — 曖昧なまま進めない - **判断に迷ったら質問する** — 曖昧なまま進めない
- **質問は一度にまとめる** — 追加の確認質問を繰り返さない

View File

@ -3,5 +3,6 @@
要件: 要件:
- 出力は最終的な指示のみ(前置き不要) - 出力は最終的な指示のみ(前置き不要)
- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く - スコープや対象(ファイル/モジュール)が出ている場合は明確に書く
- 制約や「やらないこと」を保持する - ユーザー由来の制約や「やらないこと」は保持する
- アシスタントの運用上の制約(実行禁止/ツール制限など)は指示に含めない
- 情報不足があれば「Open Questions」セクションを短く付ける - 情報不足があれば「Open Questions」セクションを短く付ける

View File

@ -95,9 +95,13 @@ steps:
- condition: 実装完了 - condition: 実装完了
next: ai_review next: ai_review
- condition: 実装未着手(レポートのみ) - condition: 実装未着手(レポートのみ)
next: plan next: ai_review
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
next: plan next: ai_review
- condition: ユーザー入力が必要
next: implement
requires_user_input: true
interactive_only: true
instruction_template: | instruction_template: |
planステップで立てた計画に従って実装してください。 planステップで立てた計画に従って実装してください。
計画レポート({report:00-plan.md})を参照し、実装を進めてください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。
@ -146,8 +150,6 @@ steps:
## テスト結果 ## テスト結果
- {実行コマンドと結果} - {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
- name: ai_review - name: ai_review
edit: false edit: false
@ -402,6 +404,17 @@ steps:
instruction_template: | instruction_template: |
レビュアーのフィードバックに対応してください。 レビュアーのフィードバックに対応してください。
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。 セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
## 証拠
- {確認したファイル/検索/差分/ログの要点を列挙}
pass_previous_response: true pass_previous_response: true
- name: supervise - name: supervise

View File

@ -153,9 +153,13 @@ steps:
- condition: 実装が完了した - condition: 実装が完了した
next: ai_review next: ai_review
- condition: 実装未着手(レポートのみ) - condition: 実装未着手(レポートのみ)
next: plan next: ai_review
- condition: 実装を進行できない - condition: 実装を進行できない
next: plan next: ai_review
- condition: ユーザー入力が必要
next: implement
requires_user_input: true
interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Phase 2: AI Review
@ -266,8 +270,6 @@ steps:
## テスト結果 ## テスト結果
- {実行コマンドと結果} - {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewerの指摘に対する修正が完了した - condition: AI Reviewerの指摘に対する修正が完了した
@ -518,6 +520,17 @@ steps:
レビュアーからのフィードバックに対応してください。 レビュアーからのフィードバックに対応してください。
「Original User Request」は参考情報であり、最新の指示ではありません。 「Original User Request」は参考情報であり、最新の指示ではありません。
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。 セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
## 証拠
- {確認したファイル/検索/差分/ログの要点を列挙}
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
@ -633,6 +646,17 @@ steps:
監督者は全体を俯瞰した視点から問題を指摘しています。 監督者は全体を俯瞰した視点から問題を指摘しています。
優先度の高い項目から順に対応してください。 優先度の高い項目から順に対応してください。
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
## 証拠
- {確認したファイル/検索/差分/ログの要点を列挙}
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: 監督者の指摘に対する修正が完了した - condition: 監督者の指摘に対する修正が完了した

View File

@ -144,9 +144,13 @@ steps:
- condition: 実装が完了した - condition: 実装が完了した
next: ai_review next: ai_review
- condition: 実装未着手(レポートのみ) - condition: 実装未着手(レポートのみ)
next: plan next: ai_review
- condition: 実装を進行できない - condition: 実装を進行できない
next: plan next: ai_review
- condition: ユーザー入力が必要
next: implement
requires_user_input: true
interactive_only: true
# =========================================== # ===========================================
# Phase 2: AI Review # Phase 2: AI Review
@ -257,8 +261,6 @@ steps:
## テスト結果 ## テスト結果
- {実行コマンドと結果} - {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewerの指摘に対する修正が完了した - condition: AI Reviewerの指摘に対する修正が完了した
@ -509,6 +511,17 @@ steps:
レビュアーからのフィードバックに対応してください。 レビュアーからのフィードバックに対応してください。
「Original User Request」は参考情報であり、最新の指示ではありません。 「Original User Request」は参考情報であり、最新の指示ではありません。
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。 セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
## 証拠
- {確認したファイル/検索/差分/ログの要点を列挙}
pass_previous_response: true pass_previous_response: true
# =========================================== # ===========================================
@ -624,6 +637,17 @@ steps:
監督者は全体を俯瞰した視点から問題を指摘しています。 監督者は全体を俯瞰した視点から問題を指摘しています。
優先度の高い項目から順に対応してください。 優先度の高い項目から順に対応してください。
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
## 証拠
- {確認したファイル/検索/差分/ログの要点を列挙}
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: 監督者の指摘に対する修正が完了した - condition: 監督者の指摘に対する修正が完了した

View File

@ -138,15 +138,17 @@ steps:
## テスト結果 ## テスト結果
- {実行コマンドと結果} - {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
rules: rules:
- condition: 実装が完了した - condition: 実装が完了した
next: ai_review next: ai_review
- condition: 実装未着手(レポートのみ) - condition: 実装未着手(レポートのみ)
next: plan next: ai_review
- condition: 実装を進行できない - condition: 実装を進行できない
next: plan next: ai_review
- condition: ユーザー入力が必要
next: implement
requires_user_input: true
interactive_only: true
- name: ai_review - name: ai_review
edit: false edit: false

View File

@ -20,7 +20,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })), loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })),
})); }));
vi.mock('../prompt/index.js', () => ({ vi.mock('../shared/prompt/index.js', () => ({
promptInput: vi.fn(), promptInput: vi.fn(),
confirm: vi.fn(), confirm: vi.fn(),
selectOption: vi.fn(), selectOption: vi.fn(),
@ -36,7 +36,8 @@ vi.mock('../shared/ui/index.js', () => ({
blankLine: vi.fn(), blankLine: vi.fn(),
})); }));
vi.mock('../shared/utils/debug.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -69,7 +70,7 @@ vi.mock('../infra/github/issue.js', () => ({
import { interactiveMode } from '../features/interactive/index.js'; import { interactiveMode } from '../features/interactive/index.js';
import { getProvider } from '../infra/providers/index.js'; import { getProvider } from '../infra/providers/index.js';
import { promptInput, confirm, selectOption } from '../prompt/index.js'; import { promptInput, confirm, selectOption } from '../shared/prompt/index.js';
import { summarizeTaskName } from '../infra/task/summarize.js'; import { summarizeTaskName } from '../infra/task/summarize.js';
import { listWorkflows } from '../infra/config/loaders/workflowLoader.js'; import { listWorkflows } from '../infra/config/loaders/workflowLoader.js';
import { resolveIssueTask } from '../infra/github/issue.js'; import { resolveIssueTask } from '../infra/github/issue.js';

View File

@ -3,7 +3,7 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { detectJudgeIndex, buildJudgePrompt } from '../claude/client.js'; import { detectJudgeIndex, buildJudgePrompt } from '../infra/claude/client.js';
describe('detectJudgeIndex', () => { describe('detectJudgeIndex', () => {
it('should detect [JUDGE:1] as index 0', () => { it('should detect [JUDGE:1] as index 0', () => {

View File

@ -5,7 +5,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock dependencies before importing the module under test // Mock dependencies before importing the module under test
vi.mock('../prompt/index.js', () => ({ vi.mock('../shared/prompt/index.js', () => ({
confirm: vi.fn(), confirm: vi.fn(),
selectOptionWithDefault: vi.fn(), selectOptionWithDefault: vi.fn(),
})); }));
@ -32,7 +32,8 @@ vi.mock('../shared/ui/index.js', () => ({
setLogLevel: vi.fn(), setLogLevel: vi.fn(),
})); }));
vi.mock('../shared/utils/debug.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -60,8 +61,8 @@ vi.mock('../infra/config/loaders/workflowLoader.js', () => ({
listWorkflows: vi.fn(() => []), listWorkflows: vi.fn(() => []),
})); }));
vi.mock('../constants.js', async (importOriginal) => { vi.mock('../shared/constants.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../constants.js')>(); const actual = await importOriginal<typeof import('../shared/constants.js')>();
return { return {
...actual, ...actual,
DEFAULT_WORKFLOW_NAME: 'default', DEFAULT_WORKFLOW_NAME: 'default',
@ -73,11 +74,12 @@ vi.mock('../infra/github/issue.js', () => ({
resolveIssueTask: vi.fn(), resolveIssueTask: vi.fn(),
})); }));
vi.mock('../shared/utils/updateNotifier.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
checkForUpdates: vi.fn(), checkForUpdates: vi.fn(),
})); }));
import { confirm } from '../prompt/index.js'; import { confirm } from '../shared/prompt/index.js';
import { createSharedClone } from '../infra/task/clone.js'; import { createSharedClone } from '../infra/task/clone.js';
import { summarizeTaskName } from '../infra/task/summarize.js'; import { summarizeTaskName } from '../infra/task/summarize.js';
import { info } from '../shared/ui/index.js'; import { info } from '../shared/ui/index.js';

View File

@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest';
import { import {
detectRuleIndex, detectRuleIndex,
isRegexSafe, isRegexSafe,
} from '../claude/client.js'; } from '../infra/claude/client.js';
describe('isRegexSafe', () => { describe('isRegexSafe', () => {
it('should accept simple patterns', () => { it('should accept simple patterns', () => {

View File

@ -21,7 +21,8 @@ vi.mock('node:fs', () => ({
existsSync: vi.fn(), existsSync: vi.fn(),
})); }));
vi.mock('../shared/utils/debug.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),

View File

@ -13,8 +13,6 @@ import {
loadWorkflow, loadWorkflow,
listWorkflows, listWorkflows,
loadAgentPromptFromPath, loadAgentPromptFromPath,
} from '../infra/config/loaders/loader.js';
import {
getCurrentWorkflow, getCurrentWorkflow,
setCurrentWorkflow, setCurrentWorkflow,
getProjectConfigDir, getProjectConfigDir,
@ -35,9 +33,9 @@ import {
getWorktreeSessionPath, getWorktreeSessionPath,
loadWorktreeSessions, loadWorktreeSessions,
updateWorktreeSession, updateWorktreeSession,
} from '../infra/config/paths.js'; getLanguage,
import { getLanguage } from '../infra/config/global/globalConfig.js'; loadProjectConfig,
import { loadProjectConfig } from '../infra/config/project/projectConfig.js'; } from '../infra/config/index.js';
describe('getBuiltinWorkflow', () => { describe('getBuiltinWorkflow', () => {
it('should return builtin workflow when it exists in resources', () => { it('should return builtin workflow when it exists in resources', () => {

View File

@ -14,7 +14,7 @@ import {
debugLog, debugLog,
infoLog, infoLog,
errorLog, errorLog,
} from '../shared/utils/debug.js'; } from '../shared/utils/index.js';
import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs'; import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';

View File

@ -28,19 +28,15 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
vi.mock('../claude/query-manager.js', () => ({
interruptAllQueries: vi.fn().mockReturnValue(0),
}));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../core/workflow/index.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import { interruptAllQueries } from '../claude/query-manager.js';
import { import {
makeResponse, makeResponse,
makeStep, makeStep,
@ -128,23 +124,11 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
expect(state.status).toBe('aborted'); expect(state.status).toBe('aborted');
expect(abortFn).toHaveBeenCalledOnce(); expect(abortFn).toHaveBeenCalledOnce();
expect(abortFn.mock.calls[0][1]).toContain('SIGINT'); expect(abortFn.mock.calls[0][1]).toContain('SIGINT');
expect(vi.mocked(interruptAllQueries)).toHaveBeenCalled();
});
});
describe('abort() calls interruptAllQueries', () => {
it('should call interruptAllQueries when abort() is called', () => {
const config = makeSimpleConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
engine.abort();
expect(vi.mocked(interruptAllQueries)).toHaveBeenCalledOnce();
}); });
}); });
describe('abort() idempotency', () => { describe('abort() idempotency', () => {
it('should only call interruptAllQueries once on multiple abort() calls', () => { it('should remain abort-requested on multiple abort() calls', () => {
const config = makeSimpleConfig(); const config = makeSimpleConfig();
const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
@ -152,7 +136,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => {
engine.abort(); engine.abort();
engine.abort(); engine.abort();
expect(vi.mocked(interruptAllQueries)).toHaveBeenCalledOnce(); expect(engine.isAbortRequested()).toBe(true);
}); });
}); });

View File

@ -21,7 +21,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn(), runStatusJudgmentPhase: vi.fn(),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));

View File

@ -26,7 +26,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));

View File

@ -27,7 +27,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));

View File

@ -31,7 +31,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));

View File

@ -26,7 +26,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));

View File

@ -18,7 +18,7 @@ import { runAgent } from '../agents/runner.js';
import { detectMatchedRule } from '../core/workflow/index.js'; import { detectMatchedRule } from '../core/workflow/index.js';
import type { RuleMatch } from '../core/workflow/index.js'; import type { RuleMatch } from '../core/workflow/index.js';
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/workflow/index.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/workflow/index.js';
import { generateReportDir } from '../shared/utils/reportDir.js'; import { generateReportDir } from '../shared/utils/index.js';
// --- Factory functions --- // --- Factory functions ---

View File

@ -27,7 +27,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
@ -130,7 +131,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
expect(phaseCtx.reportDir).not.toBe(unexpectedPath); expect(phaseCtx.reportDir).not.toBe(unexpectedPath);
}); });
it('should pass cwd-based reportDir to buildInstruction (used by {report_dir} placeholder)', async () => { it('should pass projectCwd-based reportDir to buildInstruction (used by {report_dir} placeholder)', async () => {
// Given: worktree environment with a step that uses {report_dir} in template // Given: worktree environment with a step that uses {report_dir} in template
const config: WorkflowConfig = { const config: WorkflowConfig = {
name: 'worktree-test', name: 'worktree-test',
@ -162,15 +163,15 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
// When: run the workflow // When: run the workflow
await engine.run(); await engine.run();
// Then: the instruction should contain cwd-based reportDir // Then: the instruction should contain projectCwd-based reportDir
const runAgentMock = vi.mocked(runAgent); const runAgentMock = vi.mocked(runAgent);
expect(runAgentMock).toHaveBeenCalled(); expect(runAgentMock).toHaveBeenCalled();
const instruction = runAgentMock.mock.calls[0][1] as string; const instruction = runAgentMock.mock.calls[0][1] as string;
const expectedPath = join(cloneCwd, '.takt/reports/test-report-dir'); const expectedPath = join(projectCwd, '.takt/reports/test-report-dir');
expect(instruction).toContain(expectedPath); expect(instruction).toContain(expectedPath);
// In worktree mode, projectCwd path should NOT appear // In worktree mode, cloneCwd path should NOT appear
expect(instruction).not.toContain(join(projectCwd, '.takt/reports/test-report-dir')); expect(instruction).not.toContain(join(cloneCwd, '.takt/reports/test-report-dir'));
}); });
it('should use same path in non-worktree mode (cwd === projectCwd)', async () => { it('should use same path in non-worktree mode (cwd === projectCwd)', async () => {

View File

@ -11,7 +11,7 @@ import {
EXIT_GIT_OPERATION_FAILED, EXIT_GIT_OPERATION_FAILED,
EXIT_PR_CREATION_FAILED, EXIT_PR_CREATION_FAILED,
EXIT_SIGINT, EXIT_SIGINT,
} from '../exitCodes.js'; } from '../shared/exitCodes.js';
describe('exit codes', () => { describe('exit codes', () => {
it('should have distinct values', () => { it('should have distinct values', () => {

View File

@ -20,7 +20,7 @@ vi.mock('node:os', async () => {
// Mock the prompt to track if it was called // Mock the prompt to track if it was called
const mockSelectOption = vi.fn().mockResolvedValue('en'); const mockSelectOption = vi.fn().mockResolvedValue('en');
vi.mock('../prompt/index.js', () => ({ vi.mock('../shared/prompt/index.js', () => ({
selectOptionWithDefault: mockSelectOption, selectOptionWithDefault: mockSelectOption,
})); }));

View File

@ -20,14 +20,14 @@ vi.mock('node:os', async () => {
}); });
// Mock the prompt to avoid interactive input // Mock the prompt to avoid interactive input
vi.mock('../prompt/index.js', () => ({ vi.mock('../shared/prompt/index.js', () => ({
selectOptionWithDefault: vi.fn().mockResolvedValue('ja'), selectOptionWithDefault: vi.fn().mockResolvedValue('ja'),
})); }));
// Import after mocks are set up // Import after mocks are set up
const { needsLanguageSetup } = await import('../infra/config/global/initialization.js'); const { needsLanguageSetup } = await import('../infra/config/global/initialization.js');
const { getGlobalConfigPath } = await import('../infra/config/paths.js'); const { getGlobalConfigPath } = await import('../infra/config/paths.js');
const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js'); const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../infra/resources/index.js');
describe('initialization', () => { describe('initialization', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -369,6 +369,20 @@ describe('instruction-builder', () => {
expect(result).toContain('`[AI_REVIEW:1]`'); expect(result).toContain('`[AI_REVIEW:1]`');
}); });
it('should omit interactive-only rules when interactive is false', () => {
const filteredRules: WorkflowRule[] = [
{ condition: 'Clear', next: 'implement' },
{ condition: 'User input required', next: 'implement', interactiveOnly: true },
{ condition: 'Blocked', next: 'plan' },
];
const result = generateStatusRulesFromRules('implement', filteredRules, 'en', { interactive: false });
expect(result).toContain('`[IMPLEMENT:1]`');
expect(result).toContain('`[IMPLEMENT:3]`');
expect(result).not.toContain('User input required');
expect(result).not.toContain('`[IMPLEMENT:2]`');
});
}); });
describe('buildInstruction with rules (Phase 1 — status rules injection)', () => { describe('buildInstruction with rules (Phase 1 — status rules injection)', () => {

View File

@ -12,7 +12,8 @@ vi.mock('../infra/providers/index.js', () => ({
getProvider: vi.fn(), getProvider: vi.fn(),
})); }));
vi.mock('../shared/utils/debug.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -20,13 +21,15 @@ vi.mock('../shared/utils/debug.js', () => ({
}), }),
})); }));
vi.mock('../context.js', () => ({ vi.mock('../shared/context.js', () => ({
isQuietMode: vi.fn(() => false), isQuietMode: vi.fn(() => false),
})); }));
vi.mock('../infra/config/paths.js', () => ({ vi.mock('../infra/config/paths.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
loadAgentSessions: vi.fn(() => ({})), loadAgentSessions: vi.fn(() => ({})),
updateAgentSession: vi.fn(), updateAgentSession: vi.fn(),
getProjectConfigDir: vi.fn(() => '/tmp'),
})); }));
vi.mock('../shared/ui/index.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
@ -39,7 +42,7 @@ vi.mock('../shared/ui/index.js', () => ({
})), })),
})); }));
vi.mock('../prompt/index.js', () => ({ vi.mock('../shared/prompt/index.js', () => ({
selectOption: vi.fn(), selectOption: vi.fn(),
})); }));
@ -51,7 +54,7 @@ vi.mock('node:readline', () => ({
import { createInterface } from 'node:readline'; import { createInterface } from 'node:readline';
import { getProvider } from '../infra/providers/index.js'; import { getProvider } from '../infra/providers/index.js';
import { interactiveMode } from '../features/interactive/index.js'; import { interactiveMode } from '../features/interactive/index.js';
import { selectOption } from '../prompt/index.js'; import { selectOption } from '../shared/prompt/index.js';
const mockGetProvider = vi.mocked(getProvider); const mockGetProvider = vi.mocked(getProvider);
const mockCreateInterface = vi.mocked(createInterface); const mockCreateInterface = vi.mocked(createInterface);

View File

@ -12,13 +12,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../infra/mock/index.js';
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js'; import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
// --- Mocks --- // --- Mocks ---
vi.mock('../claude/client.js', async (importOriginal) => { vi.mock('../infra/claude/client.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../claude/client.js')>(); const original = await importOriginal<typeof import('../infra/claude/client.js')>();
return { return {
...original, ...original,
callAiJudge: vi.fn().mockResolvedValue(-1), callAiJudge: vi.fn().mockResolvedValue(-1),
@ -31,7 +32,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
@ -87,6 +89,14 @@ function createTestEnv(): { dir: string; agentPaths: Record<string, string> } {
return { dir, agentPaths }; return { dir, agentPaths };
} }
function buildEngineOptions(projectCwd: string) {
return {
projectCwd,
detectRuleIndex,
callAiJudge,
};
}
function buildWorkflow(agentPaths: Record<string, string>, maxIterations: number): WorkflowConfig { function buildWorkflow(agentPaths: Record<string, string>, maxIterations: number): WorkflowConfig {
return { return {
name: 'it-error', name: 'it-error',
@ -133,7 +143,7 @@ describe('Error Recovery IT: agent blocked response', () => {
const config = buildWorkflow(agentPaths, 10); const config = buildWorkflow(agentPaths, 10);
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -150,7 +160,7 @@ describe('Error Recovery IT: agent blocked response', () => {
const config = buildWorkflow(agentPaths, 10); const config = buildWorkflow(agentPaths, 10);
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -187,7 +197,7 @@ describe('Error Recovery IT: max iterations reached', () => {
const config = buildWorkflow(agentPaths, 2); const config = buildWorkflow(agentPaths, 2);
const engine = new WorkflowEngine(config, testDir, 'Task', { const engine = new WorkflowEngine(config, testDir, 'Task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -207,7 +217,7 @@ describe('Error Recovery IT: max iterations reached', () => {
const config = buildWorkflow(agentPaths, 4); const config = buildWorkflow(agentPaths, 4);
const engine = new WorkflowEngine(config, testDir, 'Looping task', { const engine = new WorkflowEngine(config, testDir, 'Looping task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -242,7 +252,7 @@ describe('Error Recovery IT: scenario queue exhaustion', () => {
const config = buildWorkflow(agentPaths, 10); const config = buildWorkflow(agentPaths, 10);
const engine = new WorkflowEngine(config, testDir, 'Task', { const engine = new WorkflowEngine(config, testDir, 'Task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -279,7 +289,7 @@ describe('Error Recovery IT: step events on error paths', () => {
const config = buildWorkflow(agentPaths, 3); const config = buildWorkflow(agentPaths, 3);
const engine = new WorkflowEngine(config, testDir, 'Task', { const engine = new WorkflowEngine(config, testDir, 'Task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -300,7 +310,7 @@ describe('Error Recovery IT: step events on error paths', () => {
const config = buildWorkflow(agentPaths, 10); const config = buildWorkflow(agentPaths, 10);
const engine = new WorkflowEngine(config, testDir, 'Task', { const engine = new WorkflowEngine(config, testDir, 'Task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -347,7 +357,7 @@ describe('Error Recovery IT: programmatic abort', () => {
const config = buildWorkflow(agentPaths, 10); const config = buildWorkflow(agentPaths, 10);
const engine = new WorkflowEngine(config, testDir, 'Task', { const engine = new WorkflowEngine(config, testDir, 'Task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });

View File

@ -13,7 +13,7 @@ import {
getScenarioQueue, getScenarioQueue,
resetScenario, resetScenario,
type ScenarioEntry, type ScenarioEntry,
} from '../mock/scenario.js'; } from '../infra/mock/index.js';
describe('ScenarioQueue', () => { describe('ScenarioQueue', () => {
it('should consume entries in order when no agent specified', () => { it('should consume entries in order when no agent specified', () => {

View File

@ -13,7 +13,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../infra/mock/index.js';
// --- Mocks --- // --- Mocks ---
@ -31,8 +31,8 @@ const {
mockPushBranch: vi.fn(), mockPushBranch: vi.fn(),
})); }));
vi.mock('../claude/client.js', async (importOriginal) => { vi.mock('../infra/claude/client.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../claude/client.js')>(); const original = await importOriginal<typeof import('../infra/claude/client.js')>();
return { return {
...original, ...original,
callAiJudge: vi.fn().mockResolvedValue(-1), callAiJudge: vi.fn().mockResolvedValue(-1),
@ -73,12 +73,14 @@ vi.mock('../shared/ui/index.js', () => ({
})), })),
})); }));
vi.mock('../shared/utils/notification.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
notifySuccess: vi.fn(), notifySuccess: vi.fn(),
notifyError: vi.fn(), notifyError: vi.fn(),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
createSessionLog: vi.fn().mockReturnValue({ createSessionLog: vi.fn().mockReturnValue({
startTime: new Date().toISOString(), startTime: new Date().toISOString(),
@ -122,11 +124,11 @@ vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => {
}; };
}); });
vi.mock('../context.js', () => ({ vi.mock('../shared/context.js', () => ({
isQuietMode: vi.fn().mockReturnValue(true), isQuietMode: vi.fn().mockReturnValue(true),
})); }));
vi.mock('../prompt/index.js', () => ({ vi.mock('../shared/prompt/index.js', () => ({
selectOption: vi.fn().mockResolvedValue('stop'), selectOption: vi.fn().mockResolvedValue('stop'),
promptInput: vi.fn().mockResolvedValue(null), promptInput: vi.fn().mockResolvedValue(null),
})); }));
@ -144,7 +146,7 @@ import {
EXIT_ISSUE_FETCH_FAILED, EXIT_ISSUE_FETCH_FAILED,
EXIT_WORKFLOW_FAILED, EXIT_WORKFLOW_FAILED,
EXIT_PR_CREATION_FAILED, EXIT_PR_CREATION_FAILED,
} from '../exitCodes.js'; } from '../shared/exitCodes.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -12,13 +12,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../infra/mock/index.js';
// --- Mocks --- // --- Mocks ---
// Safety net: prevent callAiJudge from calling real Claude CLI. // Safety net: prevent callAiJudge from calling real Claude CLI.
vi.mock('../claude/client.js', async (importOriginal) => { vi.mock('../infra/claude/client.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../claude/client.js')>(); const original = await importOriginal<typeof import('../infra/claude/client.js')>();
return { return {
...original, ...original,
callAiJudge: vi.fn().mockResolvedValue(-1), callAiJudge: vi.fn().mockResolvedValue(-1),
@ -56,12 +56,14 @@ vi.mock('../shared/ui/index.js', () => ({
})), })),
})); }));
vi.mock('../shared/utils/notification.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
notifySuccess: vi.fn(), notifySuccess: vi.fn(),
notifyError: vi.fn(), notifyError: vi.fn(),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
createSessionLog: vi.fn().mockReturnValue({ createSessionLog: vi.fn().mockReturnValue({
startTime: new Date().toISOString(), startTime: new Date().toISOString(),
@ -104,11 +106,11 @@ vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => {
}; };
}); });
vi.mock('../context.js', () => ({ vi.mock('../shared/context.js', () => ({
isQuietMode: vi.fn().mockReturnValue(true), isQuietMode: vi.fn().mockReturnValue(true),
})); }));
vi.mock('../prompt/index.js', () => ({ vi.mock('../shared/prompt/index.js', () => ({
selectOption: vi.fn().mockResolvedValue('stop'), selectOption: vi.fn().mockResolvedValue('stop'),
promptInput: vi.fn().mockResolvedValue(null), promptInput: vi.fn().mockResolvedValue(null),
})); }));

View File

@ -21,14 +21,6 @@ import type { WorkflowStep, WorkflowState, WorkflowRule, AgentResponse } from '.
const mockCallAiJudge = vi.fn(); const mockCallAiJudge = vi.fn();
vi.mock('../claude/client.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../claude/client.js')>();
return {
...original,
callAiJudge: (...args: unknown[]) => mockCallAiJudge(...args),
};
});
vi.mock('../infra/config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
@ -41,6 +33,7 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js'; import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js';
import { detectRuleIndex } from '../infra/claude/index.js';
import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js'; import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js';
// --- Test helpers --- // --- Test helpers ---
@ -82,6 +75,8 @@ function makeCtx(stepOutputs?: Map<string, AgentResponse>): RuleEvaluatorContext
return { return {
state: makeState(stepOutputs), state: makeState(stepOutputs),
cwd: '/tmp/test', cwd: '/tmp/test',
detectRuleIndex,
callAiJudge: mockCallAiJudge,
}; };
} }

View File

@ -13,13 +13,14 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../infra/mock/index.js';
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js'; import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
// --- Mocks --- // --- Mocks ---
vi.mock('../claude/client.js', async (importOriginal) => { vi.mock('../infra/claude/client.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../claude/client.js')>(); const original = await importOriginal<typeof import('../infra/claude/client.js')>();
return { return {
...original, ...original,
callAiJudge: vi.fn().mockResolvedValue(-1), callAiJudge: vi.fn().mockResolvedValue(-1),
@ -36,7 +37,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...args), runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...args),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
@ -73,6 +75,14 @@ function createTestEnv(): { dir: string; agentPath: string } {
return { dir, agentPath }; return { dir, agentPath };
} }
function buildEngineOptions(projectCwd: string) {
return {
projectCwd,
detectRuleIndex,
callAiJudge,
};
}
function makeStep( function makeStep(
name: string, name: string,
agentPath: string, agentPath: string,
@ -132,7 +142,7 @@ describe('Three-Phase Execution IT: phase1 only (no report, no tag rules)', () =
}; };
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -184,7 +194,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
}; };
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -213,7 +223,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => {
}; };
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -265,7 +275,7 @@ describe('Three-Phase Execution IT: phase1 + phase3 (tag rules defined)', () =>
}; };
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -316,7 +326,7 @@ describe('Three-Phase Execution IT: all three phases', () => {
}; };
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -379,7 +389,7 @@ describe('Three-Phase Execution IT: phase3 tag → rule match', () => {
}; };
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });

View File

@ -13,16 +13,17 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../infra/mock/index.js';
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js'; import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
// --- Mocks (minimal — only infrastructure, not core logic) --- // --- Mocks (minimal — only infrastructure, not core logic) ---
// Safety net: prevent callAiJudge from calling real Claude CLI. // Safety net: prevent callAiJudge from calling real Claude CLI.
// Tag-based detection should always match in these tests; if it doesn't, // Tag-based detection should always match in these tests; if it doesn't,
// this mock surfaces the failure immediately instead of timing out. // this mock surfaces the failure immediately instead of timing out.
vi.mock('../claude/client.js', async (importOriginal) => { vi.mock('../infra/claude/client.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../claude/client.js')>(); const original = await importOriginal<typeof import('../infra/claude/client.js')>();
return { return {
...original, ...original,
callAiJudge: vi.fn().mockResolvedValue(-1), callAiJudge: vi.fn().mockResolvedValue(-1),
@ -35,7 +36,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
@ -89,6 +91,14 @@ function createTestEnv(): { dir: string; agentPaths: Record<string, string> } {
return { dir, agentPaths }; return { dir, agentPaths };
} }
function buildEngineOptions(projectCwd: string) {
return {
projectCwd,
detectRuleIndex,
callAiJudge,
};
}
function buildSimpleWorkflow(agentPaths: Record<string, string>): WorkflowConfig { function buildSimpleWorkflow(agentPaths: Record<string, string>): WorkflowConfig {
return { return {
name: 'it-simple', name: 'it-simple',
@ -168,7 +178,7 @@ describe('Workflow Engine IT: Happy Path', () => {
const config = buildSimpleWorkflow(agentPaths); const config = buildSimpleWorkflow(agentPaths);
const engine = new WorkflowEngine(config, testDir, 'Test task', { const engine = new WorkflowEngine(config, testDir, 'Test task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -185,7 +195,7 @@ describe('Workflow Engine IT: Happy Path', () => {
const config = buildSimpleWorkflow(agentPaths); const config = buildSimpleWorkflow(agentPaths);
const engine = new WorkflowEngine(config, testDir, 'Vague task', { const engine = new WorkflowEngine(config, testDir, 'Vague task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -228,7 +238,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
const config = buildLoopWorkflow(agentPaths); const config = buildLoopWorkflow(agentPaths);
const engine = new WorkflowEngine(config, testDir, 'Task needing fix', { const engine = new WorkflowEngine(config, testDir, 'Task needing fix', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -248,7 +258,7 @@ describe('Workflow Engine IT: Fix Loop', () => {
const config = buildLoopWorkflow(agentPaths); const config = buildLoopWorkflow(agentPaths);
const engine = new WorkflowEngine(config, testDir, 'Unfixable task', { const engine = new WorkflowEngine(config, testDir, 'Unfixable task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -286,7 +296,7 @@ describe('Workflow Engine IT: Max Iterations', () => {
config.maxIterations = 5; config.maxIterations = 5;
const engine = new WorkflowEngine(config, testDir, 'Looping task', { const engine = new WorkflowEngine(config, testDir, 'Looping task', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });
@ -322,7 +332,7 @@ describe('Workflow Engine IT: Step Output Tracking', () => {
const config = buildSimpleWorkflow(agentPaths); const config = buildSimpleWorkflow(agentPaths);
const engine = new WorkflowEngine(config, testDir, 'Track outputs', { const engine = new WorkflowEngine(config, testDir, 'Track outputs', {
projectCwd: testDir, ...buildEngineOptions(testDir),
provider: 'mock', provider: 'mock',
}); });

View File

@ -23,7 +23,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js'; import { loadWorkflow } from '../infra/config/index.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -12,12 +12,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../infra/mock/index.js';
import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js';
// --- Mocks --- // --- Mocks ---
vi.mock('../claude/client.js', async (importOriginal) => { vi.mock('../infra/claude/client.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../claude/client.js')>(); const original = await importOriginal<typeof import('../infra/claude/client.js')>();
return { return {
...original, ...original,
callAiJudge: vi.fn().mockResolvedValue(-1), callAiJudge: vi.fn().mockResolvedValue(-1),
@ -30,7 +31,8 @@ vi.mock('../core/workflow/phase-runner.js', () => ({
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
})); }));
vi.mock('../shared/utils/reportDir.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
@ -48,7 +50,7 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../core/workflow/index.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js'; import { loadWorkflow } from '../infra/config/index.js';
import type { WorkflowConfig } from '../core/models/index.js'; import type { WorkflowConfig } from '../core/models/index.js';
// --- Test helpers --- // --- Test helpers ---
@ -63,6 +65,8 @@ function createEngine(config: WorkflowConfig, dir: string, task: string): Workfl
return new WorkflowEngine(config, dir, task, { return new WorkflowEngine(config, dir, task, {
projectCwd: dir, projectCwd: dir,
provider: 'mock', provider: 'mock',
detectRuleIndex,
callAiJudge,
}); });
} }

View File

@ -56,7 +56,8 @@ vi.mock('../shared/ui/index.js', () => ({
debug: vi.fn(), debug: vi.fn(),
})); }));
// Mock debug logger // Mock debug logger
vi.mock('../shared/utils/debug.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),

View File

@ -5,14 +5,14 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Readable } from 'node:stream'; import { Readable } from 'node:stream';
import chalk from 'chalk'; import chalk from 'chalk';
import type { SelectOptionItem, KeyInputResult } from '../prompt/index.js'; import type { SelectOptionItem, KeyInputResult } from '../shared/prompt/index.js';
import { import {
renderMenu, renderMenu,
countRenderedLines, countRenderedLines,
handleKeyInput, handleKeyInput,
readMultilineFromStream, readMultilineFromStream,
} from '../prompt/index.js'; } from '../shared/prompt/index.js';
import { isFullWidth, getDisplayWidth, truncateText } from '../shared/utils/text.js'; import { isFullWidth, getDisplayWidth, truncateText } from '../shared/utils/index.js';
// Disable chalk colors for predictable test output // Disable chalk colors for predictable test output
chalk.level = 0; chalk.level = 0;
@ -310,7 +310,7 @@ describe('prompt', () => {
describe('selectOption', () => { describe('selectOption', () => {
it('should return null for empty options', async () => { it('should return null for empty options', async () => {
const { selectOption } = await import('../prompt/index.js'); const { selectOption } = await import('../shared/prompt/index.js');
const result = await selectOption('Test:', []); const result = await selectOption('Test:', []);
expect(result).toBeNull(); expect(result).toBeNull();
}); });
@ -318,13 +318,13 @@ describe('prompt', () => {
describe('selectOptionWithDefault', () => { describe('selectOptionWithDefault', () => {
it('should return default for empty options', async () => { it('should return default for empty options', async () => {
const { selectOptionWithDefault } = await import('../prompt/index.js'); const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
const result = await selectOptionWithDefault('Test:', [], 'fallback'); const result = await selectOptionWithDefault('Test:', [], 'fallback');
expect(result).toBe('fallback'); expect(result).toBe('fallback');
}); });
it('should have return type that allows null (cancel)', async () => { it('should have return type that allows null (cancel)', async () => {
const { selectOptionWithDefault } = await import('../prompt/index.js'); const { selectOptionWithDefault } = await import('../shared/prompt/index.js');
// When options are empty, default is returned (not null) // When options are empty, default is returned (not null)
const result: string | null = await selectOptionWithDefault('Test:', [], 'fallback'); const result: string | null = await selectOptionWithDefault('Test:', [], 'fallback');
expect(result).toBe('fallback'); expect(result).toBe('fallback');

View File

@ -12,7 +12,8 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn(), loadGlobalConfig: vi.fn(),
})); }));
vi.mock('../shared/utils/debug.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),

View File

@ -11,20 +11,24 @@ vi.mock('../infra/config/index.js', () => ({
loadGlobalConfig: vi.fn(() => ({})), loadGlobalConfig: vi.fn(() => ({})),
})); }));
vi.mock('../infra/task/index.js', () => ({ vi.mock('../infra/task/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
TaskRunner: vi.fn(), TaskRunner: vi.fn(),
})); }));
vi.mock('../infra/task/clone.js', () => ({ vi.mock('../infra/task/clone.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createSharedClone: vi.fn(), createSharedClone: vi.fn(),
removeClone: vi.fn(), removeClone: vi.fn(),
})); }));
vi.mock('../infra/task/autoCommit.js', () => ({ vi.mock('../infra/task/autoCommit.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
autoCommitAndPush: vi.fn(), autoCommitAndPush: vi.fn(),
})); }));
vi.mock('../infra/task/summarize.js', () => ({ vi.mock('../infra/task/summarize.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
summarizeTaskName: vi.fn(), summarizeTaskName: vi.fn(),
})); }));
@ -37,15 +41,13 @@ vi.mock('../shared/ui/index.js', () => ({
blankLine: vi.fn(), blankLine: vi.fn(),
})); }));
vi.mock('../shared/utils/debug.js', () => ({ vi.mock('../shared/utils/index.js', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
error: vi.fn(), error: vi.fn(),
}), }),
}));
vi.mock('../shared/utils/error.js', () => ({
getErrorMessage: vi.fn((e) => e.message), getErrorMessage: vi.fn((e) => e.message),
})); }));
@ -53,11 +55,11 @@ vi.mock('../features/tasks/execute/workflowExecution.js', () => ({
executeWorkflow: vi.fn(), executeWorkflow: vi.fn(),
})); }));
vi.mock('../context.js', () => ({ vi.mock('../shared/context.js', () => ({
isQuietMode: vi.fn(() => false), isQuietMode: vi.fn(() => false),
})); }));
vi.mock('../constants.js', () => ({ vi.mock('../shared/constants.js', () => ({
DEFAULT_WORKFLOW_NAME: 'default', DEFAULT_WORKFLOW_NAME: 'default',
DEFAULT_LANGUAGE: 'en', DEFAULT_LANGUAGE: 'en',
})); }));

View File

@ -24,7 +24,7 @@ vi.mock('node:module', () => {
}; };
}); });
import { checkForUpdates } from '../shared/utils/updateNotifier.js'; import { checkForUpdates } from '../shared/utils/index.js';
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();

View File

@ -11,7 +11,7 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { loadWorkflow } from '../infra/config/loaders/loader.js'; import { loadWorkflow } from '../infra/config/index.js';
describe('expert workflow parallel structure', () => { describe('expert workflow parallel structure', () => {
const workflow = loadWorkflow('expert', process.cwd()); const workflow = loadWorkflow('expert', process.cwd());

View File

@ -8,13 +8,11 @@ import {
callClaudeAgent, callClaudeAgent,
callClaudeSkill, callClaudeSkill,
type ClaudeCallOptions, type ClaudeCallOptions,
} from '../claude/client.js'; } from '../infra/claude/index.js';
import { loadCustomAgents, loadAgentPrompt } from '../infra/config/loaders/loader.js'; import { loadCustomAgents, loadAgentPrompt, loadGlobalConfig, loadProjectConfig } from '../infra/config/index.js';
import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
import { loadProjectConfig } from '../infra/config/project/projectConfig.js';
import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js'; import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js'; import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
import { createLogger } from '../shared/utils/debug.js'; import { createLogger } from '../shared/utils/index.js';
import type { RunAgentOptions } from './types.js'; import type { RunAgentOptions } from './types.js';
// Re-export for backward compatibility // Re-export for backward compatibility

View File

@ -2,7 +2,7 @@
* Type definitions for agent execution * Type definitions for agent execution
*/ */
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/types.js'; import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../infra/claude/index.js';
import type { PermissionMode } from '../core/models/index.js'; import type { PermissionMode } from '../core/models/index.js';
export type { StreamCallback }; export type { StreamCallback };

View File

@ -4,7 +4,7 @@
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, config). * Registers all named subcommands (run, watch, add, list, switch, clear, eject, config).
*/ */
import { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/paths.js'; import { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/index.js';
import { success } from '../../shared/ui/index.js'; import { success } from '../../shared/ui/index.js';
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js'; import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
import { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/index.js'; import { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/index.js';

View File

@ -8,7 +8,7 @@ import type { Command } from 'commander';
import type { TaskExecutionOptions } from '../../features/tasks/index.js'; import type { TaskExecutionOptions } from '../../features/tasks/index.js';
import type { ProviderType } from '../../infra/providers/index.js'; import type { ProviderType } from '../../infra/providers/index.js';
import { error } from '../../shared/ui/index.js'; import { error } from '../../shared/ui/index.js';
import { isIssueReference } from '../../infra/github/issue.js'; import { isIssueReference } from '../../infra/github/index.js';
/** /**
* Resolve --provider and --model options into TaskExecutionOptions. * Resolve --provider and --model options into TaskExecutionOptions.

View File

@ -6,7 +6,7 @@
* Import order matters: program setup commands routing parse. * Import order matters: program setup commands routing parse.
*/ */
import { checkForUpdates } from '../../shared/utils/updateNotifier.js'; import { checkForUpdates } from '../../shared/utils/index.js';
checkForUpdates(); checkForUpdates();

View File

@ -15,9 +15,9 @@ import {
getEffectiveDebugConfig, getEffectiveDebugConfig,
isVerboseMode, isVerboseMode,
} from '../../infra/config/index.js'; } from '../../infra/config/index.js';
import { setQuietMode } from '../../context.js'; import { setQuietMode } from '../../shared/context.js';
import { setLogLevel } from '../../shared/ui/index.js'; import { setLogLevel } from '../../shared/ui/index.js';
import { initDebugLogger, createLogger, setVerboseConsole } from '../../shared/utils/debug.js'; import { initDebugLogger, createLogger, setVerboseConsole } from '../../shared/utils/index.js';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { version: cliVersion } = require('../../../package.json') as { version: string }; const { version: cliVersion } = require('../../../package.json') as { version: string };

View File

@ -6,12 +6,12 @@
*/ */
import { info, error } from '../../shared/ui/index.js'; import { info, error } from '../../shared/ui/index.js';
import { getErrorMessage } from '../../shared/utils/error.js'; import { getErrorMessage } from '../../shared/utils/index.js';
import { resolveIssueTask, isIssueReference } from '../../infra/github/issue.js'; import { resolveIssueTask, isIssueReference } from '../../infra/github/index.js';
import { selectAndExecuteTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js'; import { selectAndExecuteTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
import { executePipeline } from '../../features/pipeline/index.js'; import { executePipeline } from '../../features/pipeline/index.js';
import { interactiveMode } from '../../features/interactive/index.js'; import { interactiveMode } from '../../features/interactive/index.js';
import { DEFAULT_WORKFLOW_NAME } from '../../constants.js'; import { DEFAULT_WORKFLOW_NAME } from '../../shared/constants.js';
import { program, resolvedCwd, pipelineMode } from './program.js'; import { program, resolvedCwd, pipelineMode } from './program.js';
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js'; import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';
@ -94,5 +94,6 @@ program
return; return;
} }
selectOptions.interactiveUserInput = true;
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides); await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
}); });

View File

@ -5,7 +5,7 @@
*/ */
import { z } from 'zod/v4'; import { z } from 'zod/v4';
import { DEFAULT_LANGUAGE } from '../../constants.js'; import { DEFAULT_LANGUAGE } from '../../shared/constants.js';
/** Agent model schema (opus, sonnet, haiku) */ /** Agent model schema (opus, sonnet, haiku) */
export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet'); export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet');
@ -107,6 +107,10 @@ export const WorkflowRuleSchema = z.object({
next: z.string().min(1).optional(), next: z.string().min(1).optional(),
/** Template for additional AI output */ /** Template for additional AI output */
appendix: z.string().optional(), appendix: z.string().optional(),
/** Require user input before continuing (interactive mode only) */
requires_user_input: z.boolean().optional(),
/** Rule applies only in interactive mode */
interactive_only: z.boolean().optional(),
}); });
/** Sub-step schema for parallel execution (agent is required) */ /** Sub-step schema for parallel execution (agent is required) */

View File

@ -13,6 +13,10 @@ export interface WorkflowRule {
next?: string; next?: string;
/** Template for additional AI output */ /** Template for additional AI output */
appendix?: string; appendix?: string;
/** Require user input before continuing (interactive mode only) */
requiresUserInput?: boolean;
/** Rule applies only in interactive mode */
interactiveOnly?: boolean;
/** Whether this condition uses ai() expression (set by loader) */ /** Whether this condition uses ai() expression (set by loader) */
isAiCondition?: boolean; isAiCondition?: boolean;
/** The condition text inside ai("...") for AI judge evaluation (set by loader) */ /** The condition text inside ai("...") for AI judge evaluation (set by loader) */

View File

@ -58,6 +58,8 @@ export class OptionsBuilder {
): RunAgentOptions { ): RunAgentOptions {
return { return {
...this.buildBaseOptions(step), ...this.buildBaseOptions(step),
// Do not pass permission mode in report/status phases.
permissionMode: undefined,
sessionId, sessionId,
allowedTools: overrides.allowedTools, allowedTools: overrides.allowedTools,
maxTurns: overrides.maxTurns, maxTurns: overrides.maxTurns,
@ -73,6 +75,7 @@ export class OptionsBuilder {
cwd: this.getCwd(), cwd: this.getCwd(),
reportDir: join(this.getProjectCwd(), this.getReportDir()), reportDir: join(this.getProjectCwd(), this.getReportDir()),
language: this.getLanguage(), language: this.getLanguage(),
interactive: this.engineOptions.interactive,
getSessionId: (agent: string) => state.agentSessions.get(agent), getSessionId: (agent: string) => state.agentSessions.get(agent),
buildResumeOptions: this.buildResumeOptions.bind(this), buildResumeOptions: this.buildResumeOptions.bind(this),
updateAgentSession, updateAgentSession,

View File

@ -15,7 +15,7 @@ import { ParallelLogger } from './parallel-logger.js';
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
import { detectMatchedRule } from '../evaluation/index.js'; import { detectMatchedRule } from '../evaluation/index.js';
import { incrementStepIteration } from './state-manager.js'; import { incrementStepIteration } from './state-manager.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger } from '../../../shared/utils/index.js';
import type { OptionsBuilder } from './OptionsBuilder.js'; import type { OptionsBuilder } from './OptionsBuilder.js';
import type { StepExecutor } from './StepExecutor.js'; import type { StepExecutor } from './StepExecutor.js';
import type { WorkflowEngineOptions } from '../types.js'; import type { WorkflowEngineOptions } from '../types.js';
@ -28,6 +28,13 @@ export interface ParallelRunnerDeps {
readonly engineOptions: WorkflowEngineOptions; readonly engineOptions: WorkflowEngineOptions;
readonly getCwd: () => string; readonly getCwd: () => string;
readonly getReportDir: () => string; readonly getReportDir: () => string;
readonly getInteractive: () => boolean;
readonly detectRuleIndex: (content: string, stepName: string) => number;
readonly callAiJudge: (
agentOutput: string,
conditions: Array<{ index: number; text: string }>,
options: { cwd: string }
) => Promise<number>;
} }
export class ParallelRunner { export class ParallelRunner {
@ -63,7 +70,13 @@ export class ParallelRunner {
: undefined; : undefined;
const phaseCtx = this.deps.optionsBuilder.buildPhaseRunnerContext(state, updateAgentSession); const phaseCtx = this.deps.optionsBuilder.buildPhaseRunnerContext(state, updateAgentSession);
const ruleCtx = { state, cwd: this.deps.getCwd() }; const ruleCtx = {
state,
cwd: this.deps.getCwd(),
interactive: this.deps.getInteractive(),
detectRuleIndex: this.deps.detectRuleIndex,
callAiJudge: this.deps.callAiJudge,
};
// Run all sub-steps concurrently // Run all sub-steps concurrently
const subResults = await Promise.all( const subResults = await Promise.all(

View File

@ -19,7 +19,7 @@ import { InstructionBuilder, isReportObjectConfig } from '../instruction/Instruc
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
import { detectMatchedRule } from '../evaluation/index.js'; import { detectMatchedRule } from '../evaluation/index.js';
import { incrementStepIteration, getPreviousOutput } from './state-manager.js'; import { incrementStepIteration, getPreviousOutput } from './state-manager.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger } from '../../../shared/utils/index.js';
import type { OptionsBuilder } from './OptionsBuilder.js'; import type { OptionsBuilder } from './OptionsBuilder.js';
const log = createLogger('step-executor'); const log = createLogger('step-executor');
@ -30,6 +30,13 @@ export interface StepExecutorDeps {
readonly getProjectCwd: () => string; readonly getProjectCwd: () => string;
readonly getReportDir: () => string; readonly getReportDir: () => string;
readonly getLanguage: () => Language | undefined; readonly getLanguage: () => Language | undefined;
readonly getInteractive: () => boolean;
readonly detectRuleIndex: (content: string, stepName: string) => number;
readonly callAiJudge: (
agentOutput: string,
conditions: Array<{ index: number; text: string }>,
options: { cwd: string }
) => Promise<number>;
} }
export class StepExecutor { export class StepExecutor {
@ -54,8 +61,9 @@ export class StepExecutor {
projectCwd: this.deps.getProjectCwd(), projectCwd: this.deps.getProjectCwd(),
userInputs: state.userInputs, userInputs: state.userInputs,
previousOutput: getPreviousOutput(state), previousOutput: getPreviousOutput(state),
reportDir: join(this.deps.getCwd(), this.deps.getReportDir()), reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()),
language: this.deps.getLanguage(), language: this.deps.getLanguage(),
interactive: this.deps.getInteractive(),
}).build(); }).build();
} }
@ -106,6 +114,9 @@ export class StepExecutor {
const match = await detectMatchedRule(step, response.content, tagContent, { const match = await detectMatchedRule(step, response.content, tagContent, {
state, state,
cwd: this.deps.getCwd(), cwd: this.deps.getCwd(),
interactive: this.deps.getInteractive(),
detectRuleIndex: this.deps.detectRuleIndex,
callAiJudge: this.deps.callAiJudge,
}); });
if (match) { if (match) {
log.debug('Rule matched', { step: step.name, ruleIndex: match.index, method: match.method }); log.debug('Rule matched', { step: step.name, ruleIndex: match.index, method: match.method });

View File

@ -25,10 +25,7 @@ import {
addUserInput as addUserInputToState, addUserInput as addUserInputToState,
incrementStepIteration, incrementStepIteration,
} from './state-manager.js'; } from './state-manager.js';
import { generateReportDir } from '../../../shared/utils/reportDir.js'; import { generateReportDir, getErrorMessage, createLogger } from '../../../shared/utils/index.js';
import { getErrorMessage } from '../../../shared/utils/error.js';
import { createLogger } from '../../../shared/utils/debug.js';
import { interruptAllQueries } from '../../../claude/query-manager.js';
import { OptionsBuilder } from './OptionsBuilder.js'; import { OptionsBuilder } from './OptionsBuilder.js';
import { StepExecutor } from './StepExecutor.js'; import { StepExecutor } from './StepExecutor.js';
import { ParallelRunner } from './ParallelRunner.js'; import { ParallelRunner } from './ParallelRunner.js';
@ -61,6 +58,12 @@ export class WorkflowEngine extends EventEmitter {
private readonly optionsBuilder: OptionsBuilder; private readonly optionsBuilder: OptionsBuilder;
private readonly stepExecutor: StepExecutor; private readonly stepExecutor: StepExecutor;
private readonly parallelRunner: ParallelRunner; private readonly parallelRunner: ParallelRunner;
private readonly detectRuleIndex: (content: string, stepName: string) => number;
private readonly callAiJudge: (
agentOutput: string,
conditions: Array<{ index: number; text: string }>,
options: { cwd: string }
) => Promise<number>;
constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions) { constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions) {
super(); super();
@ -74,6 +77,12 @@ export class WorkflowEngine extends EventEmitter {
this.ensureReportDirExists(); this.ensureReportDirExists();
this.validateConfig(); this.validateConfig();
this.state = createInitialState(config, options); this.state = createInitialState(config, options);
this.detectRuleIndex = options.detectRuleIndex ?? (() => {
throw new Error('detectRuleIndex is required for rule evaluation');
});
this.callAiJudge = options.callAiJudge ?? (async () => {
throw new Error('callAiJudge is required for rule evaluation');
});
// Initialize composed collaborators // Initialize composed collaborators
this.optionsBuilder = new OptionsBuilder( this.optionsBuilder = new OptionsBuilder(
@ -91,6 +100,9 @@ export class WorkflowEngine extends EventEmitter {
getProjectCwd: () => this.projectCwd, getProjectCwd: () => this.projectCwd,
getReportDir: () => this.reportDir, getReportDir: () => this.reportDir,
getLanguage: () => this.options.language, getLanguage: () => this.options.language,
getInteractive: () => this.options.interactive === true,
detectRuleIndex: this.detectRuleIndex,
callAiJudge: this.callAiJudge,
}); });
this.parallelRunner = new ParallelRunner({ this.parallelRunner = new ParallelRunner({
@ -99,6 +111,9 @@ export class WorkflowEngine extends EventEmitter {
engineOptions: this.options, engineOptions: this.options,
getCwd: () => this.cwd, getCwd: () => this.cwd,
getReportDir: () => this.reportDir, getReportDir: () => this.reportDir,
getInteractive: () => this.options.interactive === true,
detectRuleIndex: this.detectRuleIndex,
callAiJudge: this.callAiJudge,
}); });
log.debug('WorkflowEngine initialized', { log.debug('WorkflowEngine initialized', {
@ -183,7 +198,6 @@ export class WorkflowEngine extends EventEmitter {
if (this.abortRequested) return; if (this.abortRequested) return;
this.abortRequested = true; this.abortRequested = true;
log.info('Abort requested'); log.info('Abort requested');
interruptAllQueries();
} }
/** Check if abort has been requested */ /** Check if abort has been requested */
@ -345,6 +359,31 @@ export class WorkflowEngine extends EventEmitter {
nextStep, nextStep,
}); });
if (response.matchedRuleIndex != null && step.rules) {
const matchedRule = step.rules[response.matchedRuleIndex];
if (matchedRule?.requiresUserInput) {
if (!this.options.onUserInput) {
this.state.status = 'aborted';
this.emit('workflow:abort', this.state, 'User input required but no handler is configured');
break;
}
const userInput = await this.options.onUserInput({
step,
response,
prompt: response.content,
});
if (userInput === null) {
this.state.status = 'aborted';
this.emit('workflow:abort', this.state, 'User input cancelled');
break;
}
this.addUserInput(userInput);
this.emit('step:user_input', step, userInput);
this.state.currentStep = step.name;
continue;
}
}
if (nextStep === COMPLETE_STEP) { if (nextStep === COMPLETE_STEP) {
this.state.status = 'completed'; this.state.status = 'completed';
this.emit('workflow:complete', this.state); this.emit('workflow:complete', this.state);
@ -403,6 +442,29 @@ export class WorkflowEngine extends EventEmitter {
const nextStep = this.resolveNextStep(step, response); const nextStep = this.resolveNextStep(step, response);
const isComplete = nextStep === COMPLETE_STEP || nextStep === ABORT_STEP; const isComplete = nextStep === COMPLETE_STEP || nextStep === ABORT_STEP;
if (response.matchedRuleIndex != null && step.rules) {
const matchedRule = step.rules[response.matchedRuleIndex];
if (matchedRule?.requiresUserInput) {
if (!this.options.onUserInput) {
this.state.status = 'aborted';
return { response, nextStep: ABORT_STEP, isComplete: true, loopDetected: loopCheck.isLoop };
}
const userInput = await this.options.onUserInput({
step,
response,
prompt: response.content,
});
if (userInput === null) {
this.state.status = 'aborted';
return { response, nextStep: ABORT_STEP, isComplete: true, loopDetected: loopCheck.isLoop };
}
this.addUserInput(userInput);
this.emit('step:user_input', step, userInput);
this.state.currentStep = step.name;
return { response, nextStep: step.name, isComplete: false, loopDetected: loopCheck.isLoop };
}
}
if (!isComplete) { if (!isComplete) {
this.state.currentStep = nextStep; this.state.currentStep = nextStep;
} else { } else {

View File

@ -5,7 +5,7 @@
*/ */
import type { WorkflowStep, WorkflowState } from '../../models/types.js'; import type { WorkflowStep, WorkflowState } from '../../models/types.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger } from '../../../shared/utils/index.js';
const log = createLogger('aggregate-evaluator'); const log = createLogger('aggregate-evaluator');

View File

@ -11,8 +11,8 @@ import type {
WorkflowState, WorkflowState,
RuleMatchMethod, RuleMatchMethod,
} from '../../models/types.js'; } from '../../models/types.js';
import { detectRuleIndex, callAiJudge } from '../../../claude/client.js'; import type { AiJudgeCaller, RuleIndexDetector } from '../types.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger } from '../../../shared/utils/index.js';
import { AggregateEvaluator } from './AggregateEvaluator.js'; import { AggregateEvaluator } from './AggregateEvaluator.js';
const log = createLogger('rule-evaluator'); const log = createLogger('rule-evaluator');
@ -27,6 +27,12 @@ export interface RuleEvaluatorContext {
state: WorkflowState; state: WorkflowState;
/** Working directory (for AI judge calls) */ /** Working directory (for AI judge calls) */
cwd: string; cwd: string;
/** Whether interactive-only rules are enabled */
interactive?: boolean;
/** Rule tag index detector */
detectRuleIndex: RuleIndexDetector;
/** AI judge caller */
callAiJudge: AiJudgeCaller;
} }
/** /**
@ -50,6 +56,7 @@ export class RuleEvaluator {
async evaluate(agentContent: string, tagContent: string): Promise<RuleMatch | undefined> { async evaluate(agentContent: string, tagContent: string): Promise<RuleMatch | undefined> {
if (!this.step.rules || this.step.rules.length === 0) return undefined; if (!this.step.rules || this.step.rules.length === 0) return undefined;
const interactiveEnabled = this.ctx.interactive === true;
// 1. Aggregate conditions (all/any) — only meaningful for parallel parent steps // 1. Aggregate conditions (all/any) — only meaningful for parallel parent steps
const aggEvaluator = new AggregateEvaluator(this.step, this.ctx.state); const aggEvaluator = new AggregateEvaluator(this.step, this.ctx.state);
@ -60,17 +67,27 @@ export class RuleEvaluator {
// 2. Tag detection from Phase 3 output // 2. Tag detection from Phase 3 output
if (tagContent) { if (tagContent) {
const ruleIndex = detectRuleIndex(tagContent, this.step.name); const ruleIndex = this.ctx.detectRuleIndex(tagContent, this.step.name);
if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) { if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) {
return { index: ruleIndex, method: 'phase3_tag' }; const rule = this.step.rules[ruleIndex];
if (rule?.interactiveOnly && !interactiveEnabled) {
// Skip interactive-only rule in non-interactive mode
} else {
return { index: ruleIndex, method: 'phase3_tag' };
}
} }
} }
// 3. Tag detection from Phase 1 output (fallback) // 3. Tag detection from Phase 1 output (fallback)
if (agentContent) { if (agentContent) {
const ruleIndex = detectRuleIndex(agentContent, this.step.name); const ruleIndex = this.ctx.detectRuleIndex(agentContent, this.step.name);
if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) { if (ruleIndex >= 0 && ruleIndex < this.step.rules.length) {
return { index: ruleIndex, method: 'phase1_tag' }; const rule = this.step.rules[ruleIndex];
if (rule?.interactiveOnly && !interactiveEnabled) {
// Skip interactive-only rule in non-interactive mode
} else {
return { index: ruleIndex, method: 'phase1_tag' };
}
} }
} }
@ -99,6 +116,9 @@ export class RuleEvaluator {
const aiConditions: { index: number; text: string }[] = []; const aiConditions: { index: number; text: string }[] = [];
for (let i = 0; i < this.step.rules.length; i++) { for (let i = 0; i < this.step.rules.length; i++) {
const rule = this.step.rules[i]!; const rule = this.step.rules[i]!;
if (rule.interactiveOnly && this.ctx.interactive !== true) {
continue;
}
if (rule.isAiCondition && rule.aiConditionText) { if (rule.isAiCondition && rule.aiConditionText) {
aiConditions.push({ index: i, text: rule.aiConditionText }); aiConditions.push({ index: i, text: rule.aiConditionText });
} }
@ -112,7 +132,7 @@ export class RuleEvaluator {
}); });
const judgeConditions = aiConditions.map((c, i) => ({ index: i, text: c.text })); const judgeConditions = aiConditions.map((c, i) => ({ index: i, text: c.text }));
const judgeResult = await callAiJudge(agentOutput, judgeConditions, { cwd: this.ctx.cwd }); const judgeResult = await this.ctx.callAiJudge(agentOutput, judgeConditions, { cwd: this.ctx.cwd });
if (judgeResult >= 0 && judgeResult < aiConditions.length) { if (judgeResult >= 0 && judgeResult < aiConditions.length) {
const matched = aiConditions[judgeResult]!; const matched = aiConditions[judgeResult]!;
@ -136,14 +156,17 @@ export class RuleEvaluator {
private async evaluateAllConditionsViaAiJudge(agentOutput: string): Promise<number> { private async evaluateAllConditionsViaAiJudge(agentOutput: string): Promise<number> {
if (!this.step.rules || this.step.rules.length === 0) return -1; if (!this.step.rules || this.step.rules.length === 0) return -1;
const conditions = this.step.rules.map((rule, i) => ({ index: i, text: rule.condition })); const conditions = this.step.rules
.map((rule, i) => ({ index: i, text: rule.condition, interactiveOnly: rule.interactiveOnly }))
.filter((rule) => this.ctx.interactive === true || !rule.interactiveOnly)
.map((rule) => ({ index: rule.index, text: rule.text }));
log.debug('Evaluating all conditions via AI judge (final fallback)', { log.debug('Evaluating all conditions via AI judge (final fallback)', {
step: this.step.name, step: this.step.name,
conditionCount: conditions.length, conditionCount: conditions.length,
}); });
const judgeResult = await callAiJudge(agentOutput, conditions, { cwd: this.ctx.cwd }); const judgeResult = await this.ctx.callAiJudge(agentOutput, conditions, { cwd: this.ctx.cwd });
if (judgeResult >= 0 && judgeResult < conditions.length) { if (judgeResult >= 0 && judgeResult < conditions.length) {
log.debug('AI judge (fallback) matched condition', { log.debug('AI judge (fallback) matched condition', {

View File

@ -23,6 +23,7 @@ export type {
StreamEvent, StreamEvent,
StreamCallback, StreamCallback,
PermissionHandler, PermissionHandler,
PermissionResult,
AskUserQuestionHandler, AskUserQuestionHandler,
ProviderType, ProviderType,
} from './types.js'; } from './types.js';

View File

@ -136,7 +136,12 @@ export class InstructionBuilder {
// 7. Status Output Rules (for tag-based detection in Phase 1) // 7. Status Output Rules (for tag-based detection in Phase 1)
if (hasTagBasedRules(this.step)) { if (hasTagBasedRules(this.step)) {
const statusRulesPrompt = generateStatusRulesFromRules(this.step.name, this.step.rules!, language); const statusRulesPrompt = generateStatusRulesFromRules(
this.step.name,
this.step.rules!,
language,
{ interactive: this.context.interactive },
);
sections.push(statusRulesPrompt); sections.push(statusRulesPrompt);
} }

View File

@ -22,13 +22,17 @@ const REPORT_PHASE_STRINGS = {
noSourceEdit: '**Do NOT modify project source files.** Only output report files.', noSourceEdit: '**Do NOT modify project source files.** Only output report files.',
reportDirOnly: '**Use only the Report Directory files shown above.** Do not search or open reports outside that directory.', reportDirOnly: '**Use only the Report Directory files shown above.** Do not search or open reports outside that directory.',
instructionBody: 'Output the results of your previous work as a report.', instructionBody: 'Output the results of your previous work as a report.',
reportJsonFormat: 'Output a JSON object mapping each report file name to its content.', reportJsonFormat: 'JSON format is optional. If you use JSON, map report file names to content (file name key only).',
reportPlainAllowed: 'You may output plain text. If there are multiple report files, the same content will be written to each file.',
reportOnlyOutput: 'Output only the report content (no status tags, no commentary).',
}, },
ja: { ja: {
noSourceEdit: '**プロジェクトのソースファイルを変更しないでください。** レポートファイルのみ出力してください。', noSourceEdit: '**プロジェクトのソースファイルを変更しないでください。** レポートファイルのみ出力してください。',
reportDirOnly: '**上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。', reportDirOnly: '**上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。',
instructionBody: '前のステップの作業結果をレポートとして出力してください。', instructionBody: '前のステップの作業結果をレポートとして出力してください。',
reportJsonFormat: 'レポートファイル名→内容のJSONオブジェクトで出力してください。', reportJsonFormat: 'JSON形式は任意です。JSONを使う場合は「レポートファイル名→内容」のオブジェクトにしてくださいキーはファイル名のみ。',
reportPlainAllowed: '本文のみの出力も可です。複数ファイルの場合は同じ内容が各ファイルに書き込まれます。',
reportOnlyOutput: 'レポート本文のみを出力してください(ステータスタグやコメントは禁止)。',
}, },
} as const; } as const;
@ -103,6 +107,8 @@ export class ReportInstructionBuilder {
r.instructionBody, r.instructionBody,
r.reportJsonFormat, r.reportJsonFormat,
]; ];
instrParts.push(r.reportPlainAllowed);
instrParts.push(r.reportOnlyOutput);
// Report output instruction (auto-generated or explicit order) // Report output instruction (auto-generated or explicit order)
const reportContext: InstructionContext = { const reportContext: InstructionContext = {

View File

@ -28,6 +28,8 @@ const STATUS_JUDGMENT_STRINGS = {
export interface StatusJudgmentContext { export interface StatusJudgmentContext {
/** Language */ /** Language */
language?: Language; language?: Language;
/** Whether interactive-only rules are enabled */
interactive?: boolean;
} }
/** /**
@ -52,7 +54,12 @@ export class StatusJudgmentBuilder {
sections.push(s.header); sections.push(s.header);
// Status rules (criteria table + output format) // Status rules (criteria table + output format)
const generatedPrompt = generateStatusRulesFromRules(this.step.name, this.step.rules, language); const generatedPrompt = generateStatusRulesFromRules(
this.step.name,
this.step.rules,
language,
{ interactive: this.context.interactive },
);
sections.push(generatedPrompt); sections.push(generatedPrompt);
return sections.join('\n\n'); return sections.join('\n\n');

View File

@ -31,6 +31,8 @@ export interface InstructionContext {
reportDir?: string; reportDir?: string;
/** Language for metadata rendering. Defaults to 'en'. */ /** Language for metadata rendering. Defaults to 'en'. */
language?: Language; language?: Language;
/** Whether interactive-only rules are enabled */
interactive?: boolean;
} }
/** Execution environment metadata prepended to agent instructions */ /** Execution environment metadata prepended to agent instructions */

View File

@ -47,9 +47,14 @@ export function generateStatusRulesFromRules(
stepName: string, stepName: string,
rules: WorkflowRule[], rules: WorkflowRule[],
language: Language, language: Language,
options?: { interactive?: boolean },
): string { ): string {
const tag = stepName.toUpperCase(); const tag = stepName.toUpperCase();
const strings = RULES_PROMPT_STRINGS[language]; const strings = RULES_PROMPT_STRINGS[language];
const interactiveEnabled = options?.interactive;
const visibleRules = rules
.map((rule, index) => ({ rule, index }))
.filter(({ rule }) => interactiveEnabled !== false || !rule.interactiveOnly);
const lines: string[] = []; const lines: string[] = [];
@ -58,8 +63,8 @@ export function generateStatusRulesFromRules(
lines.push(''); lines.push('');
lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`); lines.push(`| ${strings.headerNum} | ${strings.headerCondition} | ${strings.headerTag} |`);
lines.push('|---|------|------|'); lines.push('|---|------|------|');
for (const [i, rule] of rules.entries()) { for (const { rule, index } of visibleRules) {
lines.push(`| ${i + 1} | ${rule.condition} | \`[${tag}:${i + 1}]\` |`); lines.push(`| ${index + 1} | ${rule.condition} | \`[${tag}:${index + 1}]\` |`);
} }
lines.push(''); lines.push('');
@ -68,18 +73,18 @@ export function generateStatusRulesFromRules(
lines.push(''); lines.push('');
lines.push(strings.outputInstruction); lines.push(strings.outputInstruction);
lines.push(''); lines.push('');
for (const [i, rule] of rules.entries()) { for (const { rule, index } of visibleRules) {
lines.push(`- \`[${tag}:${i + 1}]\`${rule.condition}`); lines.push(`- \`[${tag}:${index + 1}]\`${rule.condition}`);
} }
// Appendix templates (if any rules have appendix) // Appendix templates (if any rules have appendix)
const rulesWithAppendix = rules.filter((r) => r.appendix); const rulesWithAppendix = visibleRules.filter(({ rule }) => rule.appendix);
if (rulesWithAppendix.length > 0) { if (rulesWithAppendix.length > 0) {
lines.push(''); lines.push('');
lines.push(strings.appendixHeading); lines.push(strings.appendixHeading);
for (const [i, rule] of rules.entries()) { for (const { rule, index } of visibleRules) {
if (!rule.appendix) continue; if (!rule.appendix) continue;
const tagStr = `[${tag}:${i + 1}]`; const tagStr = `[${tag}:${index + 1}]`;
lines.push(''); lines.push('');
lines.push(strings.appendixInstruction.replace('{tag}', tagStr)); lines.push(strings.appendixInstruction.replace('{tag}', tagStr));
lines.push('```'); lines.push('```');

View File

@ -13,7 +13,7 @@ import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder
import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js'; import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js';
import { hasTagBasedRules } from './evaluation/rule-utils.js'; import { hasTagBasedRules } from './evaluation/rule-utils.js';
import { isReportObjectConfig } from './instruction/InstructionBuilder.js'; import { isReportObjectConfig } from './instruction/InstructionBuilder.js';
import { createLogger } from '../../shared/utils/debug.js'; import { createLogger } from '../../shared/utils/index.js';
const log = createLogger('phase-runner'); const log = createLogger('phase-runner');
@ -24,6 +24,8 @@ export interface PhaseRunnerContext {
reportDir: string; reportDir: string;
/** Language for instructions */ /** Language for instructions */
language?: Language; language?: Language;
/** Whether interactive-only rules are enabled */
interactive?: boolean;
/** Get agent session ID */ /** Get agent session ID */
getSessionId: (agent: string) => string | undefined; getSessionId: (agent: string) => string | undefined;
/** Build resume options for a step */ /** Build resume options for a step */
@ -76,6 +78,7 @@ function getReportFiles(report: WorkflowStep['report']): string[] {
function resolveReportOutputs( function resolveReportOutputs(
report: WorkflowStep['report'], report: WorkflowStep['report'],
reportDir: string,
content: string, content: string,
): Map<string, string> { ): Map<string, string> {
if (!report) return new Map(); if (!report) return new Map();
@ -83,12 +86,17 @@ function resolveReportOutputs(
const files = getReportFiles(report); const files = getReportFiles(report);
const json = parseReportJson(content); const json = parseReportJson(content);
if (!json) { if (!json) {
throw new Error('Report output must be a JSON object mapping report file names to content.'); const raw = content;
if (!raw || raw.trim().length === 0) {
throw new Error('Report output is empty.');
}
return new Map(files.map((file) => [file, raw]));
} }
const outputs = new Map<string, string>(); const outputs = new Map<string, string>();
for (const file of files) { for (const file of files) {
const value = json[file]; const absolutePath = resolve(reportDir, file);
const value = json[file] ?? json[absolutePath];
if (typeof value !== 'string') { if (typeof value !== 'string') {
throw new Error(`Report output missing content for file: ${file}`); throw new Error(`Report output missing content for file: ${file}`);
} }
@ -142,7 +150,7 @@ export async function runReportPhase(
}); });
const reportResponse = await runAgent(step.agent, reportInstruction, reportOptions); const reportResponse = await runAgent(step.agent, reportInstruction, reportOptions);
const outputs = resolveReportOutputs(step.report, reportResponse.content); const outputs = resolveReportOutputs(step.report, ctx.reportDir, reportResponse.content);
for (const [fileName, content] of outputs.entries()) { for (const [fileName, content] of outputs.entries()) {
writeReportFile(ctx.reportDir, fileName, content); writeReportFile(ctx.reportDir, fileName, content);
} }
@ -171,6 +179,7 @@ export async function runStatusJudgmentPhase(
const judgmentInstruction = new StatusJudgmentBuilder(step, { const judgmentInstruction = new StatusJudgmentBuilder(step, {
language: ctx.language, language: ctx.language,
interactive: ctx.interactive,
}).build(); }).build();
const judgmentOptions = ctx.buildResumeOptions(step, sessionId, { const judgmentOptions = ctx.buildResumeOptions(step, sessionId, {

View File

@ -5,8 +5,8 @@
* used by the workflow execution engine. * used by the workflow execution engine.
*/ */
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js'; import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js';
import type { PermissionResult } from '../../claude/types.js';
export type ProviderType = 'claude' | 'codex' | 'mock'; export type ProviderType = 'claude' | 'codex' | 'mock';
@ -66,11 +66,13 @@ export type StreamCallback = (event: StreamEvent) => void;
export interface PermissionRequest { export interface PermissionRequest {
toolName: string; toolName: string;
input: Record<string, unknown>; input: Record<string, unknown>;
suggestions?: Array<Record<string, unknown>>; suggestions?: PermissionUpdate[];
blockedPath?: string; blockedPath?: string;
decisionReason?: string; decisionReason?: string;
} }
export type { PermissionResult, PermissionUpdate };
export type PermissionHandler = (request: PermissionRequest) => Promise<PermissionResult>; export type PermissionHandler = (request: PermissionRequest) => Promise<PermissionResult>;
export interface AskUserQuestionInput { export interface AskUserQuestionInput {
@ -89,6 +91,19 @@ export type AskUserQuestionHandler = (
input: AskUserQuestionInput input: AskUserQuestionInput
) => Promise<Record<string, string>>; ) => Promise<Record<string, string>>;
export type RuleIndexDetector = (content: string, stepName: string) => number;
export interface AiJudgeCondition {
index: number;
text: string;
}
export type AiJudgeCaller = (
agentOutput: string,
conditions: AiJudgeCondition[],
options: { cwd: string }
) => Promise<number>;
/** Events emitted by workflow engine */ /** Events emitted by workflow engine */
export interface WorkflowEvents { export interface WorkflowEvents {
'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void; 'step:start': (step: WorkflowStep, iteration: number, instruction: string) => void;
@ -157,6 +172,12 @@ export interface WorkflowEngineOptions {
language?: Language; language?: Language;
provider?: ProviderType; provider?: ProviderType;
model?: string; model?: string;
/** Enable interactive-only rules and user-input transitions */
interactive?: boolean;
/** Rule tag index detector (required for rules evaluation) */
detectRuleIndex?: RuleIndexDetector;
/** AI judge caller (required for rules evaluation) */
callAiJudge?: AiJudgeCaller;
} }
/** Loop detection result */ /** Loop detection result */

View File

@ -7,8 +7,13 @@
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../../infra/config/paths.js'; import {
import { getLanguage } from '../../infra/config/global/globalConfig.js'; getGlobalWorkflowsDir,
getGlobalAgentsDir,
getBuiltinWorkflowsDir,
getBuiltinAgentsDir,
getLanguage,
} from '../../infra/config/index.js';
import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js'; import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js';
/** /**

View File

@ -7,15 +7,15 @@
import chalk from 'chalk'; import chalk from 'chalk';
import { info, success } from '../../shared/ui/index.js'; import { info, success } from '../../shared/ui/index.js';
import { selectOption } from '../../prompt/index.js'; import { selectOption } from '../../shared/prompt/index.js';
import { import {
loadProjectConfig, loadProjectConfig,
updateProjectConfig, updateProjectConfig,
type PermissionMode, } from '../../infra/config/index.js';
} from '../../infra/config/project/projectConfig.js'; import type { PermissionMode } from '../../infra/config/index.js';
// Re-export for convenience // Re-export for convenience
export type { PermissionMode } from '../../infra/config/project/projectConfig.js'; export type { PermissionMode } from '../../infra/config/index.js';
/** /**
* Get permission mode options for selection * Get permission mode options for selection

View File

@ -2,10 +2,9 @@
* Workflow switching command * Workflow switching command
*/ */
import { listWorkflows, loadWorkflow } from '../../infra/config/loaders/workflowLoader.js'; import { listWorkflows, loadWorkflow, getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/index.js';
import { getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/paths.js';
import { info, success, error } from '../../shared/ui/index.js'; import { info, success, error } from '../../shared/ui/index.js';
import { selectOption } from '../../prompt/index.js'; import { selectOption } from '../../shared/prompt/index.js';
/** /**
* Get all available workflow options * Get all available workflow options

View File

@ -15,14 +15,12 @@ import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import chalk from 'chalk'; import chalk from 'chalk';
import type { Language } from '../../core/models/index.js'; import type { Language } from '../../core/models/index.js';
import { loadGlobalConfig } from '../../infra/config/global/globalConfig.js'; import { loadGlobalConfig, loadAgentSessions, updateAgentSession } from '../../infra/config/index.js';
import { isQuietMode } from '../../context.js'; import { isQuietMode } from '../../shared/context.js';
import { loadAgentSessions, updateAgentSession } from '../../infra/config/paths.js';
import { getProvider, type ProviderType } from '../../infra/providers/index.js'; import { getProvider, type ProviderType } from '../../infra/providers/index.js';
import { selectOption } from '../../prompt/index.js'; import { selectOption } from '../../shared/prompt/index.js';
import { getLanguageResourcesDir } from '../../resources/index.js'; import { getLanguageResourcesDir } from '../../infra/resources/index.js';
import { createLogger } from '../../shared/utils/debug.js'; import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
import { getErrorMessage } from '../../shared/utils/error.js';
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js'; import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
const log = createLogger('interactive'); const log = createLogger('interactive');
@ -363,14 +361,18 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
} }
// Handle slash commands // Handle slash commands
if (trimmed === '/go') { if (trimmed.startsWith('/go')) {
const summaryPrompt = buildSummaryPrompt( const userNote = trimmed.slice(3).trim();
let summaryPrompt = buildSummaryPrompt(
history, history,
!!sessionId, !!sessionId,
prompts.summaryPrompt, prompts.summaryPrompt,
prompts.noTranscript, prompts.noTranscript,
prompts.conversationLabel, prompts.conversationLabel,
); );
if (summaryPrompt && userNote) {
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
}
if (!summaryPrompt) { if (!summaryPrompt) {
info(prompts.ui.noConversation); info(prompts.ui.noConversation);
continue; continue;

View File

@ -10,22 +10,27 @@
*/ */
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { fetchIssue, formatIssueAsTask, checkGhCli } from '../../infra/github/issue.js'; import {
import type { GitHubIssue } from '../../infra/github/types.js'; fetchIssue,
import { createPullRequest, pushBranch, buildPrBody } from '../../infra/github/pr.js'; formatIssueAsTask,
import { stageAndCommit } from '../../infra/task/git.js'; checkGhCli,
createPullRequest,
pushBranch,
buildPrBody,
type GitHubIssue,
} from '../../infra/github/index.js';
import { stageAndCommit } from '../../infra/task/index.js';
import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js'; import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js';
import { loadGlobalConfig } from '../../infra/config/global/globalConfig.js'; import { loadGlobalConfig } from '../../infra/config/index.js';
import { info, error, success, status, blankLine } from '../../shared/ui/index.js'; import { info, error, success, status, blankLine } from '../../shared/ui/index.js';
import { createLogger } from '../../shared/utils/debug.js'; import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
import { getErrorMessage } from '../../shared/utils/error.js';
import type { PipelineConfig } from '../../core/models/index.js'; import type { PipelineConfig } from '../../core/models/index.js';
import { import {
EXIT_ISSUE_FETCH_FAILED, EXIT_ISSUE_FETCH_FAILED,
EXIT_WORKFLOW_FAILED, EXIT_WORKFLOW_FAILED,
EXIT_GIT_OPERATION_FAILED, EXIT_GIT_OPERATION_FAILED,
EXIT_PR_CREATION_FAILED, EXIT_PR_CREATION_FAILED,
} from '../../exitCodes.js'; } from '../../shared/exitCodes.js';
export type { PipelineExecutionOptions }; export type { PipelineExecutionOptions };

View File

@ -8,18 +8,14 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { stringify as stringifyYaml } from 'yaml'; import { stringify as stringifyYaml } from 'yaml';
import { promptInput, confirm, selectOption } from '../../../prompt/index.js'; import { promptInput, confirm, selectOption } from '../../../shared/prompt/index.js';
import { success, info } from '../../../shared/ui/index.js'; import { success, info } from '../../../shared/ui/index.js';
import { summarizeTaskName } from '../../../infra/task/summarize.js'; import { summarizeTaskName, type TaskFileData } from '../../../infra/task/index.js';
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js'; import { loadGlobalConfig, listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
import { getProvider, type ProviderType } from '../../../infra/providers/index.js'; import { getProvider, type ProviderType } from '../../../infra/providers/index.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
import { getErrorMessage } from '../../../shared/utils/error.js'; import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/index.js';
import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js';
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
import { interactiveMode } from '../../interactive/index.js'; import { interactiveMode } from '../../interactive/index.js';
import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/issue.js';
import type { TaskFileData } from '../../../infra/task/schema.js';
const log = createLogger('add-task'); const log = createLogger('add-task');

View File

@ -6,16 +6,13 @@
* mixing CLI parsing with business logic. * mixing CLI parsing with business logic.
*/ */
import { getCurrentWorkflow } from '../../../infra/config/paths.js'; import { getCurrentWorkflow, listWorkflows, isWorkflowPath } from '../../../infra/config/index.js';
import { listWorkflows, isWorkflowPath } from '../../../infra/config/loaders/workflowLoader.js'; import { selectOptionWithDefault, confirm } from '../../../shared/prompt/index.js';
import { selectOptionWithDefault, confirm } from '../../../prompt/index.js'; import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
import { createSharedClone } from '../../../infra/task/clone.js'; import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js';
import { summarizeTaskName } from '../../../infra/task/summarize.js';
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
import { info, error, success } from '../../../shared/ui/index.js'; import { info, error, success } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger } from '../../../shared/utils/index.js';
import { createPullRequest, buildPrBody } from '../../../infra/github/pr.js'; import { createPullRequest, buildPrBody } from '../../../infra/github/index.js';
import { executeTask } from './taskExecution.js'; import { executeTask } from './taskExecution.js';
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js'; import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
@ -136,6 +133,7 @@ export async function selectAndExecuteTask(
workflowIdentifier, workflowIdentifier,
projectCwd: cwd, projectCwd: cwd,
agentOverrides, agentOverrides,
interactiveUserInput: options?.interactiveUserInput === true,
}); });
if (taskSuccess && isWorktree) { if (taskSuccess && isWorktree) {

View File

@ -2,8 +2,7 @@
* Session management helpers for agent execution * Session management helpers for agent execution
*/ */
import { loadAgentSessions, updateAgentSession } from '../../../infra/config/paths.js'; import { loadAgentSessions, updateAgentSession, loadGlobalConfig } from '../../../infra/config/index.js';
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js';
import type { AgentResponse } from '../../../core/models/index.js'; import type { AgentResponse } from '../../../core/models/index.js';
/** /**

View File

@ -3,10 +3,7 @@
*/ */
import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../../infra/config/index.js'; import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../../infra/config/index.js';
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js'; import { TaskRunner, type TaskInfo, createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
import { createSharedClone } from '../../../infra/task/clone.js';
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js';
import { summarizeTaskName } from '../../../infra/task/summarize.js';
import { import {
header, header,
info, info,
@ -15,10 +12,9 @@ import {
status, status,
blankLine, blankLine,
} from '../../../shared/ui/index.js'; } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
import { getErrorMessage } from '../../../shared/utils/error.js';
import { executeWorkflow } from './workflowExecution.js'; import { executeWorkflow } from './workflowExecution.js';
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js'; import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js'; import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js';
export type { TaskExecutionOptions, ExecuteTaskOptions }; export type { TaskExecutionOptions, ExecuteTaskOptions };
@ -29,7 +25,7 @@ const log = createLogger('task');
* Execute a single task with workflow. * Execute a single task with workflow.
*/ */
export async function executeTask(options: ExecuteTaskOptions): Promise<boolean> { export async function executeTask(options: ExecuteTaskOptions): Promise<boolean> {
const { task, cwd, workflowIdentifier, projectCwd, agentOverrides } = options; const { task, cwd, workflowIdentifier, projectCwd, agentOverrides, interactiveUserInput } = options;
const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, projectCwd); const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, projectCwd);
if (!workflowConfig) { if (!workflowConfig) {
@ -54,6 +50,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<boolean>
language: globalConfig.language, language: globalConfig.language,
provider: agentOverrides?.provider, provider: agentOverrides?.provider,
model: agentOverrides?.model, model: agentOverrides?.model,
interactiveUserInput,
}); });
return result.success; return result.success;
} }

View File

@ -21,6 +21,8 @@ export interface WorkflowExecutionOptions {
language?: Language; language?: Language;
provider?: ProviderType; provider?: ProviderType;
model?: string; model?: string;
/** Enable interactive user input during step transitions */
interactiveUserInput?: boolean;
} }
export interface TaskExecutionOptions { export interface TaskExecutionOptions {
@ -39,6 +41,8 @@ export interface ExecuteTaskOptions {
projectCwd: string; projectCwd: string;
/** Agent provider/model overrides */ /** Agent provider/model overrides */
agentOverrides?: TaskExecutionOptions; agentOverrides?: TaskExecutionOptions;
/** Enable interactive user input during step transitions */
interactiveUserInput?: boolean;
} }
export interface PipelineExecutionOptions { export interface PipelineExecutionOptions {
@ -73,4 +77,6 @@ export interface SelectAndExecuteOptions {
repo?: string; repo?: string;
workflow?: string; workflow?: string;
createWorktree?: boolean | undefined; createWorktree?: boolean | undefined;
/** Enable interactive user input during step transitions */
interactiveUserInput?: boolean;
} }

View File

@ -3,9 +3,10 @@
*/ */
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { WorkflowEngine, type IterationLimitRequest } from '../../../core/workflow/index.js'; import { WorkflowEngine, type IterationLimitRequest, type UserInputRequest } from '../../../core/workflow/index.js';
import type { WorkflowConfig } from '../../../core/models/index.js'; import type { WorkflowConfig } from '../../../core/models/index.js';
import type { WorkflowExecutionResult, WorkflowExecutionOptions } from './types.js'; import type { WorkflowExecutionResult, WorkflowExecutionOptions } from './types.js';
import { callAiJudge, detectRuleIndex, interruptAllQueries } from '../../../infra/claude/index.js';
export type { WorkflowExecutionResult, WorkflowExecutionOptions }; export type { WorkflowExecutionResult, WorkflowExecutionOptions };
@ -14,9 +15,9 @@ import {
updateAgentSession, updateAgentSession,
loadWorktreeSessions, loadWorktreeSessions,
updateWorktreeSession, updateWorktreeSession,
} from '../../../infra/config/paths.js'; loadGlobalConfig,
import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js'; } from '../../../infra/config/index.js';
import { isQuietMode } from '../../../context.js'; import { isQuietMode } from '../../../shared/context.js';
import { import {
header, header,
info, info,
@ -38,11 +39,10 @@ import {
type NdjsonStepComplete, type NdjsonStepComplete,
type NdjsonWorkflowComplete, type NdjsonWorkflowComplete,
type NdjsonWorkflowAbort, type NdjsonWorkflowAbort,
} from '../../../infra/fs/session.js'; } from '../../../infra/fs/index.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger, notifySuccess, notifyError } from '../../../shared/utils/index.js';
import { notifySuccess, notifyError } from '../../../shared/utils/notification.js'; import { selectOption, promptInput } from '../../../shared/prompt/index.js';
import { selectOption, promptInput } from '../../../prompt/index.js'; import { EXIT_SIGINT } from '../../../shared/exitCodes.js';
import { EXIT_SIGINT } from '../../../exitCodes.js';
const log = createLogger('workflow'); const log = createLogger('workflow');
@ -75,6 +75,7 @@ export async function executeWorkflow(
): Promise<WorkflowExecutionResult> { ): Promise<WorkflowExecutionResult> {
const { const {
headerPrefix = 'Running Workflow:', headerPrefix = 'Running Workflow:',
interactiveUserInput = false,
} = options; } = options;
// projectCwd is where .takt/ lives (project root, not the clone) // projectCwd is where .takt/ lives (project root, not the clone)
@ -164,8 +165,22 @@ export async function executeWorkflow(
} }
}; };
const onUserInput = interactiveUserInput
? async (request: UserInputRequest): Promise<string | null> => {
if (displayRef.current) {
displayRef.current.flush();
displayRef.current = null;
}
blankLine();
info(request.prompt.trim());
const input = await promptInput('追加の指示を入力してください(空で中止)');
return input && input.trim() ? input.trim() : null;
}
: undefined;
const engine = new WorkflowEngine(workflowConfig, cwd, task, { const engine = new WorkflowEngine(workflowConfig, cwd, task, {
onStream: streamHandler, onStream: streamHandler,
onUserInput,
initialSessions: savedSessions, initialSessions: savedSessions,
onSessionUpdate: sessionUpdateHandler, onSessionUpdate: sessionUpdateHandler,
onIterationLimit: iterationLimitHandler, onIterationLimit: iterationLimitHandler,
@ -173,6 +188,9 @@ export async function executeWorkflow(
language: options.language, language: options.language,
provider: options.provider, provider: options.provider,
model: options.model, model: options.model,
interactive: interactiveUserInput,
detectRuleIndex,
callAiJudge,
}); });
let abortReason: string | undefined; let abortReason: string | undefined;
@ -288,6 +306,7 @@ export async function executeWorkflow(
}); });
engine.on('workflow:abort', (state, reason) => { engine.on('workflow:abort', (state, reason) => {
interruptAllQueries();
log.error('Workflow aborted', { reason, iterations: state.iteration }); log.error('Workflow aborted', { reason, iterations: state.iteration });
if (displayRef.current) { if (displayRef.current) {
displayRef.current.flush(); displayRef.current.flush();

View File

@ -9,10 +9,10 @@ import {
detectDefaultBranch, detectDefaultBranch,
listTaktBranches, listTaktBranches,
buildListItems, buildListItems,
} from '../../../infra/task/branchList.js'; } from '../../../infra/task/index.js';
import { selectOption, confirm } from '../../../prompt/index.js'; import { selectOption, confirm } from '../../../shared/prompt/index.js';
import { info } from '../../../shared/ui/index.js'; import { info } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger } from '../../../shared/utils/index.js';
import type { TaskExecutionOptions } from '../execute/types.js'; import type { TaskExecutionOptions } from '../execute/types.js';
import { import {
type ListAction, type ListAction,

View File

@ -12,21 +12,19 @@ import {
removeClone, removeClone,
removeCloneMeta, removeCloneMeta,
cleanupOrphanedClone, cleanupOrphanedClone,
} from '../../../infra/task/clone.js'; } from '../../../infra/task/index.js';
import { import {
detectDefaultBranch, detectDefaultBranch,
type BranchListItem, type BranchListItem,
} from '../../../infra/task/branchList.js'; autoCommitAndPush,
import { autoCommitAndPush } from '../../../infra/task/autoCommit.js'; } from '../../../infra/task/index.js';
import { selectOption, promptInput } from '../../../prompt/index.js'; import { selectOption, promptInput } from '../../../shared/prompt/index.js';
import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js'; import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js';
import { createLogger } from '../../../shared/utils/debug.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
import { getErrorMessage } from '../../../shared/utils/error.js';
import { executeTask } from '../execute/taskExecution.js'; import { executeTask } from '../execute/taskExecution.js';
import type { TaskExecutionOptions } from '../execute/types.js'; import type { TaskExecutionOptions } from '../execute/types.js';
import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js'; import { listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js';
import { getCurrentWorkflow } from '../../../infra/config/paths.js'; import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js';
const log = createLogger('list-tasks'); const log = createLogger('list-tasks');

View File

@ -5,9 +5,8 @@
* Stays resident until Ctrl+C (SIGINT). * Stays resident until Ctrl+C (SIGINT).
*/ */
import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js'; import { TaskRunner, type TaskInfo, TaskWatcher } from '../../../infra/task/index.js';
import { TaskWatcher } from '../../../infra/task/watcher.js'; import { getCurrentWorkflow } from '../../../infra/config/index.js';
import { getCurrentWorkflow } from '../../../infra/config/paths.js';
import { import {
header, header,
info, info,
@ -16,7 +15,7 @@ import {
blankLine, blankLine,
} from '../../../shared/ui/index.js'; } from '../../../shared/ui/index.js';
import { executeAndCompleteTask } from '../execute/taskExecution.js'; import { executeAndCompleteTask } from '../execute/taskExecution.js';
import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js'; import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
import type { TaskExecutionOptions } from '../execute/types.js'; import type { TaskExecutionOptions } from '../execute/types.js';
/** /**

View File

@ -7,8 +7,39 @@
// Models // Models
export * from './core/models/index.js'; export * from './core/models/index.js';
// Configuration // Configuration (PermissionMode excluded to avoid name conflict with core/models PermissionMode)
export * from './infra/config/index.js'; export * from './infra/config/paths.js';
export * from './infra/config/loaders/index.js';
export * from './infra/config/global/index.js';
export {
loadProjectConfig,
saveProjectConfig,
updateProjectConfig,
getCurrentWorkflow,
setCurrentWorkflow,
isVerboseMode,
type ProjectPermissionMode,
type ProjectLocalConfig,
writeFileAtomic,
getInputHistoryPath,
MAX_INPUT_HISTORY,
loadInputHistory,
saveInputHistory,
addToInputHistory,
type AgentSessionData,
getAgentSessionsPath,
loadAgentSessions,
saveAgentSessions,
updateAgentSession,
clearAgentSessions,
getWorktreeSessionsDir,
encodeWorktreePath,
getWorktreeSessionPath,
loadWorktreeSessions,
updateWorktreeSession,
getClaudeProjectSessionsDir,
clearClaudeProjectSessions,
} from './infra/config/project/index.js';
// Claude integration // Claude integration
export { export {
@ -40,7 +71,7 @@ export {
detectJudgeIndex, detectJudgeIndex,
buildJudgePrompt, buildJudgePrompt,
isRegexSafe, isRegexSafe,
} from './claude/index.js'; } from './infra/claude/index.js';
export type { export type {
StreamEvent, StreamEvent,
StreamCallback, StreamCallback,
@ -60,10 +91,10 @@ export type {
ThinkingEventData, ThinkingEventData,
ResultEventData, ResultEventData,
ErrorEventData, ErrorEventData,
} from './claude/index.js'; } from './infra/claude/index.js';
// Codex integration // Codex integration
export * from './codex/index.js'; export * from './infra/codex/index.js';
// Agent execution // Agent execution
export * from './agents/index.js'; export * from './agents/index.js';
@ -117,6 +148,10 @@ export type {
// Utilities // Utilities
export * from './shared/utils/index.js'; export * from './shared/utils/index.js';
export * from './shared/ui/index.js'; export * from './shared/ui/index.js';
export * from './shared/prompt/index.js';
export * from './shared/constants.js';
export * from './shared/context.js';
export * from './shared/exitCodes.js';
// Resources (embedded prompts and templates) // Resources (embedded prompts and templates)
export * from './resources/index.js'; export * from './infra/resources/index.js';

View File

@ -6,8 +6,8 @@
import { executeClaudeCli } from './process.js'; import { executeClaudeCli } from './process.js';
import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js'; import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js';
import type { AgentResponse, Status } from '../core/models/index.js'; import type { AgentResponse, Status } from '../../core/models/index.js';
import { createLogger } from '../shared/utils/debug.js'; import { createLogger } from '../../shared/utils/index.js';
// Re-export for backward compatibility // Re-export for backward compatibility
export type { ClaudeCallOptions } from './types.js'; export type { ClaudeCallOptions } from './types.js';

View File

@ -11,8 +11,7 @@ import {
type SDKResultMessage, type SDKResultMessage,
type SDKAssistantMessage, type SDKAssistantMessage,
} from '@anthropic-ai/claude-agent-sdk'; } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '../shared/utils/debug.js'; import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
import { getErrorMessage } from '../shared/utils/error.js';
import { import {
generateQueryId, generateQueryId,
registerQuery, registerQuery,

View File

@ -16,7 +16,7 @@ import type {
PreToolUseHookInput, PreToolUseHookInput,
PermissionMode, PermissionMode,
} from '@anthropic-ai/claude-agent-sdk'; } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '../shared/utils/debug.js'; import { createLogger } from '../../shared/utils/index.js';
import type { import type {
PermissionHandler, PermissionHandler,
AskUserQuestionInput, AskUserQuestionInput,

View File

@ -5,8 +5,9 @@
* used throughout the Claude integration layer. * used throughout the Claude integration layer.
*/ */
import type { PermissionResult, PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk'; import type { PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk';
import type { PermissionMode } from '../core/models/index.js'; import type { PermissionMode } from '../../core/models/index.js';
import type { PermissionResult } from '../../core/workflow/index.js';
// Re-export PermissionResult for convenience // Re-export PermissionResult for convenience
export type { PermissionResult, PermissionUpdate }; export type { PermissionResult, PermissionUpdate };

View File

@ -5,7 +5,7 @@
* used throughout the takt codebase. * used throughout the takt codebase.
*/ */
import type { StreamCallback } from '../claude/types.js'; import type { StreamCallback } from '../claude/index.js';
export type CodexEvent = { export type CodexEvent = {
type: string; type: string;

View File

@ -5,9 +5,8 @@
*/ */
import { Codex } from '@openai/codex-sdk'; import { Codex } from '@openai/codex-sdk';
import type { AgentResponse } from '../core/models/index.js'; import type { AgentResponse } from '../../core/models/index.js';
import { createLogger } from '../shared/utils/debug.js'; import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
import { getErrorMessage } from '../shared/utils/error.js';
import type { CodexCallOptions } from './types.js'; import type { CodexCallOptions } from './types.js';
import { import {
type CodexEvent, type CodexEvent,

Some files were not shown because too many files have changed in this diff Show More