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.
**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
- **Be specific** — Specify file names, function names, and change details
- **Ask when uncertain** — Do not proceed with ambiguity
- **Ask all questions at once** — Avoid multiple rounds of follow-up questions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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(() => {
vi.clearAllMocks();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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