From b944349d8f886f41ec4d06145dde57a2282664ec Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Mon, 2 Feb 2026 21:52:40 +0900 Subject: [PATCH] refacotr --- docs/vertical-slice-migration-plan.md | 361 ++++++++++++++++++ report/plan.md | 58 +++ resources/global/en/agents/default/planner.md | 1 + .../global/en/agents/templates/planner.md | 1 + .../global/en/prompts/interactive-summary.md | 3 +- resources/global/en/workflows/default.yaml | 20 +- .../global/en/workflows/expert-cqrs.yaml | 31 +- resources/global/en/workflows/expert.yaml | 31 +- resources/global/en/workflows/simple.yaml | 9 +- resources/global/ja/agents/default/planner.md | 1 + .../global/ja/agents/templates/planner.md | 1 + .../global/ja/prompts/interactive-summary.md | 3 +- resources/global/ja/workflows/default.yaml | 21 +- .../global/ja/workflows/expert-cqrs.yaml | 32 +- resources/global/ja/workflows/expert.yaml | 32 +- resources/global/ja/workflows/simple.yaml | 10 +- src/__tests__/addTask.test.ts | 7 +- src/__tests__/ai-judge.test.ts | 2 +- src/__tests__/cli-worktree.test.ts | 14 +- src/__tests__/client.test.ts | 2 +- src/__tests__/clone.test.ts | 3 +- src/__tests__/config.test.ts | 8 +- src/__tests__/debug.test.ts | 2 +- src/__tests__/engine-abort.test.ts | 24 +- src/__tests__/engine-agent-overrides.test.ts | 3 +- src/__tests__/engine-blocked.test.ts | 3 +- src/__tests__/engine-error.test.ts | 3 +- src/__tests__/engine-happy-path.test.ts | 3 +- src/__tests__/engine-parallel.test.ts | 3 +- src/__tests__/engine-test-helpers.ts | 2 +- src/__tests__/engine-worktree-report.test.ts | 13 +- src/__tests__/exitCodes.test.ts | 2 +- .../initialization-noninteractive.test.ts | 2 +- src/__tests__/initialization.test.ts | 4 +- src/__tests__/instructionBuilder.test.ts | 14 + src/__tests__/interactive.test.ts | 13 +- src/__tests__/it-error-recovery.test.ts | 34 +- src/__tests__/it-mock-scenario.test.ts | 2 +- src/__tests__/it-pipeline-modes.test.ts | 18 +- src/__tests__/it-pipeline.test.ts | 16 +- src/__tests__/it-rule-evaluation.test.ts | 11 +- .../it-three-phase-execution.test.ts | 30 +- src/__tests__/it-workflow-execution.test.ts | 30 +- src/__tests__/it-workflow-loader.test.ts | 2 +- src/__tests__/it-workflow-patterns.test.ts | 14 +- src/__tests__/pipelineExecution.test.ts | 3 +- src/__tests__/prompt.test.ts | 12 +- src/__tests__/summarize.test.ts | 3 +- src/__tests__/taskExecution.test.ts | 22 +- src/__tests__/updateNotifier.test.ts | 2 +- .../workflow-expert-parallel.test.ts | 2 +- src/agents/runner.ts | 8 +- src/agents/types.ts | 2 +- src/app/cli/commands.ts | 2 +- src/app/cli/helpers.ts | 2 +- src/app/cli/index.ts | 2 +- src/app/cli/program.ts | 4 +- src/app/cli/routing.ts | 7 +- src/core/models/schemas.ts | 6 +- src/core/models/workflow-types.ts | 4 + src/core/workflow/engine/OptionsBuilder.ts | 3 + src/core/workflow/engine/ParallelRunner.ts | 17 +- src/core/workflow/engine/StepExecutor.ts | 15 +- src/core/workflow/engine/WorkflowEngine.ts | 72 +++- .../workflow/evaluation/AggregateEvaluator.ts | 2 +- src/core/workflow/evaluation/RuleEvaluator.ts | 41 +- src/core/workflow/index.ts | 1 + .../instruction/InstructionBuilder.ts | 7 +- .../instruction/ReportInstructionBuilder.ts | 10 +- .../instruction/StatusJudgmentBuilder.ts | 9 +- .../instruction/instruction-context.ts | 2 + src/core/workflow/instruction/status-rules.ts | 19 +- src/core/workflow/phase-runner.ts | 17 +- src/core/workflow/types.ts | 25 +- src/features/config/ejectBuiltin.ts | 9 +- src/features/config/switchConfig.ts | 8 +- src/features/config/switchWorkflow.ts | 5 +- src/features/interactive/interactive.ts | 20 +- src/features/pipeline/execute.ts | 21 +- src/features/tasks/add/index.ts | 14 +- .../tasks/execute/selectAndExecute.ts | 16 +- src/features/tasks/execute/session.ts | 3 +- src/features/tasks/execute/taskExecution.ts | 13 +- src/features/tasks/execute/types.ts | 6 + .../tasks/execute/workflowExecution.ts | 37 +- src/features/tasks/list/index.ts | 6 +- src/features/tasks/list/taskActions.ts | 16 +- src/features/tasks/watch/index.ts | 7 +- src/index.ts | 47 ++- src/{ => infra}/claude/client.ts | 4 +- src/{ => infra}/claude/executor.ts | 3 +- src/{ => infra}/claude/index.ts | 0 src/{ => infra}/claude/options-builder.ts | 2 +- src/{ => infra}/claude/process.ts | 0 src/{ => infra}/claude/query-manager.ts | 0 src/{ => infra}/claude/stream-converter.ts | 0 src/{ => infra}/claude/types.ts | 5 +- src/{ => infra}/codex/CodexStreamHandler.ts | 2 +- src/{ => infra}/codex/client.ts | 5 +- src/{ => infra}/codex/index.ts | 0 src/{ => infra}/codex/types.ts | 2 +- src/infra/config/global/globalConfig.ts | 2 +- src/infra/config/global/initialization.ts | 6 +- src/infra/config/index.ts | 5 +- src/infra/config/loaders/workflowParser.ts | 14 +- src/infra/config/loaders/workflowResolver.ts | 3 +- src/infra/config/paths.ts | 2 +- src/infra/config/project/projectConfig.ts | 2 +- src/infra/fs/index.ts | 28 ++ src/infra/fs/session.ts | 8 +- src/infra/github/index.ts | 16 + src/infra/github/issue.ts | 2 +- src/infra/github/pr.ts | 3 +- src/{ => infra}/mock/client.ts | 4 +- src/infra/mock/index.ts | 13 + src/{ => infra}/mock/scenario.ts | 0 src/{ => infra}/mock/types.ts | 2 +- src/infra/providers/claude.ts | 4 +- src/infra/providers/codex.ts | 4 +- src/infra/providers/mock.ts | 3 +- src/infra/providers/types.ts | 2 +- src/{ => infra}/resources/index.ts | 6 +- src/infra/task/autoCommit.ts | 3 +- src/infra/task/branchList.ts | 2 +- src/infra/task/clone.ts | 3 +- src/infra/task/index.ts | 1 + src/infra/task/summarize.ts | 2 +- src/infra/task/watcher.ts | 2 +- src/{ => shared}/constants.ts | 3 +- src/{ => shared}/context.ts | 0 src/{ => shared}/exitCodes.ts | 0 src/{ => shared}/prompt/confirm.ts | 0 src/{ => shared}/prompt/index.ts | 0 src/{ => shared}/prompt/select.ts | 2 +- src/shared/ui/StreamDisplay.ts | 6 +- src/shared/utils/debug.ts | 6 +- src/shared/utils/index.ts | 1 + 137 files changed, 1270 insertions(+), 361 deletions(-) create mode 100644 docs/vertical-slice-migration-plan.md create mode 100644 report/plan.md rename src/{ => infra}/claude/client.ts (98%) rename src/{ => infra}/claude/executor.ts (97%) rename src/{ => infra}/claude/index.ts (100%) rename src/{ => infra}/claude/options-builder.ts (98%) rename src/{ => infra}/claude/process.ts (100%) rename src/{ => infra}/claude/query-manager.ts (100%) rename src/{ => infra}/claude/stream-converter.ts (100%) rename src/{ => infra}/claude/types.ts (94%) rename src/{ => infra}/codex/CodexStreamHandler.ts (99%) rename src/{ => infra}/codex/client.ts (97%) rename src/{ => infra}/codex/index.ts (100%) rename src/{ => infra}/codex/types.ts (86%) create mode 100644 src/infra/fs/index.ts create mode 100644 src/infra/github/index.ts rename src/{ => infra}/mock/client.ts (94%) create mode 100644 src/infra/mock/index.ts rename src/{ => infra}/mock/scenario.ts (100%) rename src/{ => infra}/mock/types.ts (92%) rename src/{ => infra}/resources/index.ts (93%) rename src/{ => shared}/constants.ts (64%) rename src/{ => shared}/context.ts (100%) rename src/{ => shared}/exitCodes.ts (100%) rename src/{ => shared}/prompt/confirm.ts (100%) rename src/{ => shared}/prompt/index.ts (100%) rename src/{ => shared}/prompt/select.ts (99%) diff --git a/docs/vertical-slice-migration-plan.md b/docs/vertical-slice-migration-plan.md new file mode 100644 index 0000000..154a49a --- /dev/null +++ b/docs/vertical-slice-migration-plan.md @@ -0,0 +1,361 @@ +# Vertical Slice + Core ハイブリッド構成 移行計画(構造移動と import 整理) + +## 目的 +- `docs/vertical-slice-migration-map.md` に従い、機能追加なしで構造移動と import 整理を行うための作業計画を定義する。 +- 既存の `src/` 構成と依存関係を把握し、移行対象・参照元・Public API の整理ポイントを明確化する。 + +## 前提 +- 変更対象は `src/` と `docs/` の参照更新のみ。 +- 実装は行わず、移行の具体手順と注意点を記載する。 + +--- + +## 現状の `src/` 構成(トップレベル) +- `src/app/cli/*` +- `src/features/*` +- `src/core/*` +- `src/infra/*` +- `src/shared/*` +- その他: `src/agents/*`, `src/index.ts` + +## 現状の依存関係(観測ベース) +- `app` -> `features`, `infra`, `shared` +- `features` -> `core`, `infra`, `shared`(`shared/prompt`, `shared/constants`, `shared/context`, `shared/exitCodes`) +- `infra` -> `core/models`, `shared` +- `core` -> `shared`, `agents` +- `agents` -> `infra`, `shared`, `core/models`, `infra/claude` + +### 依存ルールとの差分(注意点) +- `core` が `shared` と `agents` に依存している(`core` は外側に依存しない想定)。 +- `agents` が `infra` に依存しているため、`core -> agents -> infra` の依存経路が発生している。 + +--- + +## 移動対象の分類と移動先(確定) + +### core +| 現在のパス | 移動先 | 備考 | +|---|---|---| +| `src/core/models/*` | `src/core/models/*` | 既に配置済み | +| `src/core/workflow/*` | `src/core/workflow/*` | 既に配置済み | + +### infra +| 現在のパス | 移動先 | 備考 | +|---|---|---| +| `src/infra/providers/*` | `src/infra/providers/*` | 既に配置済み | +| `src/infra/github/*` | `src/infra/github/*` | 既に配置済み | +| `src/infra/config/*` | `src/infra/config/*` | 既に配置済み | +| `src/infra/task/*` | `src/infra/task/*` | 既に配置済み | +| `src/infra/fs/session.ts` | `src/infra/fs/session.ts` | 既に配置済み | +| `src/infra/claude/*` | `src/infra/claude/*` | 外部API連携のため infra に集約 | +| `src/infra/codex/*` | `src/infra/codex/*` | 外部API連携のため infra に集約 | +| `src/infra/mock/*` | `src/infra/mock/*` | Provider 用 mock のため infra に集約 | +| `src/infra/resources/*` | `src/infra/resources/*` | FS 依存を含むため infra に集約 | + +### features +| 現在のパス | 移動先 | 備考 | +|---|---|---| +| `src/features/tasks/*` | `src/features/tasks/*` | 既に配置済み | +| `src/features/pipeline/*` | `src/features/pipeline/*` | 既に配置済み | +| `src/features/config/*` | `src/features/config/*` | 既に配置済み | +| `src/features/interactive/*` | `src/features/interactive/*` | 既に配置済み | + +### app +| 現在のパス | 移動先 | 備考 | +|---|---|---| +| `src/app/cli/*` | `src/app/cli/*` | 既に配置済み | + +### shared +| 現在のパス | 移動先 | 備考 | +|---|---|---| +| `src/shared/utils/*` | `src/shared/utils/*` | 既に配置済み | +| `src/shared/ui/*` | `src/shared/ui/*` | 既に配置済み | +| `src/shared/prompt/*` | `src/shared/prompt/*` | 共有UIユーティリティとして集約 | +| `src/shared/constants.ts` | `src/shared/constants.ts` | 共有定数として集約 | +| `src/shared/context.ts` | `src/shared/context.ts` | 共有コンテキストとして集約 | +| `src/shared/exitCodes.ts` | `src/shared/exitCodes.ts` | 共有エラーコードとして集約 | + +--- + +## 移行対象と参照元の洗い出し(現状) + +### core/models の参照元 +- `src/infra/claude/client.ts` +- `src/infra/codex/client.ts` +- `src/agents/runner.ts` +- `src/agents/types.ts` +- `src/infra/config/global/globalConfig.ts` +- `src/infra/config/global/initialization.ts` +- `src/infra/config/loaders/agentLoader.ts` +- `src/infra/config/loaders/workflowParser.ts` +- `src/infra/config/loaders/workflowResolver.ts` +- `src/infra/config/paths.ts` +- `src/infra/providers/*` +- `src/features/interactive/interactive.ts` +- `src/features/tasks/execute/*` +- `src/features/pipeline/execute.ts` +- `src/shared/utils/debug.ts` +- `src/shared/constants.ts` +- `src/infra/resources/index.ts` +- `src/__tests__/*` + +### core/workflow の参照元 +- `src/features/tasks/execute/workflowExecution.ts` +- `src/__tests__/engine-*.test.ts` +- `src/__tests__/instructionBuilder.test.ts` +- `src/__tests__/it-*.test.ts` +- `src/__tests__/parallel-logger.test.ts` +- `src/__tests__/transitions.test.ts` + +### infra/config の参照元 +- `src/app/cli/*` +- `src/agents/runner.ts` +- `src/features/interactive/interactive.ts` +- `src/features/config/*` +- `src/features/tasks/execute/*` +- `src/features/tasks/add/index.ts` +- `src/features/tasks/list/taskActions.ts` +- `src/__tests__/*` + +### infra/task の参照元 +- `src/features/tasks/*` +- `src/features/pipeline/execute.ts` +- `src/__tests__/*` + +### infra/github の参照元 +- `src/app/cli/routing.ts` +- `src/features/pipeline/execute.ts` +- `src/features/tasks/execute/selectAndExecute.ts` +- `src/features/tasks/add/index.ts` +- `src/__tests__/github-*.test.ts` + +### infra/providers の参照元 +- `src/agents/runner.ts` +- `src/features/interactive/interactive.ts` +- `src/features/tasks/add/index.ts` +- `src/features/tasks/execute/types.ts` +- `src/__tests__/addTask.test.ts` +- `src/__tests__/interactive.test.ts` +- `src/__tests__/summarize.test.ts` + +### infra/fs の参照元 +- `src/features/tasks/execute/workflowExecution.ts` +- `src/__tests__/session.test.ts` +- `src/__tests__/utils.test.ts` + +### shared/utils の参照元 +- `src/app/cli/*` +- `src/infra/*` +- `src/features/*` +- `src/agents/runner.ts` +- `src/infra/claude/*` +- `src/infra/codex/*` +- `src/core/workflow/*` +- `src/__tests__/*` + +### shared/ui の参照元 +- `src/app/cli/*` +- `src/infra/task/display.ts` +- `src/features/*` +- `src/__tests__/*` + +### shared/prompt の参照元 +- `src/features/*` +- `src/infra/config/global/initialization.ts` +- `src/__tests__/*` + +### shared/constants・shared/context・shared/exitCodes の参照元 +- `src/features/*` +- `src/infra/config/global/*` +- `src/core/models/schemas.ts` +- `src/core/workflow/engine/*` +- `src/app/cli/routing.ts` +- `src/__tests__/*` + +--- + +## Public API(index.ts)整理ポイント + +### 既存 Public API +- `src/core/models/index.ts` +- `src/core/workflow/index.ts` +- `src/features/tasks/index.ts` +- `src/features/pipeline/index.ts` +- `src/features/config/index.ts` +- `src/features/interactive/index.ts` +- `src/infra/config/index.ts` +- `src/infra/task/index.ts` +- `src/infra/providers/index.ts` +- `src/shared/utils/index.ts` +- `src/shared/ui/index.ts` +- `src/index.ts` + +### 新設/拡張が必要な Public API +- `src/infra/github/index.ts`(`issue.ts`, `pr.ts`, `types.ts` の集約) +- `src/infra/fs/index.ts`(`session.ts` の集約) +- `src/infra/resources/index.ts`(resources API の集約) +- `src/infra/config/index.ts` の拡張(`globalConfig`, `projectConfig`, `workflowLoader` などの再エクスポート) +- `src/shared/prompt/index.ts`(共通プロンプトの入口) +- `src/shared/constants.ts`, `src/shared/context.ts`, `src/shared/exitCodes.ts` の Public API 反映 +- `src/infra/claude/index.ts`, `src/infra/codex/index.ts`, `src/infra/mock/index.ts`(移動後の入口) + +### 深い import 禁止の置換方針 +- `core/*` と `features/*` は Public API(`index.ts`)からのみ import。 +- `features` から `infra` の deep import を廃止し、`infra/*/index.ts` 経由に置換。 +- `app/cli` から `infra` への direct import は必要最小限に限定し、可能なら `features` Public API に集約。 + +--- + +## 影響範囲一覧 + +### CLI エントリ +- `src/app/cli/index.ts` +- `src/app/cli/program.ts` +- `src/app/cli/commands.ts` +- `src/app/cli/routing.ts` +- `src/app/cli/helpers.ts` +- `bin/takt` + +### features 呼び出し +- `src/features/tasks/*` +- `src/features/pipeline/*` +- `src/features/config/*` +- `src/features/interactive/*` + +### docs 参照更新対象 +- `docs/data-flow.md` +- `docs/data-flow-diagrams.md` +- `docs/agents.md` +- `docs/workflows.md` +- `docs/README.ja.md` + +### テスト +- `src/__tests__/*` + +--- + +## 実施手順(推奨順序) + +### 1. core +- `core/workflow` と `core/models` の Public API を点検し、外部参照を `index.ts` 経由に統一。 +- `core` 内での `shared` 依存を整理する(ログ/エラー/レポート生成の配置を明確化)。 +- `agents` 依存の扱いを決定し、依存方向を破らない構成に合わせて移動計画を確定する。 + +### 2. infra +- `infra/github` と `infra/fs` の `index.ts` を新設し、deep import を解消する前提の API を定義。 +- `infra/config/index.ts` の再エクスポート対象を拡張し、`globalConfig`・`projectConfig`・`workflowLoader` 等を Public API 化。 +- `claude/codex/mock/resources` を `infra` 配下に移動し、参照を更新する。 + +### 3. features +- `features` から `infra` への deep import を Public API 経由に置換。 +- `prompt` の移動に合わせ、`features` 内の import を `shared/prompt` に変更。 +- `constants/context/exitCodes` の移動に合わせて参照を更新。 + +### 4. app +- `app/cli` から `features` Public API のみを使用する形に整理。 +- `app/cli` から `infra` へ直接参照している箇所は、必要に応じて `features` 経由に寄せる。 + +### 5. Public API +- `src/index.ts` の再エクスポート対象を新パスに合わせて更新。 +- 新設した `index.ts` のエクスポート整合を確認する。 + +### 6. docs +- `docs/data-flow.md` など、`src/` 参照を新パスに合わせて更新。 +- 参照パスが `index.ts` の Public API 方針に沿っているか点検。 + +--- + +## 判断ポイント +- `src/models/workflow.ts` が追加される場合、 + - **廃止**するか、 + - **`core/models/index.ts` へ統合**するかを決める。 + +--- + +## 再開指示(2026-02-02 時点の差分観測ベース) + +### 現在のブランチ +- `refactoring` + +### 進捗(差分ベースの整理) +#### core +- `src/core/models/*` と `src/core/workflow/*` が広範囲に変更されている。 + +#### infra +- 既存: `src/infra/config/*`, `src/infra/providers/*`, `src/infra/task/*`, `src/infra/github/*`, `src/infra/fs/session.ts` が更新されている。 +- 追加: `src/infra/claude/*`, `src/infra/codex/*`, `src/infra/mock/*`, `src/infra/resources/*` が新規追加されている。 +- 追加: `src/infra/github/index.ts`, `src/infra/fs/index.ts` が新規追加されている。 + +#### features +- `src/features/*` が広範囲に変更されている。 + +#### app +- `src/app/cli/*` が変更されている。 + +#### shared +- `src/shared/utils/index.ts` と `src/shared/ui/StreamDisplay.ts` が更新されている。 +- `src/shared/prompt/*`, `src/shared/constants.ts`, `src/shared/context.ts`, `src/shared/exitCodes.ts` が新規追加されている。 + +#### 削除された旧パス +- `src/claude/*`, `src/codex/*`, `src/mock/*`, `src/prompt/*`, `src/resources/index.ts` +- `src/constants.ts`, `src/context.ts`, `src/exitCodes.ts` + +#### tests +- `src/__tests__/*` が広範囲に更新されている。 + +#### resources +- `resources/global/{en,ja}/*` に更新があるため、移行作業とは独立して取り扱う。 + +#### docs +- `docs/vertical-slice-migration-plan.md` が未追跡ファイルとして存在する。 + +--- + +## 未完了セクション(要確認事項) +以下は差分観測のみでは断定できないため、再開時に確認する。 + +### core +- `core` から外部層(`shared` / `agents`)への依存が残っていないか確認する。 +- `core/models` と `core/workflow` の Public API が `index.ts` 経由に統一されているか点検する。 + +### infra +- `infra/github/index.ts`, `infra/fs/index.ts`, `infra/resources/index.ts` の再エクスポート範囲を確定する。 +- `infra/config/index.ts` の再エクスポート対象(`globalConfig`, `projectConfig`, `workflowLoader` 等)が揃っているか確認する。 +- `infra/claude`, `infra/codex`, `infra/mock` の Public API が `index.ts` に統一されているか確認する。 + +### features +- `features` から `infra` への deep import が残っていないか確認する。 +- `shared/prompt`, `shared/constants`, `shared/context`, `shared/exitCodes` への参照統一が完了しているか確認する。 + +### app +- `app/cli` が `features` Public API 経由に統一されているか確認する。 +- `app/cli` から `infra` への direct import が残っていないか確認する。 + +### Public API +- `src/index.ts` の再エクスポートが新パスに揃っているか確認する。 +- `infra`/`shared`/`features` の `index.ts` 追加分を反映できているか点検する。 + +### docs +- `docs/*` の参照パスを新構成(Public API)へ更新する。 + +--- + +## 判断ポイント(再掲) +- `src/models/workflow.ts` は直近コミットで削除されているため、 + - 廃止のまま進めるか、 + - `core/models/index.ts` へ統合して復活させるかを確定する。 + +--- + +## 参照更新の対象一覧(docs) +- `docs/data-flow.md` +- `docs/data-flow-diagrams.md` +- `docs/agents.md` +- `docs/workflows.md` +- `docs/README.ja.md` + +--- + +## 付記 +- ここに記載した移動は、既存の機能追加なしで行うこと。 +- 実装時は `core -> infra -> features -> app -> Public API -> docs` の順序を厳守する。 diff --git a/report/plan.md b/report/plan.md new file mode 100644 index 0000000..9e4950a --- /dev/null +++ b/report/plan.md @@ -0,0 +1,58 @@ +# 実装計画: Vertical Slice + Core ハイブリッド構成移行の検証・修正 + +## 現状分析 + +### ビルド・テスト状況 +- **ビルド (`npm run build`)**: ✅ パス(エラーなし) +- **テスト (`npm test`)**: ✅ 全54ファイル、802テストパス + +### レビュー結果サマリ + +| 観点 | 結果 | 詳細 | +|------|------|------| +| 依存方向の違反 | ✅ 違反なし | core→infra/features/app なし、features→app なし、infra→features/app なし | +| 旧パスの残留 | ✅ 残留なし | src/claude/, src/codex/, src/mock/, src/prompt/, src/resources/, src/constants.ts, src/context.ts, src/exitCodes.ts への参照なし | +| docs参照パスの整合 | ✅ 問題なし | 5ファイルすべて新構成のパスに更新済み | +| Public API (index.ts) 経由の統一 | ⚠️ 修正必要 | プロダクションコードに4箇所のdeep import違反あり | + +## 修正が必要な箇所 + +### Public API (index.ts) 経由の統一 — deep import 違反(プロダクションコード4箇所) + +#### 1. `src/infra/claude/types.ts` +- **現状**: `import type { PermissionResult } from '../../core/workflow/types.js'` +- **問題**: `core/workflow/index.ts` を経由せず直接 `types.js` を参照 +- **対応**: `PermissionResult` を `core/workflow/index.ts` からエクスポートし、import パスを `../../core/workflow/index.js` に変更 + +#### 2. `src/shared/ui/StreamDisplay.ts` +- **現状**: `import type { StreamEvent, StreamCallback } from '../../core/workflow/types.js'` +- **問題**: `core/workflow/index.ts` を経由せず直接 `types.js` を参照(これらの型は既にindex.tsでエクスポート済み) +- **対応**: import パスを `../../core/workflow/index.js` に変更 + +#### 3. `src/features/config/switchConfig.ts`(2箇所) +- **現状**: `import type { PermissionMode } from '../../infra/config/types.js'` および `export type { PermissionMode } from '../../infra/config/types.js'` +- **問題**: `infra/config/index.ts` を経由せず直接 `types.js` を参照 +- **対応**: `PermissionMode` を `infra/config/index.ts` からエクスポートし、import/export パスを `../../infra/config/index.js` に変更 + +### テストコードの deep import(33箇所) +- テストコードはモジュール内部を直接テストする性質上、deep import は許容範囲 +- **今回は修正対象外とする**(機能追加しない制約に基づき、テストの構造変更は行わない) + +## 実装手順 + +### Step 1: core/workflow の Public API 修正 +1. `src/core/workflow/index.ts` を確認し、`PermissionResult` をエクスポートに追加 +2. `src/infra/claude/types.ts` の import パスを `../../core/workflow/index.js` に変更 +3. `src/shared/ui/StreamDisplay.ts` の import パスを `../../core/workflow/index.js` に変更 + +### Step 2: infra/config の Public API 修正 +1. `src/infra/config/index.ts` を確認し、`PermissionMode` をエクスポートに追加 +2. `src/features/config/switchConfig.ts` の import/export パスを `../../infra/config/index.js` に変更 + +### Step 3: 最終確認 +1. `npm run build` でビルド確認 +2. `npm test` でテスト確認 + +## 制約の確認 +- ✅ 機能追加は行わない(import パスとエクスポートの整理のみ) +- ✅ 変更対象は `src/` のみ(`docs/` は変更不要) diff --git a/resources/global/en/agents/default/planner.md b/resources/global/en/agents/default/planner.md index 0ea1d8a..612c9f6 100644 --- a/resources/global/en/agents/default/planner.md +++ b/resources/global/en/agents/default/planner.md @@ -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. diff --git a/resources/global/en/agents/templates/planner.md b/resources/global/en/agents/templates/planner.md index 0e7a26b..32fc805 100644 --- a/resources/global/en/agents/templates/planner.md +++ b/resources/global/en/agents/templates/planner.md @@ -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 diff --git a/resources/global/en/prompts/interactive-summary.md b/resources/global/en/prompts/interactive-summary.md index 1156b75..31145cf 100644 --- a/resources/global/en/prompts/interactive-summary.md +++ b/resources/global/en/prompts/interactive-summary.md @@ -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. diff --git a/resources/global/en/workflows/default.yaml b/resources/global/en/workflows/default.yaml index 85bb0b1..3c8263f 100644 --- a/resources/global/en/workflows/default.yaml +++ b/resources/global/en/workflows/default.yaml @@ -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 diff --git a/resources/global/en/workflows/expert-cqrs.yaml b/resources/global/en/workflows/expert-cqrs.yaml index 2d918ff..bd280a6 100644 --- a/resources/global/en/workflows/expert-cqrs.yaml +++ b/resources/global/en/workflows/expert-cqrs.yaml @@ -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 diff --git a/resources/global/en/workflows/expert.yaml b/resources/global/en/workflows/expert.yaml index 6cbb78a..d036a9b 100644 --- a/resources/global/en/workflows/expert.yaml +++ b/resources/global/en/workflows/expert.yaml @@ -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 diff --git a/resources/global/en/workflows/simple.yaml b/resources/global/en/workflows/simple.yaml index 53c1c14..0eabd9c 100644 --- a/resources/global/en/workflows/simple.yaml +++ b/resources/global/en/workflows/simple.yaml @@ -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 diff --git a/resources/global/ja/agents/default/planner.md b/resources/global/ja/agents/default/planner.md index f156a16..196459e 100644 --- a/resources/global/ja/agents/default/planner.md +++ b/resources/global/ja/agents/default/planner.md @@ -73,3 +73,4 @@ **シンプルに分析する。** 過度に詳細な計画は不要。Coderが実装を進められる程度の方向性を示す。 **不明点は明確にする。** 推測で進めず、不明点を報告する。 +**確認が必要な場合は質問を一度にまとめる。** 追加の確認質問を繰り返さない。 diff --git a/resources/global/ja/agents/templates/planner.md b/resources/global/ja/agents/templates/planner.md index 5f23b40..6b6530e 100644 --- a/resources/global/ja/agents/templates/planner.md +++ b/resources/global/ja/agents/templates/planner.md @@ -42,3 +42,4 @@ - **推測で計画を立てない** — 必ずコードを読んで確認する - **計画は具体的に** — ファイル名、関数名、変更内容を明示する - **判断に迷ったら質問する** — 曖昧なまま進めない +- **質問は一度にまとめる** — 追加の確認質問を繰り返さない diff --git a/resources/global/ja/prompts/interactive-summary.md b/resources/global/ja/prompts/interactive-summary.md index a7b7e29..3ed31ce 100644 --- a/resources/global/ja/prompts/interactive-summary.md +++ b/resources/global/ja/prompts/interactive-summary.md @@ -3,5 +3,6 @@ 要件: - 出力は最終的な指示のみ(前置き不要) - スコープや対象(ファイル/モジュール)が出ている場合は明確に書く -- 制約や「やらないこと」を保持する +- ユーザー由来の制約や「やらないこと」は保持する +- アシスタントの運用上の制約(実行禁止/ツール制限など)は指示に含めない - 情報不足があれば「Open Questions」セクションを短く付ける diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/workflows/default.yaml index 9d06ef6..4e36fce 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/workflows/default.yaml @@ -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 diff --git a/resources/global/ja/workflows/expert-cqrs.yaml b/resources/global/ja/workflows/expert-cqrs.yaml index 451eaf0..89ce72b 100644 --- a/resources/global/ja/workflows/expert-cqrs.yaml +++ b/resources/global/ja/workflows/expert-cqrs.yaml @@ -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: 監督者の指摘に対する修正が完了した diff --git a/resources/global/ja/workflows/expert.yaml b/resources/global/ja/workflows/expert.yaml index fc251bd..2fc19f2 100644 --- a/resources/global/ja/workflows/expert.yaml +++ b/resources/global/ja/workflows/expert.yaml @@ -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: 監督者の指摘に対する修正が完了した diff --git a/resources/global/ja/workflows/simple.yaml b/resources/global/ja/workflows/simple.yaml index 2324cd5..62871d6 100644 --- a/resources/global/ja/workflows/simple.yaml +++ b/resources/global/ja/workflows/simple.yaml @@ -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 diff --git a/src/__tests__/addTask.test.ts b/src/__tests__/addTask.test.ts index 26f2e48..7bbfacd 100644 --- a/src/__tests__/addTask.test.ts +++ b/src/__tests__/addTask.test.ts @@ -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>()), 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'; diff --git a/src/__tests__/ai-judge.test.ts b/src/__tests__/ai-judge.test.ts index d86e68b..19ed04b 100644 --- a/src/__tests__/ai-judge.test.ts +++ b/src/__tests__/ai-judge.test.ts @@ -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', () => { diff --git a/src/__tests__/cli-worktree.test.ts b/src/__tests__/cli-worktree.test.ts index ae5ca04..50064e6 100644 --- a/src/__tests__/cli-worktree.test.ts +++ b/src/__tests__/cli-worktree.test.ts @@ -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>()), 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(); +vi.mock('../shared/constants.js', async (importOriginal) => { + const actual = await importOriginal(); 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>()), 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'; diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 108c669..2844311 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -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', () => { diff --git a/src/__tests__/clone.test.ts b/src/__tests__/clone.test.ts index eb9d635..2853d45 100644 --- a/src/__tests__/clone.test.ts +++ b/src/__tests__/clone.test.ts @@ -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>()), createLogger: () => ({ info: vi.fn(), debug: vi.fn(), diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 6c34d2d..287842c 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -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', () => { diff --git a/src/__tests__/debug.test.ts b/src/__tests__/debug.test.ts index 3ca7281..81adcd8 100644 --- a/src/__tests__/debug.test.ts +++ b/src/__tests__/debug.test.ts @@ -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'; diff --git a/src/__tests__/engine-abort.test.ts b/src/__tests__/engine-abort.test.ts index 99d74d8..9c1a3a8 100644 --- a/src/__tests__/engine-abort.test.ts +++ b/src/__tests__/engine-abort.test.ts @@ -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>()), 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); }); }); diff --git a/src/__tests__/engine-agent-overrides.test.ts b/src/__tests__/engine-agent-overrides.test.ts index 0a76d75..9805540 100644 --- a/src/__tests__/engine-agent-overrides.test.ts +++ b/src/__tests__/engine-agent-overrides.test.ts @@ -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>()), generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); diff --git a/src/__tests__/engine-blocked.test.ts b/src/__tests__/engine-blocked.test.ts index 49ae0dc..2ce408c 100644 --- a/src/__tests__/engine-blocked.test.ts +++ b/src/__tests__/engine-blocked.test.ts @@ -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>()), generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); diff --git a/src/__tests__/engine-error.test.ts b/src/__tests__/engine-error.test.ts index 2c28aa5..a67618e 100644 --- a/src/__tests__/engine-error.test.ts +++ b/src/__tests__/engine-error.test.ts @@ -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>()), generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); diff --git a/src/__tests__/engine-happy-path.test.ts b/src/__tests__/engine-happy-path.test.ts index 2357a70..2a201ae 100644 --- a/src/__tests__/engine-happy-path.test.ts +++ b/src/__tests__/engine-happy-path.test.ts @@ -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>()), generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); diff --git a/src/__tests__/engine-parallel.test.ts b/src/__tests__/engine-parallel.test.ts index c9132fb..6db2b12 100644 --- a/src/__tests__/engine-parallel.test.ts +++ b/src/__tests__/engine-parallel.test.ts @@ -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>()), generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); diff --git a/src/__tests__/engine-test-helpers.ts b/src/__tests__/engine-test-helpers.ts index 55e148c..99f0711 100644 --- a/src/__tests__/engine-test-helpers.ts +++ b/src/__tests__/engine-test-helpers.ts @@ -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 --- diff --git a/src/__tests__/engine-worktree-report.test.ts b/src/__tests__/engine-worktree-report.test.ts index 2ddc982..f83cba1 100644 --- a/src/__tests__/engine-worktree-report.test.ts +++ b/src/__tests__/engine-worktree-report.test.ts @@ -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>()), 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 () => { diff --git a/src/__tests__/exitCodes.test.ts b/src/__tests__/exitCodes.test.ts index 394e687..7670463 100644 --- a/src/__tests__/exitCodes.test.ts +++ b/src/__tests__/exitCodes.test.ts @@ -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', () => { diff --git a/src/__tests__/initialization-noninteractive.test.ts b/src/__tests__/initialization-noninteractive.test.ts index fee6594..a1b0d78 100644 --- a/src/__tests__/initialization-noninteractive.test.ts +++ b/src/__tests__/initialization-noninteractive.test.ts @@ -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, })); diff --git a/src/__tests__/initialization.test.ts b/src/__tests__/initialization.test.ts index 6adff90..b2e4a25 100644 --- a/src/__tests__/initialization.test.ts +++ b/src/__tests__/initialization.test.ts @@ -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(() => { diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 9f65fa1..d31e484 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -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)', () => { diff --git a/src/__tests__/interactive.test.ts b/src/__tests__/interactive.test.ts index bac06a3..d44540e 100644 --- a/src/__tests__/interactive.test.ts +++ b/src/__tests__/interactive.test.ts @@ -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>()), 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>()), 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); diff --git a/src/__tests__/it-error-recovery.test.ts b/src/__tests__/it-error-recovery.test.ts index 420df51..4c9c376 100644 --- a/src/__tests__/it-error-recovery.test.ts +++ b/src/__tests__/it-error-recovery.test.ts @@ -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(); +vi.mock('../infra/claude/client.js', async (importOriginal) => { + const original = await importOriginal(); 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>()), generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateSessionId: vi.fn().mockReturnValue('test-session-id'), })); @@ -87,6 +89,14 @@ function createTestEnv(): { dir: string; agentPaths: Record } { return { dir, agentPaths }; } +function buildEngineOptions(projectCwd: string) { + return { + projectCwd, + detectRuleIndex, + callAiJudge, + }; +} + function buildWorkflow(agentPaths: Record, 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', }); diff --git a/src/__tests__/it-mock-scenario.test.ts b/src/__tests__/it-mock-scenario.test.ts index 6fbcde5..69f4e80 100644 --- a/src/__tests__/it-mock-scenario.test.ts +++ b/src/__tests__/it-mock-scenario.test.ts @@ -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', () => { diff --git a/src/__tests__/it-pipeline-modes.test.ts b/src/__tests__/it-pipeline-modes.test.ts index f93d48c..88be12f 100644 --- a/src/__tests__/it-pipeline-modes.test.ts +++ b/src/__tests__/it-pipeline-modes.test.ts @@ -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(); +vi.mock('../infra/claude/client.js', async (importOriginal) => { + const original = await importOriginal(); 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>()), notifySuccess: vi.fn(), notifyError: vi.fn(), })); -vi.mock('../shared/utils/reportDir.js', () => ({ +vi.mock('../shared/utils/index.js', async (importOriginal) => ({ + ...(await importOriginal>()), 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 --- diff --git a/src/__tests__/it-pipeline.test.ts b/src/__tests__/it-pipeline.test.ts index 6fd950b..13f2d08 100644 --- a/src/__tests__/it-pipeline.test.ts +++ b/src/__tests__/it-pipeline.test.ts @@ -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(); +vi.mock('../infra/claude/client.js', async (importOriginal) => { + const original = await importOriginal(); 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>()), notifySuccess: vi.fn(), notifyError: vi.fn(), })); -vi.mock('../shared/utils/reportDir.js', () => ({ +vi.mock('../shared/utils/index.js', async (importOriginal) => ({ + ...(await importOriginal>()), 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), })); diff --git a/src/__tests__/it-rule-evaluation.test.ts b/src/__tests__/it-rule-evaluation.test.ts index 75a248b..9b6cc8c 100644 --- a/src/__tests__/it-rule-evaluation.test.ts +++ b/src/__tests__/it-rule-evaluation.test.ts @@ -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(); - 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): RuleEvaluatorContext return { state: makeState(stepOutputs), cwd: '/tmp/test', + detectRuleIndex, + callAiJudge: mockCallAiJudge, }; } diff --git a/src/__tests__/it-three-phase-execution.test.ts b/src/__tests__/it-three-phase-execution.test.ts index 4d332b4..cfa6805 100644 --- a/src/__tests__/it-three-phase-execution.test.ts +++ b/src/__tests__/it-three-phase-execution.test.ts @@ -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(); +vi.mock('../infra/claude/client.js', async (importOriginal) => { + const original = await importOriginal(); 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>()), 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', }); diff --git a/src/__tests__/it-workflow-execution.test.ts b/src/__tests__/it-workflow-execution.test.ts index 6ca3a64..b682e02 100644 --- a/src/__tests__/it-workflow-execution.test.ts +++ b/src/__tests__/it-workflow-execution.test.ts @@ -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(); +vi.mock('../infra/claude/client.js', async (importOriginal) => { + const original = await importOriginal(); 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>()), generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateSessionId: vi.fn().mockReturnValue('test-session-id'), })); @@ -89,6 +91,14 @@ function createTestEnv(): { dir: string; agentPaths: Record } { return { dir, agentPaths }; } +function buildEngineOptions(projectCwd: string) { + return { + projectCwd, + detectRuleIndex, + callAiJudge, + }; +} + function buildSimpleWorkflow(agentPaths: Record): 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', }); diff --git a/src/__tests__/it-workflow-loader.test.ts b/src/__tests__/it-workflow-loader.test.ts index 33d6380..74041b9 100644 --- a/src/__tests__/it-workflow-loader.test.ts +++ b/src/__tests__/it-workflow-loader.test.ts @@ -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 --- diff --git a/src/__tests__/it-workflow-patterns.test.ts b/src/__tests__/it-workflow-patterns.test.ts index d7f462e..2b6cbc0 100644 --- a/src/__tests__/it-workflow-patterns.test.ts +++ b/src/__tests__/it-workflow-patterns.test.ts @@ -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(); +vi.mock('../infra/claude/client.js', async (importOriginal) => { + const original = await importOriginal(); 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>()), 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, }); } diff --git a/src/__tests__/pipelineExecution.test.ts b/src/__tests__/pipelineExecution.test.ts index e12f63b..cf74503 100644 --- a/src/__tests__/pipelineExecution.test.ts +++ b/src/__tests__/pipelineExecution.test.ts @@ -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>()), createLogger: () => ({ info: vi.fn(), debug: vi.fn(), diff --git a/src/__tests__/prompt.test.ts b/src/__tests__/prompt.test.ts index 7efaa77..e6abb59 100644 --- a/src/__tests__/prompt.test.ts +++ b/src/__tests__/prompt.test.ts @@ -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'); diff --git a/src/__tests__/summarize.test.ts b/src/__tests__/summarize.test.ts index 319891d..e4c1c76 100644 --- a/src/__tests__/summarize.test.ts +++ b/src/__tests__/summarize.test.ts @@ -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>()), createLogger: () => ({ info: vi.fn(), debug: vi.fn(), diff --git a/src/__tests__/taskExecution.test.ts b/src/__tests__/taskExecution.test.ts index b9ef1d7..bf3bd5b 100644 --- a/src/__tests__/taskExecution.test.ts +++ b/src/__tests__/taskExecution.test.ts @@ -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>()), TaskRunner: vi.fn(), })); -vi.mock('../infra/task/clone.js', () => ({ +vi.mock('../infra/task/clone.js', async (importOriginal) => ({ + ...(await importOriginal>()), createSharedClone: vi.fn(), removeClone: vi.fn(), })); -vi.mock('../infra/task/autoCommit.js', () => ({ +vi.mock('../infra/task/autoCommit.js', async (importOriginal) => ({ + ...(await importOriginal>()), autoCommitAndPush: vi.fn(), })); -vi.mock('../infra/task/summarize.js', () => ({ +vi.mock('../infra/task/summarize.js', async (importOriginal) => ({ + ...(await importOriginal>()), 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>()), 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', })); diff --git a/src/__tests__/updateNotifier.test.ts b/src/__tests__/updateNotifier.test.ts index 24f2846..8a80e99 100644 --- a/src/__tests__/updateNotifier.test.ts +++ b/src/__tests__/updateNotifier.test.ts @@ -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(); diff --git a/src/__tests__/workflow-expert-parallel.test.ts b/src/__tests__/workflow-expert-parallel.test.ts index 64e0245..67c7185 100644 --- a/src/__tests__/workflow-expert-parallel.test.ts +++ b/src/__tests__/workflow-expert-parallel.test.ts @@ -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()); diff --git a/src/agents/runner.ts b/src/agents/runner.ts index cfc38e8..b57bcb6 100644 --- a/src/agents/runner.ts +++ b/src/agents/runner.ts @@ -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 diff --git a/src/agents/types.ts b/src/agents/types.ts index ea2a3d3..4addd54 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -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 }; diff --git a/src/app/cli/commands.ts b/src/app/cli/commands.ts index 720222a..850675e 100644 --- a/src/app/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -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'; diff --git a/src/app/cli/helpers.ts b/src/app/cli/helpers.ts index 6be2454..6320276 100644 --- a/src/app/cli/helpers.ts +++ b/src/app/cli/helpers.ts @@ -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. diff --git a/src/app/cli/index.ts b/src/app/cli/index.ts index cf7a473..add83f0 100644 --- a/src/app/cli/index.ts +++ b/src/app/cli/index.ts @@ -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(); diff --git a/src/app/cli/program.ts b/src/app/cli/program.ts index 417b167..1821324 100644 --- a/src/app/cli/program.ts +++ b/src/app/cli/program.ts @@ -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 }; diff --git a/src/app/cli/routing.ts b/src/app/cli/routing.ts index d2b15fa..9d11a4b 100644 --- a/src/app/cli/routing.ts +++ b/src/app/cli/routing.ts @@ -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); }); diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index ee9ef83..d604d96 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -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) */ diff --git a/src/core/models/workflow-types.ts b/src/core/models/workflow-types.ts index dd8dbb9..32eda9c 100644 --- a/src/core/models/workflow-types.ts +++ b/src/core/models/workflow-types.ts @@ -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) */ diff --git a/src/core/workflow/engine/OptionsBuilder.ts b/src/core/workflow/engine/OptionsBuilder.ts index c2024da..26d2313 100644 --- a/src/core/workflow/engine/OptionsBuilder.ts +++ b/src/core/workflow/engine/OptionsBuilder.ts @@ -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, diff --git a/src/core/workflow/engine/ParallelRunner.ts b/src/core/workflow/engine/ParallelRunner.ts index 5e2d3ae..d4f489b 100644 --- a/src/core/workflow/engine/ParallelRunner.ts +++ b/src/core/workflow/engine/ParallelRunner.ts @@ -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; } 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( diff --git a/src/core/workflow/engine/StepExecutor.ts b/src/core/workflow/engine/StepExecutor.ts index fb9ba74..1d085df 100644 --- a/src/core/workflow/engine/StepExecutor.ts +++ b/src/core/workflow/engine/StepExecutor.ts @@ -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; } 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 }); diff --git a/src/core/workflow/engine/WorkflowEngine.ts b/src/core/workflow/engine/WorkflowEngine.ts index 2cccf91..e49c29f 100644 --- a/src/core/workflow/engine/WorkflowEngine.ts +++ b/src/core/workflow/engine/WorkflowEngine.ts @@ -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; 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 { diff --git a/src/core/workflow/evaluation/AggregateEvaluator.ts b/src/core/workflow/evaluation/AggregateEvaluator.ts index 008d52b..ac24aa3 100644 --- a/src/core/workflow/evaluation/AggregateEvaluator.ts +++ b/src/core/workflow/evaluation/AggregateEvaluator.ts @@ -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'); diff --git a/src/core/workflow/evaluation/RuleEvaluator.ts b/src/core/workflow/evaluation/RuleEvaluator.ts index a944913..605f84d 100644 --- a/src/core/workflow/evaluation/RuleEvaluator.ts +++ b/src/core/workflow/evaluation/RuleEvaluator.ts @@ -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 { 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 { 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', { diff --git a/src/core/workflow/index.ts b/src/core/workflow/index.ts index b23987e..b1fa0ad 100644 --- a/src/core/workflow/index.ts +++ b/src/core/workflow/index.ts @@ -23,6 +23,7 @@ export type { StreamEvent, StreamCallback, PermissionHandler, + PermissionResult, AskUserQuestionHandler, ProviderType, } from './types.js'; diff --git a/src/core/workflow/instruction/InstructionBuilder.ts b/src/core/workflow/instruction/InstructionBuilder.ts index ab341ea..336b234 100644 --- a/src/core/workflow/instruction/InstructionBuilder.ts +++ b/src/core/workflow/instruction/InstructionBuilder.ts @@ -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); } diff --git a/src/core/workflow/instruction/ReportInstructionBuilder.ts b/src/core/workflow/instruction/ReportInstructionBuilder.ts index 4c5588e..b4a1fe7 100644 --- a/src/core/workflow/instruction/ReportInstructionBuilder.ts +++ b/src/core/workflow/instruction/ReportInstructionBuilder.ts @@ -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 = { diff --git a/src/core/workflow/instruction/StatusJudgmentBuilder.ts b/src/core/workflow/instruction/StatusJudgmentBuilder.ts index 17690e8..32c6e1c 100644 --- a/src/core/workflow/instruction/StatusJudgmentBuilder.ts +++ b/src/core/workflow/instruction/StatusJudgmentBuilder.ts @@ -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'); diff --git a/src/core/workflow/instruction/instruction-context.ts b/src/core/workflow/instruction/instruction-context.ts index e265306..72cf9ed 100644 --- a/src/core/workflow/instruction/instruction-context.ts +++ b/src/core/workflow/instruction/instruction-context.ts @@ -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 */ diff --git a/src/core/workflow/instruction/status-rules.ts b/src/core/workflow/instruction/status-rules.ts index 0e00201..84f64d8 100644 --- a/src/core/workflow/instruction/status-rules.ts +++ b/src/core/workflow/instruction/status-rules.ts @@ -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('```'); diff --git a/src/core/workflow/phase-runner.ts b/src/core/workflow/phase-runner.ts index 6ab0ec5..cef02d8 100644 --- a/src/core/workflow/phase-runner.ts +++ b/src/core/workflow/phase-runner.ts @@ -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 { 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(); 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, { diff --git a/src/core/workflow/types.ts b/src/core/workflow/types.ts index dbf87f0..081f5e4 100644 --- a/src/core/workflow/types.ts +++ b/src/core/workflow/types.ts @@ -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; - suggestions?: Array>; + suggestions?: PermissionUpdate[]; blockedPath?: string; decisionReason?: string; } +export type { PermissionResult, PermissionUpdate }; + export type PermissionHandler = (request: PermissionRequest) => Promise; export interface AskUserQuestionInput { @@ -89,6 +91,19 @@ export type AskUserQuestionHandler = ( input: AskUserQuestionInput ) => Promise>; +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; + /** 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 */ diff --git a/src/features/config/ejectBuiltin.ts b/src/features/config/ejectBuiltin.ts index 9b46c01..20e20f5 100644 --- a/src/features/config/ejectBuiltin.ts +++ b/src/features/config/ejectBuiltin.ts @@ -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'; /** diff --git a/src/features/config/switchConfig.ts b/src/features/config/switchConfig.ts index 8c74847..ded0a00 100644 --- a/src/features/config/switchConfig.ts +++ b/src/features/config/switchConfig.ts @@ -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 diff --git a/src/features/config/switchWorkflow.ts b/src/features/config/switchWorkflow.ts index 1d2091c..a04ab58 100644 --- a/src/features/config/switchWorkflow.ts +++ b/src/features/config/switchWorkflow.ts @@ -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 diff --git a/src/features/interactive/interactive.ts b/src/features/interactive/interactive.ts index 4b5ff81..595e05b 100644 --- a/src/features/interactive/interactive.ts +++ b/src/features/interactive/interactive.ts @@ -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; diff --git a/src/features/pipeline/execute.ts b/src/features/pipeline/execute.ts index 33482b6..4f56966 100644 --- a/src/features/pipeline/execute.ts +++ b/src/features/pipeline/execute.ts @@ -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 }; diff --git a/src/features/tasks/add/index.ts b/src/features/tasks/add/index.ts index 9e5c168..46b5bf2 100644 --- a/src/features/tasks/add/index.ts +++ b/src/features/tasks/add/index.ts @@ -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'); diff --git a/src/features/tasks/execute/selectAndExecute.ts b/src/features/tasks/execute/selectAndExecute.ts index 811d5bc..5a34609 100644 --- a/src/features/tasks/execute/selectAndExecute.ts +++ b/src/features/tasks/execute/selectAndExecute.ts @@ -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) { diff --git a/src/features/tasks/execute/session.ts b/src/features/tasks/execute/session.ts index 13b9899..9a9649e 100644 --- a/src/features/tasks/execute/session.ts +++ b/src/features/tasks/execute/session.ts @@ -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'; /** diff --git a/src/features/tasks/execute/taskExecution.ts b/src/features/tasks/execute/taskExecution.ts index 9012656..6a02ebc 100644 --- a/src/features/tasks/execute/taskExecution.ts +++ b/src/features/tasks/execute/taskExecution.ts @@ -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 { - 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 language: globalConfig.language, provider: agentOverrides?.provider, model: agentOverrides?.model, + interactiveUserInput, }); return result.success; } diff --git a/src/features/tasks/execute/types.ts b/src/features/tasks/execute/types.ts index eca493e..5b3b782 100644 --- a/src/features/tasks/execute/types.ts +++ b/src/features/tasks/execute/types.ts @@ -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; } diff --git a/src/features/tasks/execute/workflowExecution.ts b/src/features/tasks/execute/workflowExecution.ts index d0a2a20..bfc6982 100644 --- a/src/features/tasks/execute/workflowExecution.ts +++ b/src/features/tasks/execute/workflowExecution.ts @@ -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 { 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 => { + 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(); diff --git a/src/features/tasks/list/index.ts b/src/features/tasks/list/index.ts index 3e9d4ef..433be55 100644 --- a/src/features/tasks/list/index.ts +++ b/src/features/tasks/list/index.ts @@ -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, diff --git a/src/features/tasks/list/taskActions.ts b/src/features/tasks/list/taskActions.ts index 9058258..3a9f332 100644 --- a/src/features/tasks/list/taskActions.ts +++ b/src/features/tasks/list/taskActions.ts @@ -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'); diff --git a/src/features/tasks/watch/index.ts b/src/features/tasks/watch/index.ts index caffcc3..8d68d67 100644 --- a/src/features/tasks/watch/index.ts +++ b/src/features/tasks/watch/index.ts @@ -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'; /** diff --git a/src/index.ts b/src/index.ts index 1caca1f..72e8376 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,39 @@ // Models export * from './core/models/index.js'; -// Configuration -export * from './infra/config/index.js'; +// Configuration (PermissionMode excluded to avoid name conflict with core/models PermissionMode) +export * from './infra/config/paths.js'; +export * from './infra/config/loaders/index.js'; +export * from './infra/config/global/index.js'; +export { + loadProjectConfig, + saveProjectConfig, + updateProjectConfig, + getCurrentWorkflow, + setCurrentWorkflow, + isVerboseMode, + type ProjectPermissionMode, + type ProjectLocalConfig, + writeFileAtomic, + getInputHistoryPath, + MAX_INPUT_HISTORY, + loadInputHistory, + saveInputHistory, + addToInputHistory, + type AgentSessionData, + getAgentSessionsPath, + loadAgentSessions, + saveAgentSessions, + updateAgentSession, + clearAgentSessions, + getWorktreeSessionsDir, + encodeWorktreePath, + getWorktreeSessionPath, + loadWorktreeSessions, + updateWorktreeSession, + getClaudeProjectSessionsDir, + clearClaudeProjectSessions, +} from './infra/config/project/index.js'; // Claude integration export { @@ -40,7 +71,7 @@ export { detectJudgeIndex, buildJudgePrompt, isRegexSafe, -} from './claude/index.js'; +} from './infra/claude/index.js'; export type { StreamEvent, StreamCallback, @@ -60,10 +91,10 @@ export type { ThinkingEventData, ResultEventData, ErrorEventData, -} from './claude/index.js'; +} from './infra/claude/index.js'; // Codex integration -export * from './codex/index.js'; +export * from './infra/codex/index.js'; // Agent execution export * from './agents/index.js'; @@ -117,6 +148,10 @@ export type { // Utilities export * from './shared/utils/index.js'; export * from './shared/ui/index.js'; +export * from './shared/prompt/index.js'; +export * from './shared/constants.js'; +export * from './shared/context.js'; +export * from './shared/exitCodes.js'; // Resources (embedded prompts and templates) -export * from './resources/index.js'; +export * from './infra/resources/index.js'; diff --git a/src/claude/client.ts b/src/infra/claude/client.ts similarity index 98% rename from src/claude/client.ts rename to src/infra/claude/client.ts index ad01cb4..17a4934 100644 --- a/src/claude/client.ts +++ b/src/infra/claude/client.ts @@ -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'; diff --git a/src/claude/executor.ts b/src/infra/claude/executor.ts similarity index 97% rename from src/claude/executor.ts rename to src/infra/claude/executor.ts index b2251ce..fe4462b 100644 --- a/src/claude/executor.ts +++ b/src/infra/claude/executor.ts @@ -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, diff --git a/src/claude/index.ts b/src/infra/claude/index.ts similarity index 100% rename from src/claude/index.ts rename to src/infra/claude/index.ts diff --git a/src/claude/options-builder.ts b/src/infra/claude/options-builder.ts similarity index 98% rename from src/claude/options-builder.ts rename to src/infra/claude/options-builder.ts index d9aa6da..4c1346b 100644 --- a/src/claude/options-builder.ts +++ b/src/infra/claude/options-builder.ts @@ -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, diff --git a/src/claude/process.ts b/src/infra/claude/process.ts similarity index 100% rename from src/claude/process.ts rename to src/infra/claude/process.ts diff --git a/src/claude/query-manager.ts b/src/infra/claude/query-manager.ts similarity index 100% rename from src/claude/query-manager.ts rename to src/infra/claude/query-manager.ts diff --git a/src/claude/stream-converter.ts b/src/infra/claude/stream-converter.ts similarity index 100% rename from src/claude/stream-converter.ts rename to src/infra/claude/stream-converter.ts diff --git a/src/claude/types.ts b/src/infra/claude/types.ts similarity index 94% rename from src/claude/types.ts rename to src/infra/claude/types.ts index c186985..a4e5d16 100644 --- a/src/claude/types.ts +++ b/src/infra/claude/types.ts @@ -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 }; diff --git a/src/codex/CodexStreamHandler.ts b/src/infra/codex/CodexStreamHandler.ts similarity index 99% rename from src/codex/CodexStreamHandler.ts rename to src/infra/codex/CodexStreamHandler.ts index c6ab1d5..16f6945 100644 --- a/src/codex/CodexStreamHandler.ts +++ b/src/infra/codex/CodexStreamHandler.ts @@ -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; diff --git a/src/codex/client.ts b/src/infra/codex/client.ts similarity index 97% rename from src/codex/client.ts rename to src/infra/codex/client.ts index be0008d..983dd88 100644 --- a/src/codex/client.ts +++ b/src/infra/codex/client.ts @@ -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, diff --git a/src/codex/index.ts b/src/infra/codex/index.ts similarity index 100% rename from src/codex/index.ts rename to src/infra/codex/index.ts diff --git a/src/codex/types.ts b/src/infra/codex/types.ts similarity index 86% rename from src/codex/types.ts rename to src/infra/codex/types.ts index 78f8b0a..45a689c 100644 --- a/src/codex/types.ts +++ b/src/infra/codex/types.ts @@ -2,7 +2,7 @@ * Type definitions for Codex SDK integration */ -import type { StreamCallback } from '../claude/types.js'; +import type { StreamCallback } from '../claude/index.js'; /** Options for calling Codex */ export interface CodexCallOptions { diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index b89ed59..84a41ed 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -11,7 +11,7 @@ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { GlobalConfigSchema } from '../../../core/models/index.js'; import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js'; import { getGlobalConfigPath, getProjectConfigPath } from '../paths.js'; -import { DEFAULT_LANGUAGE } from '../../../constants.js'; +import { DEFAULT_LANGUAGE } from '../../../shared/constants.js'; /** Create default global configuration (fresh instance each call) */ function createDefaultGlobalConfig(): GlobalConfig { diff --git a/src/infra/config/global/initialization.ts b/src/infra/config/global/initialization.ts index 399cd10..43a04dd 100644 --- a/src/infra/config/global/initialization.ts +++ b/src/infra/config/global/initialization.ts @@ -9,15 +9,15 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import type { Language } from '../../../core/models/index.js'; -import { DEFAULT_LANGUAGE } from '../../../constants.js'; -import { selectOptionWithDefault } from '../../../prompt/index.js'; +import { DEFAULT_LANGUAGE } from '../../../shared/constants.js'; +import { selectOptionWithDefault } from '../../../shared/prompt/index.js'; import { getGlobalConfigDir, getGlobalConfigPath, getProjectConfigDir, ensureDir, } from '../paths.js'; -import { copyProjectResourcesToDir, getLanguageResourcesDir } from '../../../resources/index.js'; +import { copyProjectResourcesToDir, getLanguageResourcesDir } from '../../resources/index.js'; import { setLanguage, setProvider } from './globalConfig.js'; /** diff --git a/src/infra/config/index.ts b/src/infra/config/index.ts index 06829c1..03378b8 100644 --- a/src/infra/config/index.ts +++ b/src/infra/config/index.ts @@ -3,5 +3,6 @@ */ export * from './paths.js'; -export * from './loaders/loader.js'; -export * from './global/initialization.js'; +export * from './loaders/index.js'; +export * from './global/index.js'; +export * from './project/index.js'; diff --git a/src/infra/config/loaders/workflowParser.ts b/src/infra/config/loaders/workflowParser.ts index 2dacb9a..58fd6e3 100644 --- a/src/infra/config/loaders/workflowParser.ts +++ b/src/infra/config/loaders/workflowParser.ts @@ -101,7 +101,13 @@ const AGGREGATE_CONDITION_REGEX = /^(all|any)\("(.+)"\)$/; /** * Parse a rule's condition for ai() and all()/any() expressions. */ -function normalizeRule(r: { condition: string; next?: string; appendix?: string }): WorkflowRule { +function normalizeRule(r: { + condition: string; + next?: string; + appendix?: string; + requires_user_input?: boolean; + interactive_only?: boolean; +}): WorkflowRule { const next = r.next ?? ''; const aiMatch = r.condition.match(AI_CONDITION_REGEX); if (aiMatch?.[1]) { @@ -109,6 +115,8 @@ function normalizeRule(r: { condition: string; next?: string; appendix?: string condition: r.condition, next, appendix: r.appendix, + requiresUserInput: r.requires_user_input, + interactiveOnly: r.interactive_only, isAiCondition: true, aiConditionText: aiMatch[1], }; @@ -120,6 +128,8 @@ function normalizeRule(r: { condition: string; next?: string; appendix?: string condition: r.condition, next, appendix: r.appendix, + requiresUserInput: r.requires_user_input, + interactiveOnly: r.interactive_only, isAggregateCondition: true, aggregateType: aggMatch[1] as 'all' | 'any', aggregateConditionText: aggMatch[2], @@ -130,6 +140,8 @@ function normalizeRule(r: { condition: string; next?: string; appendix?: string condition: r.condition, next, appendix: r.appendix, + requiresUserInput: r.requires_user_input, + interactiveOnly: r.interactive_only, }; } diff --git a/src/infra/config/loaders/workflowResolver.ts b/src/infra/config/loaders/workflowResolver.ts index a133d4e..9d81af1 100644 --- a/src/infra/config/loaders/workflowResolver.ts +++ b/src/infra/config/loaders/workflowResolver.ts @@ -11,8 +11,7 @@ import { homedir } from 'node:os'; import type { WorkflowConfig } from '../../../core/models/index.js'; import { getGlobalWorkflowsDir, getBuiltinWorkflowsDir, getProjectConfigDir } from '../paths.js'; import { getLanguage, getDisabledBuiltins } from '../global/globalConfig.js'; -import { createLogger } from '../../../shared/utils/debug.js'; -import { getErrorMessage } from '../../../shared/utils/error.js'; +import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; import { loadWorkflowFromFile } from './workflowParser.js'; const log = createLogger('workflow-resolver'); diff --git a/src/infra/config/paths.ts b/src/infra/config/paths.ts index 26897bd..858355f 100644 --- a/src/infra/config/paths.ts +++ b/src/infra/config/paths.ts @@ -9,7 +9,7 @@ import { homedir } from 'node:os'; import { join, resolve } from 'node:path'; import { existsSync, mkdirSync } from 'node:fs'; import type { Language } from '../../core/models/index.js'; -import { getLanguageResourcesDir } from '../../resources/index.js'; +import { getLanguageResourcesDir } from '../resources/index.js'; /** Get takt global config directory (~/.takt) */ export function getGlobalConfigDir(): string { diff --git a/src/infra/config/project/projectConfig.ts b/src/infra/config/project/projectConfig.ts index d447c5d..ef88c56 100644 --- a/src/infra/config/project/projectConfig.ts +++ b/src/infra/config/project/projectConfig.ts @@ -7,7 +7,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { parse, stringify } from 'yaml'; -import { copyProjectResourcesToDir } from '../../../resources/index.js'; +import { copyProjectResourcesToDir } from '../../resources/index.js'; import type { PermissionMode, ProjectPermissionMode, ProjectLocalConfig } from '../types.js'; export type { PermissionMode, ProjectPermissionMode, ProjectLocalConfig }; diff --git a/src/infra/fs/index.ts b/src/infra/fs/index.ts new file mode 100644 index 0000000..933a7b9 --- /dev/null +++ b/src/infra/fs/index.ts @@ -0,0 +1,28 @@ +/** + * Filesystem utilities - barrel exports + */ + +export type { + SessionLog, + NdjsonWorkflowStart, + NdjsonStepStart, + NdjsonStepComplete, + NdjsonWorkflowComplete, + NdjsonWorkflowAbort, + NdjsonRecord, + LatestLogPointer, +} from './session.js'; + +export { + SessionManager, + appendNdjsonLine, + initNdjsonLog, + loadNdjsonLog, + generateSessionId, + generateReportDir, + createSessionLog, + finalizeSessionLog, + loadSessionLog, + loadProjectContext, + updateLatestPointer, +} from './session.js'; diff --git a/src/infra/fs/session.ts b/src/infra/fs/session.ts index 98479a2..4c233c7 100644 --- a/src/infra/fs/session.ts +++ b/src/infra/fs/session.ts @@ -4,14 +4,14 @@ import { existsSync, readFileSync, copyFileSync, appendFileSync } from 'node:fs'; import { join } from 'node:path'; -import { getProjectLogsDir, getGlobalLogsDir, ensureDir, writeFileAtomic } from '../config/paths.js'; -import { generateReportDir as buildReportDir } from '../../shared/utils/reportDir.js'; +import { getProjectLogsDir, getGlobalLogsDir, ensureDir, writeFileAtomic } from '../config/index.js'; +import { generateReportDir as buildReportDir } from '../../shared/utils/index.js'; import type { SessionLog, NdjsonRecord, NdjsonWorkflowStart, LatestLogPointer, -} from '../../shared/utils/types.js'; +} from '../../shared/utils/index.js'; // Re-export types for backward compatibility export type { @@ -23,7 +23,7 @@ export type { NdjsonWorkflowAbort, NdjsonRecord, LatestLogPointer, -} from '../../shared/utils/types.js'; +} from '../../shared/utils/index.js'; /** * Manages session lifecycle: ID generation, NDJSON logging, diff --git a/src/infra/github/index.ts b/src/infra/github/index.ts new file mode 100644 index 0000000..d0e2fc6 --- /dev/null +++ b/src/infra/github/index.ts @@ -0,0 +1,16 @@ +/** + * GitHub integration - barrel exports + */ + +export type { GitHubIssue, GhCliStatus, CreatePrOptions, CreatePrResult } from './types.js'; + +export { + checkGhCli, + fetchIssue, + formatIssueAsTask, + parseIssueNumbers, + isIssueReference, + resolveIssueTask, +} from './issue.js'; + +export { pushBranch, createPullRequest, buildPrBody } from './pr.js'; diff --git a/src/infra/github/issue.ts b/src/infra/github/issue.ts index 6bc2523..6d4cf82 100644 --- a/src/infra/github/issue.ts +++ b/src/infra/github/issue.ts @@ -6,7 +6,7 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../../shared/utils/debug.js'; +import { createLogger } from '../../shared/utils/index.js'; import type { GitHubIssue, GhCliStatus } from './types.js'; export type { GitHubIssue, GhCliStatus }; diff --git a/src/infra/github/pr.ts b/src/infra/github/pr.ts index 19cd859..d0233bc 100644 --- a/src/infra/github/pr.ts +++ b/src/infra/github/pr.ts @@ -5,8 +5,7 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../../shared/utils/debug.js'; -import { getErrorMessage } from '../../shared/utils/error.js'; +import { createLogger, getErrorMessage } from '../../shared/utils/index.js'; import { checkGhCli } from './issue.js'; import type { GitHubIssue, CreatePrOptions, CreatePrResult } from './types.js'; diff --git a/src/mock/client.ts b/src/infra/mock/client.ts similarity index 94% rename from src/mock/client.ts rename to src/infra/mock/client.ts index 759686d..5c7a7d3 100644 --- a/src/mock/client.ts +++ b/src/infra/mock/client.ts @@ -6,8 +6,8 @@ */ import { randomUUID } from 'node:crypto'; -import type { StreamEvent } from '../claude/process.js'; -import type { AgentResponse } from '../core/models/index.js'; +import type { StreamEvent } from '../claude/index.js'; +import type { AgentResponse } from '../../core/models/index.js'; import { getScenarioQueue } from './scenario.js'; import type { MockCallOptions } from './types.js'; diff --git a/src/infra/mock/index.ts b/src/infra/mock/index.ts new file mode 100644 index 0000000..697ef53 --- /dev/null +++ b/src/infra/mock/index.ts @@ -0,0 +1,13 @@ +/** + * Mock provider utilities - barrel exports + */ + +export { callMock, callMockCustom } from './client.js'; +export { + ScenarioQueue, + loadScenarioFile, + setMockScenario, + getScenarioQueue, + resetScenario, +} from './scenario.js'; +export type { MockCallOptions, ScenarioEntry } from './types.js'; diff --git a/src/mock/scenario.ts b/src/infra/mock/scenario.ts similarity index 100% rename from src/mock/scenario.ts rename to src/infra/mock/scenario.ts diff --git a/src/mock/types.ts b/src/infra/mock/types.ts similarity index 92% rename from src/mock/types.ts rename to src/infra/mock/types.ts index ab76664..078f4f0 100644 --- a/src/mock/types.ts +++ b/src/infra/mock/types.ts @@ -2,7 +2,7 @@ * Mock module type definitions */ -import type { StreamCallback } from '../claude/process.js'; +import type { StreamCallback } from '../claude/index.js'; /** Options for mock calls */ export interface MockCallOptions { diff --git a/src/infra/providers/claude.ts b/src/infra/providers/claude.ts index 8ef5189..13f6f01 100644 --- a/src/infra/providers/claude.ts +++ b/src/infra/providers/claude.ts @@ -2,8 +2,8 @@ * Claude provider implementation */ -import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../../claude/client.js'; -import { resolveAnthropicApiKey } from '../config/global/globalConfig.js'; +import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../claude/index.js'; +import { resolveAnthropicApiKey } from '../config/index.js'; import type { AgentResponse } from '../../core/models/index.js'; import type { Provider, ProviderCallOptions } from './types.js'; diff --git a/src/infra/providers/codex.ts b/src/infra/providers/codex.ts index 45a37c4..cb54b55 100644 --- a/src/infra/providers/codex.ts +++ b/src/infra/providers/codex.ts @@ -2,8 +2,8 @@ * Codex provider implementation */ -import { callCodex, callCodexCustom, type CodexCallOptions } from '../../codex/client.js'; -import { resolveOpenaiApiKey } from '../config/global/globalConfig.js'; +import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/index.js'; +import { resolveOpenaiApiKey } from '../config/index.js'; import type { AgentResponse } from '../../core/models/index.js'; import type { Provider, ProviderCallOptions } from './types.js'; diff --git a/src/infra/providers/mock.ts b/src/infra/providers/mock.ts index ef61799..8850878 100644 --- a/src/infra/providers/mock.ts +++ b/src/infra/providers/mock.ts @@ -2,8 +2,7 @@ * Mock provider implementation */ -import { callMock, callMockCustom } from '../../mock/client.js'; -import type { MockCallOptions } from '../../mock/types.js'; +import { callMock, callMockCustom, type MockCallOptions } from '../mock/index.js'; import type { AgentResponse } from '../../core/models/index.js'; import type { Provider, ProviderCallOptions } from './types.js'; diff --git a/src/infra/providers/types.ts b/src/infra/providers/types.ts index 956b516..e5a6aad 100644 --- a/src/infra/providers/types.ts +++ b/src/infra/providers/types.ts @@ -2,7 +2,7 @@ * Type definitions for the provider abstraction layer */ -import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../../claude/types.js'; +import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/index.js'; import type { AgentResponse, PermissionMode } from '../../core/models/index.js'; /** Common options for all providers */ diff --git a/src/resources/index.ts b/src/infra/resources/index.ts similarity index 93% rename from src/resources/index.ts rename to src/infra/resources/index.ts index ce320cf..67f48bc 100644 --- a/src/resources/index.ts +++ b/src/infra/resources/index.ts @@ -12,7 +12,7 @@ import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import type { Language } from '../core/models/index.js'; +import type { Language } from '../../core/models/index.js'; /** * Get the resources directory path @@ -20,8 +20,8 @@ import type { Language } from '../core/models/index.js'; */ export function getResourcesDir(): string { const currentDir = dirname(fileURLToPath(import.meta.url)); - // From src/resources or dist/resources, go up to project root then into resources/ - return join(currentDir, '..', '..', 'resources'); + // From src/infra/resources or dist/infra/resources, go up to project root then into resources/ + return join(currentDir, '..', '..', '..', 'resources'); } /** diff --git a/src/infra/task/autoCommit.ts b/src/infra/task/autoCommit.ts index 9732b6c..f304602 100644 --- a/src/infra/task/autoCommit.ts +++ b/src/infra/task/autoCommit.ts @@ -8,8 +8,7 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../../shared/utils/debug.js'; -import { getErrorMessage } from '../../shared/utils/error.js'; +import { createLogger, getErrorMessage } from '../../shared/utils/index.js'; import { stageAndCommit } from './git.js'; const log = createLogger('autoCommit'); diff --git a/src/infra/task/branchList.ts b/src/infra/task/branchList.ts index bc056d6..f99c88d 100644 --- a/src/infra/task/branchList.ts +++ b/src/infra/task/branchList.ts @@ -7,7 +7,7 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../../shared/utils/debug.js'; +import { createLogger } from '../../shared/utils/index.js'; import type { BranchInfo, BranchListItem } from './types.js'; diff --git a/src/infra/task/clone.ts b/src/infra/task/clone.ts index eea8152..059af9b 100644 --- a/src/infra/task/clone.ts +++ b/src/infra/task/clone.ts @@ -10,8 +10,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { execFileSync } from 'node:child_process'; -import { createLogger } from '../../shared/utils/debug.js'; -import { slugify } from '../../shared/utils/slug.js'; +import { createLogger, slugify } from '../../shared/utils/index.js'; import { loadGlobalConfig } from '../config/global/globalConfig.js'; import type { WorktreeOptions, WorktreeResult } from './types.js'; diff --git a/src/infra/task/index.ts b/src/infra/task/index.ts index 28909bf..25596de 100644 --- a/src/infra/task/index.ts +++ b/src/infra/task/index.ts @@ -42,6 +42,7 @@ export { getOriginalInstruction, buildListItems, } from './branchList.js'; +export { stageAndCommit } from './git.js'; export { autoCommitAndPush, type AutoCommitResult } from './autoCommit.js'; export { summarizeTaskName } from './summarize.js'; export { TaskWatcher, type TaskWatcherOptions } from './watcher.js'; diff --git a/src/infra/task/summarize.ts b/src/infra/task/summarize.ts index a25e212..e786ed5 100644 --- a/src/infra/task/summarize.ts +++ b/src/infra/task/summarize.ts @@ -7,7 +7,7 @@ import * as wanakana from 'wanakana'; import { loadGlobalConfig } from '../config/global/globalConfig.js'; import { getProvider, type ProviderType } from '../providers/index.js'; -import { createLogger } from '../../shared/utils/debug.js'; +import { createLogger } from '../../shared/utils/index.js'; import type { SummarizeOptions } from './types.js'; export type { SummarizeOptions }; diff --git a/src/infra/task/watcher.ts b/src/infra/task/watcher.ts index d45c3b1..5cb1b7f 100644 --- a/src/infra/task/watcher.ts +++ b/src/infra/task/watcher.ts @@ -5,7 +5,7 @@ * Uses polling (not fs.watch) for cross-platform reliability. */ -import { createLogger } from '../../shared/utils/debug.js'; +import { createLogger } from '../../shared/utils/index.js'; import { TaskRunner } from './runner.js'; import type { TaskInfo } from './types.js'; diff --git a/src/constants.ts b/src/shared/constants.ts similarity index 64% rename from src/constants.ts rename to src/shared/constants.ts index 52faf18..33c7457 100644 --- a/src/constants.ts +++ b/src/shared/constants.ts @@ -2,7 +2,8 @@ * Application-wide constants */ -import type { Language } from './core/models/index.js'; +/** Supported language codes (duplicated from core/models to avoid shared → core dependency) */ +type Language = 'en' | 'ja'; /** Default workflow name when none specified */ export const DEFAULT_WORKFLOW_NAME = 'default'; diff --git a/src/context.ts b/src/shared/context.ts similarity index 100% rename from src/context.ts rename to src/shared/context.ts diff --git a/src/exitCodes.ts b/src/shared/exitCodes.ts similarity index 100% rename from src/exitCodes.ts rename to src/shared/exitCodes.ts diff --git a/src/prompt/confirm.ts b/src/shared/prompt/confirm.ts similarity index 100% rename from src/prompt/confirm.ts rename to src/shared/prompt/confirm.ts diff --git a/src/prompt/index.ts b/src/shared/prompt/index.ts similarity index 100% rename from src/prompt/index.ts rename to src/shared/prompt/index.ts diff --git a/src/prompt/select.ts b/src/shared/prompt/select.ts similarity index 99% rename from src/prompt/select.ts rename to src/shared/prompt/select.ts index 4e0da3a..cdb24b9 100644 --- a/src/prompt/select.ts +++ b/src/shared/prompt/select.ts @@ -5,7 +5,7 @@ */ import chalk from 'chalk'; -import { truncateText } from '../shared/utils/text.js'; +import { truncateText } from '../utils/index.js'; /** Option type for selectOption */ export interface SelectOptionItem { diff --git a/src/shared/ui/StreamDisplay.ts b/src/shared/ui/StreamDisplay.ts index d10203a..8b98484 100644 --- a/src/shared/ui/StreamDisplay.ts +++ b/src/shared/ui/StreamDisplay.ts @@ -6,7 +6,11 @@ */ import chalk from 'chalk'; -import type { StreamEvent, StreamCallback } from '../../claude/types.js'; +// NOTE: type-only import from core — acceptable because StreamDisplay is +// a UI renderer tightly coupled to the workflow event protocol. +// Moving StreamEvent/StreamCallback to shared would require relocating all +// dependent event-data types, which is out of scope for this refactoring. +import type { StreamEvent, StreamCallback } from '../../core/workflow/index.js'; import { truncate } from './LogManager.js'; /** Stream display manager for real-time Claude output */ diff --git a/src/shared/utils/debug.ts b/src/shared/utils/debug.ts index 1d99546..9449949 100644 --- a/src/shared/utils/debug.ts +++ b/src/shared/utils/debug.ts @@ -6,7 +6,11 @@ import { existsSync, appendFileSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import type { DebugConfig } from '../../core/models/index.js'; +/** Debug configuration (duplicated from core/models to avoid shared → core dependency) */ +interface DebugConfig { + enabled: boolean; + logFile?: string; +} /** * Debug logger singleton. diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index 45d8b1f..1f49ef9 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -5,6 +5,7 @@ export * from './debug.js'; export * from './error.js'; export * from './notification.js'; +export * from './reportDir.js'; export * from './slug.js'; export * from './text.js'; export * from './types.js';