diff --git a/docs/vertical-slice-migration-map.md b/docs/vertical-slice-migration-map.md deleted file mode 100644 index 3b67970..0000000 --- a/docs/vertical-slice-migration-map.md +++ /dev/null @@ -1,111 +0,0 @@ -# Vertical Slice + Core ハイブリッド構成 マッピング案 - -## 目的 -- CLI中心の機能(コマンド)を slice 化し、変更影響を局所化する。 -- Workflow Engine などのコアは内向き依存(Clean)で保護する。 -- Public API(`index.ts`)で境界を固定し、深い import を避ける。 - -## 依存ルール(簡易) -- `core` は外側に依存しない。 -- `infra` は `core` に依存できる。 -- `features` は `core` / `infra` / `shared` に依存できる。 -- `app` は配線専用(入口)。 - -## 移行マップ - -### 1) app/cli(CLI入口・配線) -``` -src/cli/index.ts -> src/app/cli/index.ts -src/cli/program.ts -> src/app/cli/program.ts -src/cli/commands.ts -> src/app/cli/commands.ts -src/cli/routing.ts -> src/app/cli/routing.ts -src/cli/helpers.ts -> src/app/cli/helpers.ts -``` -- `app/cli/index.ts` は CLI エントリのみ。 -- ルーティングは `features` の Public API を呼ぶだけにする。 - -### 2) features(コマンド単位) -``` -src/commands/index.ts -> src/features/tasks/index.ts -src/commands/runAllTasks.ts -> src/features/tasks/run/index.ts -src/commands/watchTasks.ts -> src/features/tasks/watch/index.ts -src/commands/addTask.ts -> src/features/tasks/add/index.ts -src/commands/listTasks.ts -> src/features/tasks/list/index.ts -src/commands/execution/selectAndExecute.ts -> src/features/tasks/execute/selectAndExecute.ts -src/commands/execution/types.ts -> src/features/tasks/execute/types.ts - -src/commands/pipeline/executePipeline.ts -> src/features/pipeline/execute.ts -src/commands/pipeline/index.ts -> src/features/pipeline/index.ts - -src/commands/switchWorkflow.ts -> src/features/config/switchWorkflow.ts -src/commands/switchConfig.ts -> src/features/config/switchConfig.ts -src/commands/ejectBuiltin.ts -> src/features/config/ejectBuiltin.ts -``` -- `features/tasks` は run/watch/add/list の共通入口を持つ。 -- `features/pipeline` は pipeline モードの専用 slice。 -- `features/config` は設定系(switch/eject)を集約。 - -### 3) core/workflow(中核ロジック) -``` -src/workflow/engine/* -> src/core/workflow/engine/* -src/workflow/instruction/* -> src/core/workflow/instruction/* -src/workflow/evaluation/* -> src/core/workflow/evaluation/* -src/workflow/types.ts -> src/core/workflow/types.ts -src/workflow/constants.ts -> src/core/workflow/constants.ts -src/workflow/index.ts -> src/core/workflow/index.ts -``` -- `core/workflow/index.ts` だけを Public API として使用。 -- `engine/`, `instruction/`, `evaluation/` 間の依存は内向き(core 内のみ)。 - -### 4) core/models(型・スキーマ) -``` -src/models/schemas.ts -> src/core/models/schemas.ts -src/models/types.ts -> src/core/models/types.ts -src/models/workflow-types.ts -> src/core/models/workflow-types.ts -src/models/index.ts -> src/core/models/index.ts -``` -- `core/models/index.ts` を Public API 化。 - -### 5) infra(外部I/O) -``` -src/providers/* -> src/infra/providers/* -src/github/* -> src/infra/github/* -src/config/* -> src/infra/config/* -src/task/* -> src/infra/task/* -src/utils/session.ts -> src/infra/fs/session.ts -src/utils/git/* -> src/infra/git/* -``` -- GitHub API / FS / Git / Provider など外部依存は `infra` に集約。 - -### 6) shared(横断ユーティリティ) -``` -src/utils/error.ts -> src/shared/utils/error.ts -src/utils/debug.ts -> src/shared/utils/debug.ts -src/utils/ui.ts -> src/shared/ui/index.ts -src/utils/* -> src/shared/utils/* (外部I/O以外) -``` -- 共有は `shared` に集めるが、肥大化は避ける。 - -### 7) docs(参照パス修正) -``` -docs/data-flow.md -> パス参照を app/core/features に合わせて更新 -`src/cli.ts` 参照 -> `src/app/cli/index.ts` に更新 -`src/workflow/state-manager.ts` 参照 -> `src/core/workflow/engine/state-manager.ts` -`src/workflow/transitions.ts` 参照 -> `src/core/workflow/engine/transitions.ts` -``` - -## Public API ルール -- `core/*` と `features/*` は **必ず `index.ts` から import**。 -- 深い import(`../engine/xxx` など)は禁止。 - -## 移行順序(推奨) -1. `core/` に workflow + models を集約 -2. `infra/` に外部I/Oを移動 -3. `features/` にコマンド単位で集約 -4. `app/cli` にエントリを移す -5. Public API を整理し、深い import を排除 -6. docs の参照を更新 - -## 備考 -- `src/workflow/index.ts` は `core/workflow/index.ts` に移し、外部からはここだけを参照。 -- `src/models/workflow.ts` のようなプレースホルダは廃止するか、`core/models/index.ts` へ統合する。 diff --git a/docs/vertical-slice-migration-plan.md b/docs/vertical-slice-migration-plan.md deleted file mode 100644 index 154a49a..0000000 --- a/docs/vertical-slice-migration-plan.md +++ /dev/null @@ -1,361 +0,0 @@ -# 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/docs/workflows.md b/docs/workflows.md index 5a77a60..5510c76 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -15,6 +15,11 @@ A workflow is a YAML file that defines a sequence of steps executed by AI agents - `~/.takt/workflows/` — User workflows (override builtins with the same name) - Use `takt eject ` to copy a builtin to `~/.takt/workflows/` for customization +## Workflow Categories + +ワークフローの選択 UI をカテゴリ分けしたい場合は、`workflow_categories` を設定します。 +詳細は `docs/workflow-categories.md` を参照してください。 + ## Workflow Schema ```yaml diff --git a/report/plan.md b/report/plan.md deleted file mode 100644 index 9e4950a..0000000 --- a/report/plan.md +++ /dev/null @@ -1,58 +0,0 @@ -# 実装計画: 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/default-categories.yaml b/resources/global/en/default-categories.yaml new file mode 100644 index 0000000..2193277 --- /dev/null +++ b/resources/global/en/default-categories.yaml @@ -0,0 +1,25 @@ +workflow_categories: + "🚀 Quick Start": + - minimal + - default + + "🔍 Review & Fix": + - review-fix-minimal + + "🎨 Frontend": + [] + + "⚙️ Backend": + [] + + "🔧 Full Stack": + - expert + - expert-cqrs + + "Others": + - research + - magi + - review-only + +show_others_category: true +others_category_name: "Others" diff --git a/resources/global/en/workflows/default.yaml b/resources/global/en/workflows/default.yaml index f393063..b20a98a 100644 --- a/resources/global/en/workflows/default.yaml +++ b/resources/global/en/workflows/default.yaml @@ -87,6 +87,7 @@ steps: - name: implement edit: true agent: ../agents/default/coder.md + session: refresh report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md diff --git a/resources/global/en/workflows/expert-cqrs.yaml b/resources/global/en/workflows/expert-cqrs.yaml index 272ee21..ceab123 100644 --- a/resources/global/en/workflows/expert-cqrs.yaml +++ b/resources/global/en/workflows/expert-cqrs.yaml @@ -86,6 +86,7 @@ steps: - name: implement edit: true agent: ../agents/default/coder.md + session: refresh report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md diff --git a/resources/global/en/workflows/expert.yaml b/resources/global/en/workflows/expert.yaml index c117a4c..0b79511 100644 --- a/resources/global/en/workflows/expert.yaml +++ b/resources/global/en/workflows/expert.yaml @@ -98,6 +98,7 @@ steps: - name: implement edit: true agent: ../agents/default/coder.md + session: refresh report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md diff --git a/resources/global/en/workflows/minimal.yaml b/resources/global/en/workflows/minimal.yaml new file mode 100644 index 0000000..d1af610 --- /dev/null +++ b/resources/global/en/workflows/minimal.yaml @@ -0,0 +1,427 @@ +# Minimal TAKT Workflow +# Implement -> Parallel Review (AI + Supervisor) -> Fix if needed -> Complete +# (Simplest configuration - no plan, no architect review) +# +# Template Variables (auto-injected): +# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {max_iterations} - Maximum iterations allowed for the workflow +# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {task} - Original user request (auto-injected) +# {previous_response} - Output from the previous step (auto-injected) +# {user_inputs} - Accumulated user inputs during workflow (auto-injected) +# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") + +name: minimal +description: Minimal development workflow (implement -> parallel review -> fix if needed -> complete) + +max_iterations: 20 + +initial_step: implement + +steps: + - name: implement + edit: true + agent: ../agents/default/coder.md + report: + - Scope: 01-coder-scope.md + - Decisions: 02-coder-decisions.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + instruction_template: | + Implement the task. + Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. + + **Scope report format (create at implementation start):** + ```markdown + # Change Scope Declaration + + ## Task + {One-line task summary} + + ## Planned Changes + | Type | File | + |------|------| + | Create | `src/example.ts` | + | Modify | `src/routes.ts` | + + ## Estimated Size + Small / Medium / Large + + ## Impact Scope + - {Affected modules or features} + ``` + + **Decisions report format (on completion, only if decisions were made):** + ```markdown + # Decision Log + + ## 1. {Decision Content} + - **Background**: {Why the decision was needed} + - **Options Considered**: {List of options} + - **Reason**: {Why this option was chosen} + ``` + + **Required output (include headings)** + ## Work done + - {summary of work performed} + ## Changes made + - {summary of code changes} + ## Test results + - {command and outcome} + + rules: + - condition: Implementation complete + next: reviewers + - condition: Cannot proceed, insufficient info + next: ABORT + - condition: User input required because there are items to confirm with the user + next: implement + requires_user_input: true + interactive_only: true + + - name: reviewers + parallel: + - name: ai_review + edit: false + agent: ../agents/default/ai-antipattern-reviewer.md + report: + name: 03-ai-review.md + format: | + ```markdown + # AI-Generated Code Review + + ## Result: APPROVE / REJECT + + ## Summary + {One sentence summarizing result} + + ## Verified Items + | Aspect | Result | Notes | + |--------|--------|-------| + | Assumption validity | ✅ | - | + | API/Library existence | ✅ | - | + | Context fit | ✅ | - | + | Scope | ✅ | - | + + ## Issues (if REJECT) + | # | Category | Location | Issue | + |---|----------|----------|-------| + | 1 | Hallucinated API | `src/file.ts:23` | Non-existent method | + ``` + + **Cognitive load reduction rules:** + - No issues -> Summary 1 line + check table only (10 lines or less) + - Issues found -> + Issues in table format (25 lines or less) + allowed_tools: + - Read + - Glob + - Grep + - Write + - WebSearch + - WebFetch + instruction_template: | + Review the code for AI-specific issues: + - Assumption validation + - Plausible but wrong patterns + - Context fit with existing codebase + - Scope creep detection + rules: + - condition: No AI-specific issues + - condition: AI-specific issues found + + - name: supervise + edit: false + agent: ../agents/default/supervisor.md + report: + - Validation: 05-supervisor-validation.md + - Summary: summary.md + allowed_tools: + - Read + - Glob + - Grep + - Write + - Bash + - WebSearch + - WebFetch + instruction_template: | + Run tests, verify the build, and perform final approval. + + **Workflow Overall Review:** + 1. Does the implementation meet the original request? + 2. Were AI Review issues addressed? + 3. Was the original task objective achieved? + + **Review Reports:** Read all reports in Report Directory and + check for any unaddressed improvement suggestions. + + **Validation report format:** + ```markdown + # Final Validation Results + + ## Result: APPROVE / REJECT + + ## Validation Summary + | Item | Status | Verification Method | + |------|--------|---------------------| + | Requirements met | ✅ | Matched against requirements list | + | Tests | ✅ | `npm test` (N passed) | + | Build | ✅ | `npm run build` succeeded | + | Functional check | ✅ | Main flows verified | + + ## Deliverables + - Created: {Created files} + - Modified: {Modified files} + + ## Incomplete Items (if REJECT) + | # | Item | Reason | + |---|------|--------| + | 1 | {Item} | {Reason} | + ``` + + **Summary report format (only if APPROVE):** + ```markdown + # Task Completion Summary + + ## Task + {Original request in 1-2 sentences} + + ## Result + ✅ Complete + + ## Changes + | Type | File | Summary | + |------|------|---------| + | Create | `src/file.ts` | Summary description | + + ## Review Results + | Review | Result | + |--------|--------| + | AI Review | ✅ APPROVE | + | Supervisor | ✅ APPROVE | + + ## Verification Commands + ```bash + npm test + npm run build + ``` + ``` + rules: + - condition: All checks passed + - condition: Requirements unmet, tests failing + + rules: + - condition: all("No AI-specific issues", "All checks passed") + next: COMPLETE + - condition: all("AI-specific issues found", "Requirements unmet, tests failing") + next: fix_both + - condition: any("AI-specific issues found") + next: ai_fix + - condition: any("Requirements unmet, tests failing") + next: supervise_fix + + - name: fix_both + parallel: + - name: ai_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI Reviewer's issues fixed + - condition: No fix needed (verified target files/spec) + - condition: Cannot proceed, insufficient info + instruction_template: | + **This is AI Review iteration {step_iteration}.** + + If this is iteration 2 or later, it means your previous fixes were not actually applied. + **Your belief that you "already fixed it" is wrong.** + + **First, acknowledge:** + - Files you thought were "fixed" are actually not fixed + - Your understanding of previous work is incorrect + - You need to start from zero + + **Required actions:** + 1. Open all flagged files with Read tool (drop assumptions, verify facts) + 2. Search for problem code with grep to confirm it exists + 3. Fix confirmed problems with Edit tool + 4. Run tests to verify (e.g., `npm test`, `./gradlew test`) + 5. Report specifically "what you checked and what you fixed" + + **Report format:** + - ❌ "Already fixed" + - ✅ "Checked file X at L123, found problem Y, fixed to Z" + + **Absolutely prohibited:** + - Reporting "fixed" without opening files + - Judging based on assumptions + - Leaving problems that AI Reviewer REJECTED + + **Handling "no fix needed" (required)** + - Do not claim "no fix needed" unless you can show the checked target file(s) for each AI Review issue + - If an issue involves generated code or spec sync, and you cannot verify the source spec, output the tag for "Cannot proceed, insufficient info" + - When "no fix needed", output the tag for "Cannot proceed, insufficient info" and include the reason + checked scope + + **Required output (include headings)** + ## Files checked + - {path:line} + ## Searches run + - {command and summary} + ## Fixes applied + - {what changed} + ## Test results + - {command and outcome} + + - name: supervise_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: Supervisor's issues fixed + - condition: Cannot proceed, insufficient info + instruction_template: | + Fix the issues pointed out by the supervisor. + + 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} + + rules: + - condition: all("AI Reviewer's issues fixed", "Supervisor's issues fixed") + next: reviewers + - condition: any("No fix needed (verified target files/spec)", "Cannot proceed, insufficient info") + next: implement + + - name: ai_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI Reviewer's issues fixed + next: reviewers + - condition: No fix needed (verified target files/spec) + next: implement + - condition: Cannot proceed, insufficient info + next: implement + instruction_template: | + **This is AI Review iteration {step_iteration}.** + + If this is iteration 2 or later, it means your previous fixes were not actually applied. + **Your belief that you "already fixed it" is wrong.** + + **First, acknowledge:** + - Files you thought were "fixed" are actually not fixed + - Your understanding of previous work is incorrect + - You need to start from zero + + **Required actions:** + 1. Open all flagged files with Read tool (drop assumptions, verify facts) + 2. Search for problem code with grep to confirm it exists + 3. Fix confirmed problems with Edit tool + 4. Run tests to verify (e.g., `npm test`, `./gradlew test`) + 5. Report specifically "what you checked and what you fixed" + + **Report format:** + - ❌ "Already fixed" + - ✅ "Checked file X at L123, found problem Y, fixed to Z" + + **Absolutely prohibited:** + - Reporting "fixed" without opening files + - Judging based on assumptions + - Leaving problems that AI Reviewer REJECTED + + **Handling "no fix needed" (required)** + - Do not claim "no fix needed" unless you can show the checked target file(s) for each AI Review issue + - If an issue involves generated code or spec sync, and you cannot verify the source spec, output the tag for "Cannot proceed, insufficient info" + - When "no fix needed", output the tag for "Cannot proceed, insufficient info" and include the reason + checked scope + + **Required output (include headings)** + ## Files checked + - {path:line} + ## Searches run + - {command and summary} + ## Fixes applied + - {what changed} + ## Test results + - {command and outcome} + + - name: supervise_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: Supervisor's issues fixed + next: reviewers + - condition: Cannot proceed, insufficient info + next: implement + instruction_template: | + Fix the issues pointed out by the supervisor. + + 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} diff --git a/resources/global/en/workflows/review-fix-minimal.yaml b/resources/global/en/workflows/review-fix-minimal.yaml new file mode 100644 index 0000000..0bc6931 --- /dev/null +++ b/resources/global/en/workflows/review-fix-minimal.yaml @@ -0,0 +1,427 @@ +# Review-Fix Minimal TAKT Workflow +# Review -> Fix (if needed) -> Re-review -> Complete +# (Starts with review, no implementation step) +# +# Template Variables (auto-injected): +# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {max_iterations} - Maximum iterations allowed for the workflow +# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {task} - Original user request (auto-injected) +# {previous_response} - Output from the previous step (auto-injected) +# {user_inputs} - Accumulated user inputs during workflow (auto-injected) +# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") + +name: review-fix-minimal +description: Review and fix workflow for existing code (starts with review, no implementation) + +max_iterations: 20 + +initial_step: reviewers + +steps: + - name: implement + edit: true + agent: ../agents/default/coder.md + report: + - Scope: 01-coder-scope.md + - Decisions: 02-coder-decisions.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + instruction_template: | + Implement the task. + Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. + + **Scope report format (create at implementation start):** + ```markdown + # Change Scope Declaration + + ## Task + {One-line task summary} + + ## Planned Changes + | Type | File | + |------|------| + | Create | `src/example.ts` | + | Modify | `src/routes.ts` | + + ## Estimated Size + Small / Medium / Large + + ## Impact Scope + - {Affected modules or features} + ``` + + **Decisions report format (on completion, only if decisions were made):** + ```markdown + # Decision Log + + ## 1. {Decision Content} + - **Background**: {Why the decision was needed} + - **Options Considered**: {List of options} + - **Reason**: {Why this option was chosen} + ``` + + **Required output (include headings)** + ## Work done + - {summary of work performed} + ## Changes made + - {summary of code changes} + ## Test results + - {command and outcome} + + rules: + - condition: Implementation complete + next: reviewers + - condition: Cannot proceed, insufficient info + next: ABORT + - condition: User input required because there are items to confirm with the user + next: implement + requires_user_input: true + interactive_only: true + + - name: reviewers + parallel: + - name: ai_review + edit: false + agent: ../agents/default/ai-antipattern-reviewer.md + report: + name: 03-ai-review.md + format: | + ```markdown + # AI-Generated Code Review + + ## Result: APPROVE / REJECT + + ## Summary + {One sentence summarizing result} + + ## Verified Items + | Aspect | Result | Notes | + |--------|--------|-------| + | Assumption validity | ✅ | - | + | API/Library existence | ✅ | - | + | Context fit | ✅ | - | + | Scope | ✅ | - | + + ## Issues (if REJECT) + | # | Category | Location | Issue | + |---|----------|----------|-------| + | 1 | Hallucinated API | `src/file.ts:23` | Non-existent method | + ``` + + **Cognitive load reduction rules:** + - No issues -> Summary 1 line + check table only (10 lines or less) + - Issues found -> + Issues in table format (25 lines or less) + allowed_tools: + - Read + - Glob + - Grep + - Write + - WebSearch + - WebFetch + instruction_template: | + Review the code for AI-specific issues: + - Assumption validation + - Plausible but wrong patterns + - Context fit with existing codebase + - Scope creep detection + rules: + - condition: No AI-specific issues + - condition: AI-specific issues found + + - name: supervise + edit: false + agent: ../agents/default/supervisor.md + report: + - Validation: 05-supervisor-validation.md + - Summary: summary.md + allowed_tools: + - Read + - Glob + - Grep + - Write + - Bash + - WebSearch + - WebFetch + instruction_template: | + Run tests, verify the build, and perform final approval. + + **Workflow Overall Review:** + 1. Does the implementation meet the original request? + 2. Were AI Review issues addressed? + 3. Was the original task objective achieved? + + **Review Reports:** Read all reports in Report Directory and + check for any unaddressed improvement suggestions. + + **Validation report format:** + ```markdown + # Final Validation Results + + ## Result: APPROVE / REJECT + + ## Validation Summary + | Item | Status | Verification Method | + |------|--------|---------------------| + | Requirements met | ✅ | Matched against requirements list | + | Tests | ✅ | `npm test` (N passed) | + | Build | ✅ | `npm run build` succeeded | + | Functional check | ✅ | Main flows verified | + + ## Deliverables + - Created: {Created files} + - Modified: {Modified files} + + ## Incomplete Items (if REJECT) + | # | Item | Reason | + |---|------|--------| + | 1 | {Item} | {Reason} | + ``` + + **Summary report format (only if APPROVE):** + ```markdown + # Task Completion Summary + + ## Task + {Original request in 1-2 sentences} + + ## Result + ✅ Complete + + ## Changes + | Type | File | Summary | + |------|------|---------| + | Create | `src/file.ts` | Summary description | + + ## Review Results + | Review | Result | + |--------|--------| + | AI Review | ✅ APPROVE | + | Supervisor | ✅ APPROVE | + + ## Verification Commands + ```bash + npm test + npm run build + ``` + ``` + rules: + - condition: All checks passed + - condition: Requirements unmet, tests failing + + rules: + - condition: all("No AI-specific issues", "All checks passed") + next: COMPLETE + - condition: all("AI-specific issues found", "Requirements unmet, tests failing") + next: fix_both + - condition: any("AI-specific issues found") + next: ai_fix + - condition: any("Requirements unmet, tests failing") + next: supervise_fix + + - name: fix_both + parallel: + - name: ai_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI Reviewer's issues fixed + - condition: No fix needed (verified target files/spec) + - condition: Cannot proceed, insufficient info + instruction_template: | + **This is AI Review iteration {step_iteration}.** + + If this is iteration 2 or later, it means your previous fixes were not actually applied. + **Your belief that you "already fixed it" is wrong.** + + **First, acknowledge:** + - Files you thought were "fixed" are actually not fixed + - Your understanding of previous work is incorrect + - You need to start from zero + + **Required actions:** + 1. Open all flagged files with Read tool (drop assumptions, verify facts) + 2. Search for problem code with grep to confirm it exists + 3. Fix confirmed problems with Edit tool + 4. Run tests to verify (e.g., `npm test`, `./gradlew test`) + 5. Report specifically "what you checked and what you fixed" + + **Report format:** + - ❌ "Already fixed" + - ✅ "Checked file X at L123, found problem Y, fixed to Z" + + **Absolutely prohibited:** + - Reporting "fixed" without opening files + - Judging based on assumptions + - Leaving problems that AI Reviewer REJECTED + + **Handling "no fix needed" (required)** + - Do not claim "no fix needed" unless you can show the checked target file(s) for each AI Review issue + - If an issue involves generated code or spec sync, and you cannot verify the source spec, output the tag for "Cannot proceed, insufficient info" + - When "no fix needed", output the tag for "Cannot proceed, insufficient info" and include the reason + checked scope + + **Required output (include headings)** + ## Files checked + - {path:line} + ## Searches run + - {command and summary} + ## Fixes applied + - {what changed} + ## Test results + - {command and outcome} + + - name: supervise_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: Supervisor's issues fixed + - condition: Cannot proceed, insufficient info + instruction_template: | + Fix the issues pointed out by the supervisor. + + 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} + + rules: + - condition: all("AI Reviewer's issues fixed", "Supervisor's issues fixed") + next: reviewers + - condition: any("No fix needed (verified target files/spec)", "Cannot proceed, insufficient info") + next: implement + + - name: ai_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI Reviewer's issues fixed + next: reviewers + - condition: No fix needed (verified target files/spec) + next: implement + - condition: Cannot proceed, insufficient info + next: implement + instruction_template: | + **This is AI Review iteration {step_iteration}.** + + If this is iteration 2 or later, it means your previous fixes were not actually applied. + **Your belief that you "already fixed it" is wrong.** + + **First, acknowledge:** + - Files you thought were "fixed" are actually not fixed + - Your understanding of previous work is incorrect + - You need to start from zero + + **Required actions:** + 1. Open all flagged files with Read tool (drop assumptions, verify facts) + 2. Search for problem code with grep to confirm it exists + 3. Fix confirmed problems with Edit tool + 4. Run tests to verify (e.g., `npm test`, `./gradlew test`) + 5. Report specifically "what you checked and what you fixed" + + **Report format:** + - ❌ "Already fixed" + - ✅ "Checked file X at L123, found problem Y, fixed to Z" + + **Absolutely prohibited:** + - Reporting "fixed" without opening files + - Judging based on assumptions + - Leaving problems that AI Reviewer REJECTED + + **Handling "no fix needed" (required)** + - Do not claim "no fix needed" unless you can show the checked target file(s) for each AI Review issue + - If an issue involves generated code or spec sync, and you cannot verify the source spec, output the tag for "Cannot proceed, insufficient info" + - When "no fix needed", output the tag for "Cannot proceed, insufficient info" and include the reason + checked scope + + **Required output (include headings)** + ## Files checked + - {path:line} + ## Searches run + - {command and summary} + ## Fixes applied + - {what changed} + ## Test results + - {command and outcome} + + - name: supervise_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: Supervisor's issues fixed + next: reviewers + - condition: Cannot proceed, insufficient info + next: implement + instruction_template: | + Fix the issues pointed out by the supervisor. + + 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} diff --git a/resources/global/en/workflows/simple.yaml b/resources/global/en/workflows/simple.yaml deleted file mode 100644 index d016579..0000000 --- a/resources/global/en/workflows/simple.yaml +++ /dev/null @@ -1,346 +0,0 @@ -# Simple TAKT Workflow -# Plan -> Implement -> AI Review -> Architect Review -> Supervisor Approval -# -# Template Variables (auto-injected by engine): -# {iteration} - Workflow-wide turn count -# {max_iterations} - Maximum iterations allowed -# {step_iteration} - Per-step iteration count -# {task} - Original user request -# {previous_response} - Output from the previous step -# {user_inputs} - Accumulated user inputs during workflow -# {report_dir} - Report directory name -# -# Auto-injected sections (do NOT include in instruction_template): -# ## Workflow Context - iteration, step_iteration, report info -# ## User Request - {task} -# ## Previous Response - {previous_response} -# ## Additional User Inputs - {user_inputs} - -name: simple -description: Simplified development workflow (plan -> implement -> ai_review -> review -> supervise) - -max_iterations: 20 - -initial_step: plan - -steps: - - name: plan - edit: false - agent: ../agents/default/planner.md - report: - name: 00-plan.md - format: | - ```markdown - # Task Plan - - ## Original Request - {User's request as-is} - - ## Analysis Results - - ### Objective - {What needs to be achieved} - - ### Scope - {Impact scope} - - ### Implementation Approach - {How to proceed} - - ## Clarifications Needed (if any) - - {Unclear points or items requiring confirmation} - ``` - allowed_tools: - - Read - - Glob - - Grep - - Write - - Bash - - WebSearch - - WebFetch - rules: - - condition: Requirements are clear and implementable - next: implement - - condition: User is asking a question - next: COMPLETE - - condition: Requirements unclear, insufficient info - next: ABORT - pass_previous_response: true - instruction_template: | - ## Previous Response (when returned from implement) - {previous_response} - - Analyze the task and create an implementation plan. - - **Note:** If returned from implement step (Previous Response exists), - review and revise the plan based on that feedback (replan). - - **Tasks (for implementation tasks):** - 1. Understand the requirements - 2. Identify impact scope - 3. Decide implementation approach - - - name: implement - edit: true - agent: ../agents/default/coder.md - report: - - Scope: 01-coder-scope.md - - Decisions: 02-coder-decisions.md - allowed_tools: - - Read - - Glob - - Grep - - Edit - - Write - - Bash - - WebSearch - - WebFetch - permission_mode: edit - rules: - - condition: Implementation complete - next: ai_review - - condition: No implementation (report only) - next: ai_review - - condition: Cannot proceed, insufficient info - 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. - Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. - - **Scope report format (create at implementation start):** - ```markdown - # Change Scope Declaration - - ## Task - {One-line task summary} - - ## Planned Changes - | Type | File | - |------|------| - | Create | `src/example.ts` | - | Modify | `src/routes.ts` | - - ## Estimated Size - Small / Medium / Large - - ## Impact Scope - - {Affected modules or features} - ``` - - **Decisions report format (on completion, only if decisions were made):** - ```markdown - # Decision Log - - ## 1. {Decision Content} - - **Background**: {Why the decision was needed} - - **Options Considered**: {List of options} - - **Reason**: {Why this option was chosen} - ``` - - **Required output (include headings)** - ## Work done - - {summary of work performed} - ## Changes made - - {summary of code changes} - ## Test results - - {command and outcome} - - **No-implementation handling (required)** - - **Required output (include headings)** - ## Work done - - {summary of work performed} - ## Changes made - - {summary of code changes} - ## Test results - - {command and outcome} - - - name: ai_review - edit: false - agent: ../agents/default/ai-antipattern-reviewer.md - report: - name: 03-ai-review.md - format: | - ```markdown - # AI-Generated Code Review - - ## Result: APPROVE / REJECT - - ## Summary - {One sentence summarizing result} - - ## Verified Items - | Aspect | Result | Notes | - |--------|--------|-------| - | Assumption validity | ✅ | - | - | API/Library existence | ✅ | - | - | Context fit | ✅ | - | - | Scope | ✅ | - | - - ## Issues (if REJECT) - | # | Category | Location | Issue | - |---|----------|----------|-------| - | 1 | Hallucinated API | `src/file.ts:23` | Non-existent method | - ``` - - **Cognitive load reduction rules:** - - No issues -> Summary 1 line + check table only (10 lines or less) - - Issues found -> + Issues in table format (25 lines or less) - allowed_tools: - - Read - - Glob - - Grep - - Write - - WebSearch - - WebFetch - rules: - - condition: No AI-specific issues - next: review - - condition: AI-specific issues found - next: plan - instruction_template: | - Review the code for AI-specific issues: - - Assumption validation - - Plausible but wrong patterns - - Context fit with existing codebase - - Scope creep detection - - - name: review - edit: false - agent: ../agents/default/architecture-reviewer.md - report: - name: 04-architect-review.md - format: | - ```markdown - # Architecture Review - - ## Result: APPROVE / REJECT - - ## Summary - {1-2 sentences summarizing result} - - ## Reviewed Perspectives - - [x] Structure & Design - - [x] Code Quality - - [x] Change Scope - - ## Issues (if REJECT) - | # | Location | Issue | Fix | - |---|----------|-------|-----| - | 1 | `src/file.ts:42` | Issue description | Fix method | - - ## Improvement Suggestions (optional, non-blocking) - - {Future improvement suggestions} - ``` - - **Cognitive load reduction rules:** - - APPROVE + no issues -> Summary only (5 lines or less) - - APPROVE + minor suggestions -> Summary + suggestions (15 lines or less) - - REJECT -> Issues in table format (30 lines or less) - allowed_tools: - - Read - - Glob - - Grep - - Write - - WebSearch - - WebFetch - rules: - - condition: No issues found - next: supervise - - condition: Structural fix required - next: plan - instruction_template: | - Focus on **architecture and design** review. Do NOT review AI-specific issues (that's already done). - - Review the changes and provide feedback. - - **Note:** In simple workflow, IMPROVE judgment is not used. - If there are minor suggestions, use APPROVE + comments. - - - name: supervise - edit: false - agent: ../agents/default/supervisor.md - report: - - Validation: 05-supervisor-validation.md - - Summary: summary.md - allowed_tools: - - Read - - Glob - - Grep - - Write - - Bash - - WebSearch - - WebFetch - rules: - - condition: All checks passed - next: COMPLETE - - condition: Requirements unmet, tests failing - next: plan - instruction_template: | - Run tests, verify the build, and perform final approval. - - **Workflow Overall Review:** - 1. Does the implementation match the plan ({report:00-plan.md})? - 2. Were all review step issues addressed? - 3. Was the original task objective achieved? - - **Review Reports:** Read all reports in Report Directory and - check for any unaddressed improvement suggestions. - - **Validation report format:** - ```markdown - # Final Validation Results - - ## Result: APPROVE / REJECT - - ## Validation Summary - | Item | Status | Verification Method | - |------|--------|---------------------| - | Requirements met | ✅ | Matched against requirements list | - | Tests | ✅ | `npm test` (N passed) | - | Build | ✅ | `npm run build` succeeded | - | Functional check | ✅ | Main flows verified | - - ## Deliverables - - Created: {Created files} - - Modified: {Modified files} - - ## Incomplete Items (if REJECT) - | # | Item | Reason | - |---|------|--------| - | 1 | {Item} | {Reason} | - ``` - - **Summary report format (only if APPROVE):** - ```markdown - # Task Completion Summary - - ## Task - {Original request in 1-2 sentences} - - ## Result - ✅ Complete - - ## Changes - | Type | File | Summary | - |------|------|---------| - | Create | `src/file.ts` | Summary description | - - ## Review Results - | Review | Result | - |--------|--------| - | AI Review | ✅ APPROVE | - | Architect | ✅ APPROVE | - | Supervisor | ✅ APPROVE | - - ## Verification Commands - ```bash - npm test - npm run build - ``` - ``` diff --git a/resources/global/ja/default-categories.yaml b/resources/global/ja/default-categories.yaml new file mode 100644 index 0000000..81b71a0 --- /dev/null +++ b/resources/global/ja/default-categories.yaml @@ -0,0 +1,25 @@ +workflow_categories: + "🚀 クイックスタート": + - minimal + - default + + "🔍 レビュー&修正": + - review-fix-minimal + + "🎨 フロントエンド": + [] + + "⚙️ バックエンド": + [] + + "🔧 フルスタック": + - expert + - expert-cqrs + + "その他": + - research + - magi + - review-only + +show_others_category: true +others_category_name: "その他" diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/workflows/default.yaml index aa94b6b..14f9a4a 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/workflows/default.yaml @@ -78,6 +78,7 @@ steps: - name: implement edit: true agent: ../agents/default/coder.md + session: refresh report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md diff --git a/resources/global/ja/workflows/expert-cqrs.yaml b/resources/global/ja/workflows/expert-cqrs.yaml index 4424d64..ec0b87c 100644 --- a/resources/global/ja/workflows/expert-cqrs.yaml +++ b/resources/global/ja/workflows/expert-cqrs.yaml @@ -95,6 +95,7 @@ steps: - name: implement edit: true agent: ../agents/default/coder.md + session: refresh report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md diff --git a/resources/global/ja/workflows/expert.yaml b/resources/global/ja/workflows/expert.yaml index 02b9c43..7cbf23b 100644 --- a/resources/global/ja/workflows/expert.yaml +++ b/resources/global/ja/workflows/expert.yaml @@ -86,6 +86,7 @@ steps: - name: implement edit: true agent: ../agents/default/coder.md + session: refresh report: - Scope: 01-coder-scope.md - Decisions: 02-coder-decisions.md diff --git a/resources/global/ja/workflows/minimal.yaml b/resources/global/ja/workflows/minimal.yaml new file mode 100644 index 0000000..0cab46b --- /dev/null +++ b/resources/global/ja/workflows/minimal.yaml @@ -0,0 +1,427 @@ +# Simple TAKT Workflow +# Implement -> AI Review -> Supervisor Approval +# (最もシンプルな構成 - plan, architect review, fix ステップなし) +# +# Template Variables (auto-injected): +# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {max_iterations} - Maximum iterations allowed for the workflow +# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {task} - Original user request (auto-injected) +# {previous_response} - Output from the previous step (auto-injected) +# {user_inputs} - Accumulated user inputs during workflow (auto-injected) +# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") + +name: minimal +description: Minimal development workflow (implement -> parallel review -> fix if needed -> complete) + +max_iterations: 20 + +initial_step: implement + +steps: + - name: implement + edit: true + agent: ../agents/default/coder.md + report: + - Scope: 01-coder-scope.md + - Decisions: 02-coder-decisions.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + instruction_template: | + タスクを実装してください。 + Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 + + **Scopeレポートフォーマット(実装開始時に作成):** + ```markdown + # 変更スコープ宣言 + + ## タスク + {タスクの1行要約} + + ## 変更予定 + | 種別 | ファイル | + |------|---------| + | 作成 | `src/example.ts` | + | 変更 | `src/routes.ts` | + + ## 推定規模 + Small / Medium / Large + + ## 影響範囲 + - {影響するモジュールや機能} + ``` + + **Decisionsレポートフォーマット(実装完了時、決定がある場合のみ):** + ```markdown + # 決定ログ + + ## 1. {決定内容} + - **背景**: {なぜ決定が必要だったか} + - **検討した選択肢**: {選択肢リスト} + - **理由**: {選んだ理由} + ``` + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + + rules: + - condition: 実装が完了した + next: reviewers + - condition: 実装を進行できない + next: ABORT + - condition: ユーザーへの確認事項があるためユーザー入力が必要 + next: implement + requires_user_input: true + interactive_only: true + + - name: reviewers + parallel: + - name: ai_review + edit: false + agent: ../agents/default/ai-antipattern-reviewer.md + report: + name: 03-ai-review.md + format: | + ```markdown + # AI生成コードレビュー + + ## 結果: APPROVE / REJECT + + ## サマリー + {1文で結果を要約} + + ## 検証した項目 + | 観点 | 結果 | 備考 | + |------|------|------| + | 仮定の妥当性 | ✅ | - | + | API/ライブラリの実在 | ✅ | - | + | コンテキスト適合 | ✅ | - | + | スコープ | ✅ | - | + + ## 問題点(REJECTの場合) + | # | カテゴリ | 場所 | 問題 | + |---|---------|------|------| + | 1 | 幻覚API | `src/file.ts:23` | 存在しないメソッド | + ``` + + **認知負荷軽減ルール:** + - 問題なし → サマリー1文 + チェック表のみ(10行以内) + - 問題あり → + 問題を表形式で(25行以内) + allowed_tools: + - Read + - Glob + - Grep + - Write + - WebSearch + - WebFetch + instruction_template: | + AI特有の問題についてコードをレビューしてください: + - 仮定の検証 + - もっともらしいが間違っているパターン + - 既存コードベースとの適合性 + - スコープクリープの検出 + rules: + - condition: "AI特有の問題なし" + - condition: "AI特有の問題あり" + + - name: supervise + edit: false + agent: ../agents/default/supervisor.md + report: + - Validation: 05-supervisor-validation.md + - Summary: summary.md + allowed_tools: + - Read + - Glob + - Grep + - Write + - Bash + - WebSearch + - WebFetch + instruction_template: | + テスト実行、ビルド確認、最終承認を行ってください。 + + **ワークフロー全体の確認:** + 1. 実装結果が元の要求を満たしているか + 2. AI Reviewの指摘が対応されているか + 3. 元のタスク目的が達成されているか + + **レポートの確認:** Report Directory内の全レポートを読み、 + 未対応の改善提案がないか確認してください。 + + **Validationレポートフォーマット:** + ```markdown + # 最終検証結果 + + ## 結果: APPROVE / REJECT + + ## 検証サマリー + | 項目 | 状態 | 確認方法 | + |------|------|---------| + | 要求充足 | ✅ | 要求リストと照合 | + | テスト | ✅ | `npm test` (N passed) | + | ビルド | ✅ | `npm run build` 成功 | + | 動作確認 | ✅ | 主要フロー確認 | + + ## 成果物 + - 作成: {作成したファイル} + - 変更: {変更したファイル} + + ## 未完了項目(REJECTの場合) + | # | 項目 | 理由 | + |---|------|------| + | 1 | {項目} | {理由} | + ``` + + **Summaryレポートフォーマット(APPROVEの場合のみ):** + ```markdown + # タスク完了サマリー + + ## タスク + {元の要求を1-2文で} + + ## 結果 + ✅ 完了 + + ## 変更内容 + | 種別 | ファイル | 概要 | + |------|---------|------| + | 作成 | `src/file.ts` | 概要説明 | + + ## レビュー結果 + | レビュー | 結果 | + |---------|------| + | AI Review | ✅ APPROVE | + | Supervisor | ✅ APPROVE | + + ## 確認コマンド + ```bash + npm test + npm run build + ``` + ``` + rules: + - condition: "すべて問題なし" + - condition: "要求未達成、テスト失敗、ビルドエラー" + + rules: + - condition: all("AI特有の問題なし", "すべて問題なし") + next: COMPLETE + - condition: all("AI特有の問題あり", "要求未達成、テスト失敗、ビルドエラー") + next: fix_both + - condition: any("AI特有の問題あり") + next: ai_fix + - condition: any("要求未達成、テスト失敗、ビルドエラー") + next: supervise_fix + + - name: fix_both + parallel: + - name: ai_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI問題の修正完了 + - condition: 修正不要(指摘対象ファイル/仕様の確認済み) + - condition: 判断できない、情報不足 + instruction_template: | + **これは {step_iteration} 回目の AI Review です。** + + 2回目以降は、前回の修正が実際には行われていなかったということです。 + **あなたの「修正済み」という認識が間違っています。** + + **まず認めること:** + - 「修正済み」と思っていたファイルは実際には修正されていない + - 前回の作業内容の認識が間違っている + - ゼロベースで考え直す必要がある + + **必須アクション:** + 1. 指摘された全ファイルを Read tool で開く(思い込みを捨てて事実確認) + 2. 問題箇所を grep で検索して実在を確認する + 3. 確認した問題を Edit tool で修正する + 4. テストを実行して検証する(例: `npm test`, `./gradlew test`) + 5. 「何を確認して、何を修正したか」を具体的に報告する + + **報告フォーマット:** + - ❌ 「既に修正されています」 + - ✅ 「ファイルXのL123を確認した結果、問題Yが存在したため、Zに修正しました」 + + **絶対に禁止:** + - ファイルを開かずに「修正済み」と報告 + - 思い込みで判断 + - AI Reviewer が REJECT した問題の放置 + + **修正不要の扱い(必須)** + - AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない + - 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「判断できない、情報不足」に対応するタグを出力する + - 修正不要の場合は「判断できない、情報不足」に対応するタグを出力し、理由と確認範囲を明記する + + **必須出力(見出しを含める)** + ## 確認したファイル + - {ファイルパス:行番号} + ## 実行した検索 + - {コマンドと要約} + ## 修正内容 + - {変更内容} + ## テスト結果 + - {実行コマンドと結果} + + - name: supervise_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: 監督者の指摘に対する修正が完了した + - condition: 修正を進行できない + instruction_template: | + 監督者からの指摘を修正してください。 + + 監督者は全体を俯瞰した視点から問題を指摘しています。 + 優先度の高い項目から順に対応してください。 + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + ## 証拠 + - {確認したファイル/検索/差分/ログの要点を列挙} + + rules: + - condition: all("AI問題の修正完了", "監督者の指摘に対する修正が完了した") + next: reviewers + - condition: any("修正不要(指摘対象ファイル/仕様の確認済み)", "判断できない、情報不足", "修正を進行できない") + next: implement + + - name: ai_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI問題の修正完了 + next: reviewers + - condition: 修正不要(指摘対象ファイル/仕様の確認済み) + next: implement + - condition: 判断できない、情報不足 + next: implement + instruction_template: | + **これは {step_iteration} 回目の AI Review です。** + + 2回目以降は、前回の修正が実際には行われていなかったということです。 + **あなたの「修正済み」という認識が間違っています。** + + **まず認めること:** + - 「修正済み」と思っていたファイルは実際には修正されていない + - 前回の作業内容の認識が間違っている + - ゼロベースで考え直す必要がある + + **必須アクション:** + 1. 指摘された全ファイルを Read tool で開く(思い込みを捨てて事実確認) + 2. 問題箇所を grep で検索して実在を確認する + 3. 確認した問題を Edit tool で修正する + 4. テストを実行して検証する(例: `npm test`, `./gradlew test`) + 5. 「何を確認して、何を修正したか」を具体的に報告する + + **報告フォーマット:** + - ❌ 「既に修正されています」 + - ✅ 「ファイルXのL123を確認した結果、問題Yが存在したため、Zに修正しました」 + + **絶対に禁止:** + - ファイルを開かずに「修正済み」と報告 + - 思い込みで判断 + - AI Reviewer が REJECT した問題の放置 + + **修正不要の扱い(必須)** + - AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない + - 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「判断できない、情報不足」に対応するタグを出力する + - 修正不要の場合は「判断できない、情報不足」に対応するタグを出力し、理由と確認範囲を明記する + + **必須出力(見出しを含める)** + ## 確認したファイル + - {ファイルパス:行番号} + ## 実行した検索 + - {コマンドと要約} + ## 修正内容 + - {変更内容} + ## テスト結果 + - {実行コマンドと結果} + + - name: supervise_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: 監督者の指摘に対する修正が完了した + next: reviewers + - condition: 修正を進行できない + next: implement + instruction_template: | + 監督者からの指摘を修正してください。 + + 監督者は全体を俯瞰した視点から問題を指摘しています。 + 優先度の高い項目から順に対応してください。 + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + ## 証拠 + - {確認したファイル/検索/差分/ログの要点を列挙} diff --git a/resources/global/ja/workflows/review-fix-minimal.yaml b/resources/global/ja/workflows/review-fix-minimal.yaml new file mode 100644 index 0000000..052f3e8 --- /dev/null +++ b/resources/global/ja/workflows/review-fix-minimal.yaml @@ -0,0 +1,427 @@ +# Review-Fix Minimal TAKT Workflow +# Review -> Fix (if needed) -> Re-review -> Complete +# (レビューから開始、実装ステップなし) +# +# Template Variables (auto-injected): +# {iteration} - Workflow-wide turn count (total steps executed across all agents) +# {max_iterations} - Maximum iterations allowed for the workflow +# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) +# {task} - Original user request (auto-injected) +# {previous_response} - Output from the previous step (auto-injected) +# {user_inputs} - Accumulated user inputs during workflow (auto-injected) +# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") + +name: review-fix-minimal +description: 既存コードのレビューと修正ワークフロー(レビュー開始、実装なし) + +max_iterations: 20 + +initial_step: reviewers + +steps: + - name: implement + edit: true + agent: ../agents/default/coder.md + report: + - Scope: 01-coder-scope.md + - Decisions: 02-coder-decisions.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + instruction_template: | + タスクを実装してください。 + Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 + + **Scopeレポートフォーマット(実装開始時に作成):** + ```markdown + # 変更スコープ宣言 + + ## タスク + {タスクの1行要約} + + ## 変更予定 + | 種別 | ファイル | + |------|---------| + | 作成 | `src/example.ts` | + | 変更 | `src/routes.ts` | + + ## 推定規模 + Small / Medium / Large + + ## 影響範囲 + - {影響するモジュールや機能} + ``` + + **Decisionsレポートフォーマット(実装完了時、決定がある場合のみ):** + ```markdown + # 決定ログ + + ## 1. {決定内容} + - **背景**: {なぜ決定が必要だったか} + - **検討した選択肢**: {選択肢リスト} + - **理由**: {選んだ理由} + ``` + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + + rules: + - condition: 実装が完了した + next: reviewers + - condition: 実装を進行できない + next: ABORT + - condition: ユーザーへの確認事項があるためユーザー入力が必要 + next: implement + requires_user_input: true + interactive_only: true + + - name: reviewers + parallel: + - name: ai_review + edit: false + agent: ../agents/default/ai-antipattern-reviewer.md + report: + name: 03-ai-review.md + format: | + ```markdown + # AI生成コードレビュー + + ## 結果: APPROVE / REJECT + + ## サマリー + {1文で結果を要約} + + ## 検証した項目 + | 観点 | 結果 | 備考 | + |------|------|------| + | 仮定の妥当性 | ✅ | - | + | API/ライブラリの実在 | ✅ | - | + | コンテキスト適合 | ✅ | - | + | スコープ | ✅ | - | + + ## 問題点(REJECTの場合) + | # | カテゴリ | 場所 | 問題 | + |---|---------|------|------| + | 1 | 幻覚API | `src/file.ts:23` | 存在しないメソッド | + ``` + + **認知負荷軽減ルール:** + - 問題なし → サマリー1文 + チェック表のみ(10行以内) + - 問題あり → + 問題を表形式で(25行以内) + allowed_tools: + - Read + - Glob + - Grep + - Write + - WebSearch + - WebFetch + instruction_template: | + AI特有の問題についてコードをレビューしてください: + - 仮定の検証 + - もっともらしいが間違っているパターン + - 既存コードベースとの適合性 + - スコープクリープの検出 + rules: + - condition: "AI特有の問題なし" + - condition: "AI特有の問題あり" + + - name: supervise + edit: false + agent: ../agents/default/supervisor.md + report: + - Validation: 05-supervisor-validation.md + - Summary: summary.md + allowed_tools: + - Read + - Glob + - Grep + - Write + - Bash + - WebSearch + - WebFetch + instruction_template: | + テスト実行、ビルド確認、最終承認を行ってください。 + + **ワークフロー全体の確認:** + 1. 実装結果が元の要求を満たしているか + 2. AI Reviewの指摘が対応されているか + 3. 元のタスク目的が達成されているか + + **レポートの確認:** Report Directory内の全レポートを読み、 + 未対応の改善提案がないか確認してください。 + + **Validationレポートフォーマット:** + ```markdown + # 最終検証結果 + + ## 結果: APPROVE / REJECT + + ## 検証サマリー + | 項目 | 状態 | 確認方法 | + |------|------|---------| + | 要求充足 | ✅ | 要求リストと照合 | + | テスト | ✅ | `npm test` (N passed) | + | ビルド | ✅ | `npm run build` 成功 | + | 動作確認 | ✅ | 主要フロー確認 | + + ## 成果物 + - 作成: {作成したファイル} + - 変更: {変更したファイル} + + ## 未完了項目(REJECTの場合) + | # | 項目 | 理由 | + |---|------|------| + | 1 | {項目} | {理由} | + ``` + + **Summaryレポートフォーマット(APPROVEの場合のみ):** + ```markdown + # タスク完了サマリー + + ## タスク + {元の要求を1-2文で} + + ## 結果 + ✅ 完了 + + ## 変更内容 + | 種別 | ファイル | 概要 | + |------|---------|------| + | 作成 | `src/file.ts` | 概要説明 | + + ## レビュー結果 + | レビュー | 結果 | + |---------|------| + | AI Review | ✅ APPROVE | + | Supervisor | ✅ APPROVE | + + ## 確認コマンド + ```bash + npm test + npm run build + ``` + ``` + rules: + - condition: "すべて問題なし" + - condition: "要求未達成、テスト失敗、ビルドエラー" + + rules: + - condition: all("AI特有の問題なし", "すべて問題なし") + next: COMPLETE + - condition: all("AI特有の問題あり", "要求未達成、テスト失敗、ビルドエラー") + next: fix_both + - condition: any("AI特有の問題あり") + next: ai_fix + - condition: any("要求未達成、テスト失敗、ビルドエラー") + next: supervise_fix + + - name: fix_both + parallel: + - name: ai_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI問題の修正完了 + - condition: 修正不要(指摘対象ファイル/仕様の確認済み) + - condition: 判断できない、情報不足 + instruction_template: | + **これは {step_iteration} 回目の AI Review です。** + + 2回目以降は、前回の修正が実際には行われていなかったということです。 + **あなたの「修正済み」という認識が間違っています。** + + **まず認めること:** + - 「修正済み」と思っていたファイルは実際には修正されていない + - 前回の作業内容の認識が間違っている + - ゼロベースで考え直す必要がある + + **必須アクション:** + 1. 指摘された全ファイルを Read tool で開く(思い込みを捨てて事実確認) + 2. 問題箇所を grep で検索して実在を確認する + 3. 確認した問題を Edit tool で修正する + 4. テストを実行して検証する(例: `npm test`, `./gradlew test`) + 5. 「何を確認して、何を修正したか」を具体的に報告する + + **報告フォーマット:** + - ❌ 「既に修正されています」 + - ✅ 「ファイルXのL123を確認した結果、問題Yが存在したため、Zに修正しました」 + + **絶対に禁止:** + - ファイルを開かずに「修正済み」と報告 + - 思い込みで判断 + - AI Reviewer が REJECT した問題の放置 + + **修正不要の扱い(必須)** + - AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない + - 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「判断できない、情報不足」に対応するタグを出力する + - 修正不要の場合は「判断できない、情報不足」に対応するタグを出力し、理由と確認範囲を明記する + + **必須出力(見出しを含める)** + ## 確認したファイル + - {ファイルパス:行番号} + ## 実行した検索 + - {コマンドと要約} + ## 修正内容 + - {変更内容} + ## テスト結果 + - {実行コマンドと結果} + + - name: supervise_fix_parallel + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: 監督者の指摘に対する修正が完了した + - condition: 修正を進行できない + instruction_template: | + 監督者からの指摘を修正してください。 + + 監督者は全体を俯瞰した視点から問題を指摘しています。 + 優先度の高い項目から順に対応してください。 + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + ## 証拠 + - {確認したファイル/検索/差分/ログの要点を列挙} + + rules: + - condition: all("AI問題の修正完了", "監督者の指摘に対する修正が完了した") + next: reviewers + - condition: any("修正不要(指摘対象ファイル/仕様の確認済み)", "判断できない、情報不足", "修正を進行できない") + next: implement + + - name: ai_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: AI問題の修正完了 + next: reviewers + - condition: 修正不要(指摘対象ファイル/仕様の確認済み) + next: implement + - condition: 判断できない、情報不足 + next: implement + instruction_template: | + **これは {step_iteration} 回目の AI Review です。** + + 2回目以降は、前回の修正が実際には行われていなかったということです。 + **あなたの「修正済み」という認識が間違っています。** + + **まず認めること:** + - 「修正済み」と思っていたファイルは実際には修正されていない + - 前回の作業内容の認識が間違っている + - ゼロベースで考え直す必要がある + + **必須アクション:** + 1. 指摘された全ファイルを Read tool で開く(思い込みを捨てて事実確認) + 2. 問題箇所を grep で検索して実在を確認する + 3. 確認した問題を Edit tool で修正する + 4. テストを実行して検証する(例: `npm test`, `./gradlew test`) + 5. 「何を確認して、何を修正したか」を具体的に報告する + + **報告フォーマット:** + - ❌ 「既に修正されています」 + - ✅ 「ファイルXのL123を確認した結果、問題Yが存在したため、Zに修正しました」 + + **絶対に禁止:** + - ファイルを開かずに「修正済み」と報告 + - 思い込みで判断 + - AI Reviewer が REJECT した問題の放置 + + **修正不要の扱い(必須)** + - AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない + - 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「判断できない、情報不足」に対応するタグを出力する + - 修正不要の場合は「判断できない、情報不足」に対応するタグを出力し、理由と確認範囲を明記する + + **必須出力(見出しを含める)** + ## 確認したファイル + - {ファイルパス:行番号} + ## 実行した検索 + - {コマンドと要約} + ## 修正内容 + - {変更内容} + ## テスト結果 + - {実行コマンドと結果} + + - name: supervise_fix + edit: true + agent: ../agents/default/coder.md + allowed_tools: + - Read + - Glob + - Grep + - Edit + - Write + - Bash + - WebSearch + - WebFetch + permission_mode: edit + pass_previous_response: true + rules: + - condition: 監督者の指摘に対する修正が完了した + next: reviewers + - condition: 修正を進行できない + next: implement + instruction_template: | + 監督者からの指摘を修正してください。 + + 監督者は全体を俯瞰した視点から問題を指摘しています。 + 優先度の高い項目から順に対応してください。 + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + ## 証拠 + - {確認したファイル/検索/差分/ログの要点を列挙} diff --git a/resources/global/ja/workflows/simple.yaml b/resources/global/ja/workflows/simple.yaml deleted file mode 100644 index b33e68b..0000000 --- a/resources/global/ja/workflows/simple.yaml +++ /dev/null @@ -1,336 +0,0 @@ -# Simple TAKT Workflow -# Plan -> Implement -> AI Review -> Architect Review -> Supervisor Approval -# (defaultの簡略版 - improve, fix, ai_fix, security_review, security_fix を削除) -# -# Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total steps executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow -# {step_iteration} - Per-step iteration count (how many times THIS step has been executed) -# {task} - Original user request (auto-injected) -# {previous_response} - Output from the previous step (auto-injected) -# {user_inputs} - Accumulated user inputs during workflow (auto-injected) -# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") - -name: simple -description: Simplified development workflow (plan -> implement -> ai_review -> review -> supervise) - -max_iterations: 20 - -initial_step: plan - -steps: - - name: plan - edit: false - agent: ../agents/default/planner.md - report: - name: 00-plan.md - format: | - ```markdown - # タスク計画 - - ## 元の要求 - {ユーザーの要求をそのまま記載} - - ## 分析結果 - - ### 目的 - {達成すべきこと} - - ### スコープ - {影響範囲} - - ### 実装アプローチ - {どう進めるか} - - ## 確認事項(あれば) - - {不明点や確認が必要な点} - ``` - allowed_tools: - - Read - - Glob - - Grep - - Write - - Bash - - WebSearch - - WebFetch - instruction_template: | - ## Previous Response (implementからの差し戻し時) - {previous_response} - - タスクを分析し、実装方針を立ててください。 - - **注意:** Previous Responseがある場合は差し戻しのため、 - その内容を踏まえて計画を見直してください(replan)。 - - **やること(実装タスクの場合):** - 1. タスクの要件を理解する - 2. 影響範囲を特定する - 3. 実装アプローチを決める - pass_previous_response: true - rules: - - condition: "要件が明確で実装可能" - next: implement - - condition: "ユーザーが質問をしている(実装タスクではない)" - next: COMPLETE - - condition: "要件が不明確、情報不足" - next: ABORT - appendix: | - 確認事項: - - {質問1} - - {質問2} - - - name: implement - edit: true - agent: ../agents/default/coder.md - report: - - Scope: 01-coder-scope.md - - Decisions: 02-coder-decisions.md - allowed_tools: - - Read - - Glob - - Grep - - Edit - - Write - - Bash - - WebSearch - - WebFetch - permission_mode: edit - instruction_template: | - planステップで立てた計画に従って実装してください。 - 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 - Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 - - **Scopeレポートフォーマット(実装開始時に作成):** - ```markdown - # 変更スコープ宣言 - - ## タスク - {タスクの1行要約} - - ## 変更予定 - | 種別 | ファイル | - |------|---------| - | 作成 | `src/example.ts` | - | 変更 | `src/routes.ts` | - - ## 推定規模 - Small / Medium / Large - - ## 影響範囲 - - {影響するモジュールや機能} - ``` - - **Decisionsレポートフォーマット(実装完了時、決定がある場合のみ):** - ```markdown - # 決定ログ - - ## 1. {決定内容} - - **背景**: {なぜ決定が必要だったか} - - **検討した選択肢**: {選択肢リスト} - - **理由**: {選んだ理由} - ``` - - **必須出力(見出しを含める)** - ## 作業結果 - - {実施内容の要約} - ## 変更内容 - - {変更内容の要約} - ## テスト結果 - - {実行コマンドと結果} - - rules: - - condition: 実装が完了した - next: ai_review - - condition: 実装未着手(レポートのみ) - next: ai_review - - condition: 実装を進行できない - next: ai_review - - condition: ユーザー入力が必要 - next: implement - requires_user_input: true - interactive_only: true - - - name: ai_review - edit: false - agent: ../agents/default/ai-antipattern-reviewer.md - report: - name: 03-ai-review.md - format: | - ```markdown - # AI生成コードレビュー - - ## 結果: APPROVE / REJECT - - ## サマリー - {1文で結果を要約} - - ## 検証した項目 - | 観点 | 結果 | 備考 | - |------|------|------| - | 仮定の妥当性 | ✅ | - | - | API/ライブラリの実在 | ✅ | - | - | コンテキスト適合 | ✅ | - | - | スコープ | ✅ | - | - - ## 問題点(REJECTの場合) - | # | カテゴリ | 場所 | 問題 | - |---|---------|------|------| - | 1 | 幻覚API | `src/file.ts:23` | 存在しないメソッド | - ``` - - **認知負荷軽減ルール:** - - 問題なし → サマリー1文 + チェック表のみ(10行以内) - - 問題あり → + 問題を表形式で(25行以内) - allowed_tools: - - Read - - Glob - - Grep - - Write - - WebSearch - - WebFetch - instruction_template: | - AI特有の問題についてコードをレビューしてください: - - 仮定の検証 - - もっともらしいが間違っているパターン - - 既存コードベースとの適合性 - - スコープクリープの検出 - rules: - - condition: "AI特有の問題なし" - next: review - - condition: "AI特有の問題あり" - next: plan - - - name: review - edit: false - agent: ../agents/default/architecture-reviewer.md - report: - name: 04-architect-review.md - format: | - ```markdown - # アーキテクチャレビュー - - ## 結果: APPROVE / REJECT - - ## サマリー - {1-2文で結果を要約} - - ## 確認した観点 - - [x] 構造・設計 - - [x] コード品質 - - [x] 変更スコープ - - ## 問題点(REJECTの場合) - | # | 場所 | 問題 | 修正案 | - |---|------|------|--------| - | 1 | `src/file.ts:42` | 問題の説明 | 修正方法 | - - ## 改善提案(任意・ブロッキングではない) - - {将来的な改善提案} - ``` - - **認知負荷軽減ルール:** - - APPROVE + 問題なし → サマリーのみ(5行以内) - - APPROVE + 軽微な提案 → サマリー + 改善提案(15行以内) - - REJECT → 問題点を表形式で(30行以内) - allowed_tools: - - Read - - Glob - - Grep - - Write - - WebSearch - - WebFetch - instruction_template: | - **アーキテクチャと設計**のレビューに集中してください。AI特有の問題はレビューしないでください(前のステップで完了済み)。 - - 変更をレビューしてフィードバックを提供してください。 - - **注意:** simpleワークフローではIMPROVE判定は使用しません。 - 軽微な改善提案がある場合は APPROVE + コメントとしてください。 - rules: - - condition: "問題なし" - next: supervise - - condition: "構造的な修正必要" - next: plan - - - name: supervise - edit: false - agent: ../agents/default/supervisor.md - report: - - Validation: 05-supervisor-validation.md - - Summary: summary.md - allowed_tools: - - Read - - Glob - - Grep - - Write - - Bash - - WebSearch - - WebFetch - instruction_template: | - テスト実行、ビルド確認、最終承認を行ってください。 - - **ワークフロー全体の確認:** - 1. 計画({report:00-plan.md})と実装結果が一致しているか - 2. 各レビューステップの指摘が対応されているか - 3. 元のタスク目的が達成されているか - - **レポートの確認:** Report Directory内の全レポートを読み、 - 未対応の改善提案がないか確認してください。 - - **Validationレポートフォーマット:** - ```markdown - # 最終検証結果 - - ## 結果: APPROVE / REJECT - - ## 検証サマリー - | 項目 | 状態 | 確認方法 | - |------|------|---------| - | 要求充足 | ✅ | 要求リストと照合 | - | テスト | ✅ | `npm test` (N passed) | - | ビルド | ✅ | `npm run build` 成功 | - | 動作確認 | ✅ | 主要フロー確認 | - - ## 成果物 - - 作成: {作成したファイル} - - 変更: {変更したファイル} - - ## 未完了項目(REJECTの場合) - | # | 項目 | 理由 | - |---|------|------| - | 1 | {項目} | {理由} | - ``` - - **Summaryレポートフォーマット(APPROVEの場合のみ):** - ```markdown - # タスク完了サマリー - - ## タスク - {元の要求を1-2文で} - - ## 結果 - ✅ 完了 - - ## 変更内容 - | 種別 | ファイル | 概要 | - |------|---------|------| - | 作成 | `src/file.ts` | 概要説明 | - - ## レビュー結果 - | レビュー | 結果 | - |---------|------| - | AI Review | ✅ APPROVE | - | Architect | ✅ APPROVE | - | Supervisor | ✅ APPROVE | - - ## 確認コマンド - ```bash - npm test - npm run build - ``` - ``` - rules: - - condition: "すべて問題なし" - next: COMPLETE - - condition: "要求未達成、テスト失敗、ビルドエラー" - next: plan diff --git a/resources/project/dotgitignore b/resources/project/dotgitignore index c902ffd..41d2f61 100644 --- a/resources/project/dotgitignore +++ b/resources/project/dotgitignore @@ -3,6 +3,7 @@ logs/ reports/ completed/ tasks/ +worktrees/ worktree-meta/ clone-meta/ worktree-sessions/ diff --git a/src/__tests__/bookmark.test.ts b/src/__tests__/bookmark.test.ts new file mode 100644 index 0000000..af45afc --- /dev/null +++ b/src/__tests__/bookmark.test.ts @@ -0,0 +1,104 @@ +/** + * Tests for workflow bookmark functionality + */ + +import { describe, it, expect } from 'vitest'; +import { handleKeyInput } from '../shared/prompt/index.js'; +import { applyBookmarks, type SelectionOption } from '../features/workflowSelection/index.js'; + +describe('handleKeyInput - bookmark action', () => { + const totalItems = 4; + const optionCount = 3; + const hasCancelOption = true; + + it('should return bookmark action for b key', () => { + const result = handleKeyInput('b', 1, totalItems, hasCancelOption, optionCount); + expect(result).toEqual({ action: 'bookmark', selectedIndex: 1 }); + }); + + it('should return bookmark action with current index', () => { + const result = handleKeyInput('b', 0, totalItems, hasCancelOption, optionCount); + expect(result).toEqual({ action: 'bookmark', selectedIndex: 0 }); + }); + + it('should return bookmark action at last option index', () => { + const result = handleKeyInput('b', 2, totalItems, hasCancelOption, optionCount); + expect(result).toEqual({ action: 'bookmark', selectedIndex: 2 }); + }); + + it('should not interfere with existing key bindings', () => { + // j/k should still work + expect(handleKeyInput('j', 0, totalItems, hasCancelOption, optionCount)).toEqual({ action: 'move', newIndex: 1 }); + expect(handleKeyInput('k', 1, totalItems, hasCancelOption, optionCount)).toEqual({ action: 'move', newIndex: 0 }); + // Enter should still confirm + expect(handleKeyInput('\r', 0, totalItems, hasCancelOption, optionCount)).toEqual({ action: 'confirm', selectedIndex: 0 }); + // Esc should still cancel + expect(handleKeyInput('\x1B', 0, totalItems, hasCancelOption, optionCount)).toEqual({ action: 'cancel', cancelIndex: 3 }); + }); +}); + +describe('applyBookmarks', () => { + const options: SelectionOption[] = [ + { label: 'alpha', value: 'alpha' }, + { label: 'beta', value: 'beta' }, + { label: 'gamma', value: 'gamma' }, + { label: 'delta', value: 'delta' }, + ]; + + it('should move bookmarked items to the top with ★ prefix', () => { + const result = applyBookmarks(options, ['gamma']); + expect(result[0]!.label).toBe('★ gamma'); + expect(result[0]!.value).toBe('gamma'); + expect(result).toHaveLength(4); + }); + + it('should preserve order of non-bookmarked items', () => { + const result = applyBookmarks(options, ['gamma']); + const rest = result.slice(1); + expect(rest.map((o) => o.value)).toEqual(['alpha', 'beta', 'delta']); + }); + + it('should handle multiple bookmarks preserving their relative order', () => { + const result = applyBookmarks(options, ['delta', 'alpha']); + // Bookmarked items appear first, in the order they appear in options (not in bookmarks array) + expect(result[0]!.value).toBe('alpha'); + expect(result[0]!.label).toBe('★ alpha'); + expect(result[1]!.value).toBe('delta'); + expect(result[1]!.label).toBe('★ delta'); + expect(result.slice(2).map((o) => o.value)).toEqual(['beta', 'gamma']); + }); + + it('should return unchanged options when no bookmarks', () => { + const result = applyBookmarks(options, []); + expect(result).toEqual(options); + }); + + it('should ignore bookmarks that do not match any option', () => { + const result = applyBookmarks(options, ['nonexistent']); + expect(result).toEqual(options); + }); + + it('should not mutate original options', () => { + const original = options.map((o) => ({ ...o })); + applyBookmarks(options, ['gamma']); + expect(options).toEqual(original); + }); + + it('should work with category-prefixed values', () => { + const categoryOptions: SelectionOption[] = [ + { label: 'simple', value: 'simple' }, + { label: '📁 frontend/', value: '__category__:frontend' }, + { label: '📁 backend/', value: '__category__:backend' }, + ]; + // Only workflow values should match; categories are not bookmarkable + const result = applyBookmarks(categoryOptions, ['simple']); + expect(result[0]!.label).toBe('★ simple'); + expect(result.slice(1).map((o) => o.value)).toEqual(['__category__:frontend', '__category__:backend']); + }); + + it('should handle all items bookmarked', () => { + const result = applyBookmarks(options, ['alpha', 'beta', 'gamma', 'delta']); + expect(result.every((o) => o.label.startsWith('★ '))).toBe(true); + expect(result.map((o) => o.value)).toEqual(['alpha', 'beta', 'gamma', 'delta']); + }); +}); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 287842c..1c27b5f 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -208,10 +208,10 @@ describe('loadWorkflow (builtin fallback)', () => { expect(workflow).toBeNull(); }); - it('should load builtin workflows like simple, research', () => { - const simple = loadWorkflow('simple', process.cwd()); - expect(simple).not.toBeNull(); - expect(simple!.name).toBe('simple'); + it('should load builtin workflows like minimal, research', () => { + const minimal = loadWorkflow('minimal', process.cwd()); + expect(minimal).not.toBeNull(); + expect(minimal!.name).toBe('minimal'); const research = loadWorkflow('research', process.cwd()); expect(research).not.toBeNull(); @@ -236,7 +236,7 @@ describe('listWorkflows (builtin fallback)', () => { it('should include builtin workflows', () => { const workflows = listWorkflows(testDir); expect(workflows).toContain('default'); - expect(workflows).toContain('simple'); + expect(workflows).toContain('minimal'); }); it('should return sorted list', () => { @@ -263,7 +263,7 @@ describe('loadAllWorkflows (builtin fallback)', () => { it('should include builtin workflows in the map', () => { const workflows = loadAllWorkflows(testDir); expect(workflows.has('default')).toBe(true); - expect(workflows.has('simple')).toBe(true); + expect(workflows.has('minimal')).toBe(true); }); }); diff --git a/src/__tests__/it-pipeline-modes.test.ts b/src/__tests__/it-pipeline-modes.test.ts index 88be12f..4027c24 100644 --- a/src/__tests__/it-pipeline-modes.test.ts +++ b/src/__tests__/it-pipeline-modes.test.ts @@ -272,18 +272,16 @@ describe('Pipeline Modes IT: --task + --workflow name (builtin)', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should load and execute builtin simple workflow by name', async () => { + it('should load and execute builtin minimal workflow by name', async () => { setMockScenario([ - { agent: 'planner', status: 'done', content: '[PLAN:1]\n\nRequirements are clear.' }, { agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' }, - { agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:1]\n\nNo issues.' }, - { agent: 'architecture-reviewer', status: 'done', content: '[REVIEW:1]\n\nNo issues found.' }, - { agent: 'supervisor', status: 'done', content: '[SUPERVISE:1]\n\nAll checks passed.' }, + { agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:0]\n\nNo AI-specific issues.' }, + { agent: 'supervisor', status: 'done', content: '[SUPERVISE:0]\n\nAll checks passed.' }, ]); const exitCode = await executePipeline({ task: 'Add a feature', - workflow: 'simple', + workflow: 'minimal', autoPr: false, skipGit: true, cwd: testDir, diff --git a/src/__tests__/it-pipeline.test.ts b/src/__tests__/it-pipeline.test.ts index 13f2d08..2c12f7f 100644 --- a/src/__tests__/it-pipeline.test.ts +++ b/src/__tests__/it-pipeline.test.ts @@ -221,20 +221,18 @@ describe('Pipeline Integration Tests', () => { }); it('should complete pipeline with workflow name + skip-git + mock scenario', async () => { - // Use builtin 'simple' workflow + // Use builtin 'minimal' workflow // agent field: extractAgentName result (from .md filename) // tag in content: [STEP_NAME:N] where STEP_NAME is the step name uppercased setMockScenario([ - { agent: 'planner', status: 'done', content: '[PLAN:1]\n\nRequirements are clear and implementable.' }, { agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' }, - { agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:1]\n\nNo AI-specific issues.' }, - { agent: 'architecture-reviewer', status: 'done', content: '[REVIEW:1]\n\nNo issues found.' }, - { agent: 'supervisor', status: 'done', content: '[SUPERVISE:1]\n\nAll checks passed.' }, + { agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:0]\n\nNo AI-specific issues.' }, + { agent: 'supervisor', status: 'done', content: '[SUPERVISE:0]\n\nAll checks passed.' }, ]); const exitCode = await executePipeline({ task: 'Add a hello world function', - workflow: 'simple', + workflow: 'minimal', autoPr: false, skipGit: true, cwd: testDir, diff --git a/src/__tests__/it-workflow-loader.test.ts b/src/__tests__/it-workflow-loader.test.ts index 74041b9..82365d1 100644 --- a/src/__tests__/it-workflow-loader.test.ts +++ b/src/__tests__/it-workflow-loader.test.ts @@ -44,7 +44,7 @@ describe('Workflow Loader IT: builtin workflow loading', () => { rmSync(testDir, { recursive: true, force: true }); }); - const builtinNames = ['default', 'simple', 'expert', 'expert-cqrs', 'research', 'magi', 'review-only']; + const builtinNames = ['default', 'minimal', 'expert', 'expert-cqrs', 'research', 'magi', 'review-only', 'review-fix-minimal']; for (const name of builtinNames) { it(`should load builtin workflow: ${name}`, () => { @@ -119,7 +119,7 @@ describe('Workflow Loader IT: agent path resolution', () => { }); it('should resolve relative agent paths from workflow YAML location', () => { - const config = loadWorkflow('simple', testDir); + const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); for (const step of config!.steps) { @@ -186,7 +186,7 @@ describe('Workflow Loader IT: rule syntax parsing', () => { }); it('should parse standard rules with next step', () => { - const config = loadWorkflow('simple', testDir); + const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); const planStep = config!.steps.find((s) => s.name === 'plan'); @@ -214,14 +214,14 @@ describe('Workflow Loader IT: workflow config validation', () => { }); it('should set max_iterations from YAML', () => { - const config = loadWorkflow('simple', testDir); + const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); expect(typeof config!.maxIterations).toBe('number'); expect(config!.maxIterations).toBeGreaterThan(0); }); it('should set initial_step from YAML', () => { - const config = loadWorkflow('simple', testDir); + const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); expect(typeof config!.initialStep).toBe('string'); @@ -253,7 +253,7 @@ describe('Workflow Loader IT: workflow config validation', () => { }); it('should set passPreviousResponse from YAML', () => { - const config = loadWorkflow('simple', testDir); + const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); // At least some steps should have passPreviousResponse set @@ -320,7 +320,7 @@ describe('Workflow Loader IT: report config loading', () => { }); it('should load single report config', () => { - const config = loadWorkflow('simple', testDir); + const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); // simple workflow: plan step has a report config diff --git a/src/__tests__/it-workflow-patterns.test.ts b/src/__tests__/it-workflow-patterns.test.ts index 2b6cbc0..a725bec 100644 --- a/src/__tests__/it-workflow-patterns.test.ts +++ b/src/__tests__/it-workflow-patterns.test.ts @@ -70,7 +70,7 @@ function createEngine(config: WorkflowConfig, dir: string, task: string): Workfl }); } -describe('Workflow Patterns IT: simple workflow', () => { +describe('Workflow Patterns IT: minimal workflow', () => { let testDir: string; beforeEach(() => { @@ -83,30 +83,28 @@ describe('Workflow Patterns IT: simple workflow', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should complete: plan → implement → ai_review → review → supervise → COMPLETE', async () => { - const config = loadWorkflow('simple', testDir); + it('should complete: implement → reviewers (parallel: ai_review + supervise) → COMPLETE', async () => { + const config = loadWorkflow('minimal', testDir); expect(config).not.toBeNull(); setMockScenario([ - { agent: 'planner', status: 'done', content: '[PLAN:1]\n\nRequirements are clear.' }, - { agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' }, - { agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:1]\n\nNo AI-specific issues.' }, - { agent: 'architecture-reviewer', status: 'done', content: '[REVIEW:1]\n\nNo issues found.' }, - { agent: 'supervisor', status: 'done', content: '[SUPERVISE:1]\n\nAll checks passed.' }, + { agent: 'coder', status: 'done', content: '[IMPLEMENT:0]\n\nImplementation complete.' }, + { agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:0]\n\nNo AI-specific issues.' }, + { agent: 'supervisor', status: 'done', content: '[SUPERVISE:0]\n\nAll checks passed.' }, ]); const engine = createEngine(config!, testDir, 'Test task'); const state = await engine.run(); expect(state.status).toBe('completed'); - expect(state.iteration).toBe(5); + expect(state.iteration).toBe(3); }); - it('should ABORT when plan returns rule 3 (requirements unclear)', async () => { - const config = loadWorkflow('simple', testDir); + it('should ABORT when implement cannot proceed', async () => { + const config = loadWorkflow('minimal', testDir); setMockScenario([ - { agent: 'planner', status: 'done', content: '[PLAN:3]\n\nRequirements unclear.' }, + { agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nCannot proceed, insufficient info.' }, ]); const engine = createEngine(config!, testDir, 'Vague task'); @@ -116,19 +114,6 @@ describe('Workflow Patterns IT: simple workflow', () => { expect(state.iteration).toBe(1); }); - it('should COMPLETE when plan detects a question (rule 2)', async () => { - const config = loadWorkflow('simple', testDir); - - setMockScenario([ - { agent: 'planner', status: 'done', content: '[PLAN:2]\n\nUser is asking a question.' }, - ]); - - const engine = createEngine(config!, testDir, 'What is X?'); - const state = await engine.run(); - - expect(state.status).toBe('completed'); - expect(state.iteration).toBe(1); - }); }); describe('Workflow Patterns IT: default workflow (parallel reviewers)', () => { diff --git a/src/__tests__/workflow-categories.test.ts b/src/__tests__/workflow-categories.test.ts new file mode 100644 index 0000000..4e922f7 --- /dev/null +++ b/src/__tests__/workflow-categories.test.ts @@ -0,0 +1,301 @@ +/** + * Tests for workflow category (subdirectory) support — Issue #85 + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { + listWorkflows, + listWorkflowEntries, + loadAllWorkflows, + loadWorkflow, +} from '../infra/config/loaders/workflowLoader.js'; +import type { WorkflowDirEntry } from '../infra/config/loaders/workflowLoader.js'; +import { + buildWorkflowSelectionItems, + buildTopLevelSelectOptions, + parseCategorySelection, + buildCategoryWorkflowOptions, + type WorkflowSelectionItem, +} from '../features/workflowSelection/index.js'; + +const SAMPLE_WORKFLOW = `name: test-workflow +description: Test workflow +initial_step: step1 +max_iterations: 1 + +steps: + - name: step1 + agent: coder + instruction: "{task}" +`; + +function createWorkflow(dir: string, name: string, content?: string): void { + writeFileSync(join(dir, `${name}.yaml`), content ?? SAMPLE_WORKFLOW); +} + +describe('workflow categories - directory scanning', () => { + let tempDir: string; + let workflowsDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + workflowsDir = join(tempDir, '.takt', 'workflows'); + mkdirSync(workflowsDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should discover root-level workflows', () => { + createWorkflow(workflowsDir, 'simple'); + createWorkflow(workflowsDir, 'advanced'); + + const workflows = listWorkflows(tempDir); + expect(workflows).toContain('simple'); + expect(workflows).toContain('advanced'); + }); + + it('should discover workflows in subdirectories with category prefix', () => { + const frontendDir = join(workflowsDir, 'frontend'); + mkdirSync(frontendDir); + createWorkflow(frontendDir, 'react'); + createWorkflow(frontendDir, 'vue'); + + const workflows = listWorkflows(tempDir); + expect(workflows).toContain('frontend/react'); + expect(workflows).toContain('frontend/vue'); + }); + + it('should discover both root-level and categorized workflows', () => { + createWorkflow(workflowsDir, 'simple'); + + const frontendDir = join(workflowsDir, 'frontend'); + mkdirSync(frontendDir); + createWorkflow(frontendDir, 'react'); + + const backendDir = join(workflowsDir, 'backend'); + mkdirSync(backendDir); + createWorkflow(backendDir, 'api'); + + const workflows = listWorkflows(tempDir); + expect(workflows).toContain('simple'); + expect(workflows).toContain('frontend/react'); + expect(workflows).toContain('backend/api'); + }); + + it('should not scan deeper than 1 level', () => { + const deepDir = join(workflowsDir, 'category', 'subcategory'); + mkdirSync(deepDir, { recursive: true }); + createWorkflow(deepDir, 'deep'); + + const workflows = listWorkflows(tempDir); + // category/subcategory should be treated as a directory entry, not scanned further + expect(workflows).not.toContain('category/subcategory/deep'); + // Only 1-level: category/deep would not exist since deep.yaml is in subcategory + expect(workflows).not.toContain('deep'); + }); +}); + +describe('workflow categories - listWorkflowEntries', () => { + let tempDir: string; + let workflowsDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + workflowsDir = join(tempDir, '.takt', 'workflows'); + mkdirSync(workflowsDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should return entries with category information', () => { + createWorkflow(workflowsDir, 'simple'); + + const frontendDir = join(workflowsDir, 'frontend'); + mkdirSync(frontendDir); + createWorkflow(frontendDir, 'react'); + + const entries = listWorkflowEntries(tempDir); + const simpleEntry = entries.find((e) => e.name === 'simple'); + const reactEntry = entries.find((e) => e.name === 'frontend/react'); + + expect(simpleEntry).toBeDefined(); + expect(simpleEntry!.category).toBeUndefined(); + + expect(reactEntry).toBeDefined(); + expect(reactEntry!.category).toBe('frontend'); + }); +}); + +describe('workflow categories - loadAllWorkflows', () => { + let tempDir: string; + let workflowsDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + workflowsDir = join(tempDir, '.takt', 'workflows'); + mkdirSync(workflowsDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should load categorized workflows with qualified names as keys', () => { + const frontendDir = join(workflowsDir, 'frontend'); + mkdirSync(frontendDir); + createWorkflow(frontendDir, 'react'); + + const workflows = loadAllWorkflows(tempDir); + expect(workflows.has('frontend/react')).toBe(true); + }); +}); + +describe('workflow categories - loadWorkflow', () => { + let tempDir: string; + let workflowsDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + workflowsDir = join(tempDir, '.takt', 'workflows'); + mkdirSync(workflowsDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should load workflow by category/name identifier', () => { + const frontendDir = join(workflowsDir, 'frontend'); + mkdirSync(frontendDir); + createWorkflow(frontendDir, 'react'); + + const workflow = loadWorkflow('frontend/react', tempDir); + expect(workflow).not.toBeNull(); + expect(workflow!.name).toBe('test-workflow'); + }); + + it('should return null for non-existent category/name', () => { + const workflow = loadWorkflow('nonexistent/workflow', tempDir); + expect(workflow).toBeNull(); + }); + + it('should support .yml extension in subdirectories', () => { + const backendDir = join(workflowsDir, 'backend'); + mkdirSync(backendDir); + writeFileSync(join(backendDir, 'api.yml'), SAMPLE_WORKFLOW); + + const workflow = loadWorkflow('backend/api', tempDir); + expect(workflow).not.toBeNull(); + }); +}); + +describe('buildWorkflowSelectionItems', () => { + it('should separate root workflows and categories', () => { + const entries: WorkflowDirEntry[] = [ + { name: 'simple', path: '/tmp/simple.yaml' }, + { name: 'frontend/react', path: '/tmp/frontend/react.yaml', category: 'frontend' }, + { name: 'frontend/vue', path: '/tmp/frontend/vue.yaml', category: 'frontend' }, + { name: 'backend/api', path: '/tmp/backend/api.yaml', category: 'backend' }, + ]; + + const items = buildWorkflowSelectionItems(entries); + + const workflows = items.filter((i) => i.type === 'workflow'); + const categories = items.filter((i) => i.type === 'category'); + + expect(workflows).toHaveLength(1); + expect(workflows[0]!.name).toBe('simple'); + + expect(categories).toHaveLength(2); + const frontend = categories.find((c) => c.name === 'frontend'); + expect(frontend).toBeDefined(); + expect(frontend!.type === 'category' && frontend!.workflows).toEqual(['frontend/react', 'frontend/vue']); + + const backend = categories.find((c) => c.name === 'backend'); + expect(backend).toBeDefined(); + expect(backend!.type === 'category' && backend!.workflows).toEqual(['backend/api']); + }); + + it('should sort items alphabetically', () => { + const entries: WorkflowDirEntry[] = [ + { name: 'zebra', path: '/tmp/zebra.yaml' }, + { name: 'alpha', path: '/tmp/alpha.yaml' }, + { name: 'misc/playground', path: '/tmp/misc/playground.yaml', category: 'misc' }, + ]; + + const items = buildWorkflowSelectionItems(entries); + const names = items.map((i) => i.name); + expect(names).toEqual(['alpha', 'misc', 'zebra']); + }); + + it('should return empty array for empty input', () => { + const items = buildWorkflowSelectionItems([]); + expect(items).toEqual([]); + }); +}); + +describe('2-stage category selection helpers', () => { + const items: WorkflowSelectionItem[] = [ + { type: 'workflow', name: 'simple' }, + { type: 'category', name: 'frontend', workflows: ['frontend/react', 'frontend/vue'] }, + { type: 'category', name: 'backend', workflows: ['backend/api'] }, + ]; + + describe('buildTopLevelSelectOptions', () => { + it('should encode categories with prefix in value', () => { + const options = buildTopLevelSelectOptions(items, ''); + const categoryOption = options.find((o) => o.label.includes('frontend')); + expect(categoryOption).toBeDefined(); + expect(categoryOption!.value).toBe('__category__:frontend'); + }); + + it('should mark current workflow', () => { + const options = buildTopLevelSelectOptions(items, 'simple'); + const simpleOption = options.find((o) => o.value === 'simple'); + expect(simpleOption!.label).toContain('(current)'); + }); + + it('should mark category containing current workflow', () => { + const options = buildTopLevelSelectOptions(items, 'frontend/react'); + const frontendOption = options.find((o) => o.value === '__category__:frontend'); + expect(frontendOption!.label).toContain('(current)'); + }); + }); + + describe('parseCategorySelection', () => { + it('should return category name for category selection', () => { + expect(parseCategorySelection('__category__:frontend')).toBe('frontend'); + }); + + it('should return null for direct workflow selection', () => { + expect(parseCategorySelection('simple')).toBeNull(); + }); + }); + + describe('buildCategoryWorkflowOptions', () => { + it('should return options for workflows in a category', () => { + const options = buildCategoryWorkflowOptions(items, 'frontend', ''); + expect(options).not.toBeNull(); + expect(options).toHaveLength(2); + expect(options![0]!.value).toBe('frontend/react'); + expect(options![0]!.label).toBe('react'); + }); + + it('should mark current workflow in category', () => { + const options = buildCategoryWorkflowOptions(items, 'frontend', 'frontend/vue'); + const vueOption = options!.find((o) => o.value === 'frontend/vue'); + expect(vueOption!.label).toContain('(current)'); + }); + + it('should return null for non-existent category', () => { + expect(buildCategoryWorkflowOptions(items, 'nonexistent', '')).toBeNull(); + }); + }); +}); diff --git a/src/__tests__/workflow-category-config.test.ts b/src/__tests__/workflow-category-config.test.ts new file mode 100644 index 0000000..ffc6459 --- /dev/null +++ b/src/__tests__/workflow-category-config.test.ts @@ -0,0 +1,214 @@ +/** + * Tests for workflow category configuration loading and building + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { randomUUID } from 'node:crypto'; +import type { WorkflowConfig } from '../core/models/index.js'; + +const pathsState = vi.hoisted(() => ({ + globalConfigPath: '', + projectConfigPath: '', + resourcesDir: '', +})); + +vi.mock('../infra/config/paths.js', async (importOriginal) => { + const original = await importOriginal() as Record; + return { + ...original, + getGlobalConfigPath: () => pathsState.globalConfigPath, + getProjectConfigPath: () => pathsState.projectConfigPath, + }; +}); + +vi.mock('../infra/resources/index.js', async (importOriginal) => { + const original = await importOriginal() as Record; + return { + ...original, + getLanguageResourcesDir: () => pathsState.resourcesDir, + }; +}); + +vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => { + const original = await importOriginal() as Record; + return { + ...original, + getLanguage: () => 'en', + }; +}); + +const { + getWorkflowCategories, + loadDefaultCategories, + buildCategorizedWorkflows, + findWorkflowCategories, +} = await import('../infra/config/loaders/workflowCategories.js'); + +function writeYaml(path: string, content: string): void { + writeFileSync(path, content.trim() + '\n', 'utf-8'); +} + +function createWorkflowMap(names: string[]): Map { + const workflows = new Map(); + for (const name of names) { + workflows.set(name, { + name, + steps: [], + initialStep: 'start', + maxIterations: 1, + }); + } + return workflows; +} + +describe('workflow category config loading', () => { + let testDir: string; + let resourcesDir: string; + let globalConfigPath: string; + let projectConfigPath: string; + + beforeEach(() => { + testDir = join(tmpdir(), `takt-cat-config-${randomUUID()}`); + resourcesDir = join(testDir, 'resources'); + globalConfigPath = join(testDir, 'global-config.yaml'); + projectConfigPath = join(testDir, 'project-config.yaml'); + + mkdirSync(resourcesDir, { recursive: true }); + pathsState.globalConfigPath = globalConfigPath; + pathsState.projectConfigPath = projectConfigPath; + pathsState.resourcesDir = resourcesDir; + + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + it('should load default categories when no configs define workflow_categories', () => { + writeYaml(join(resourcesDir, 'default-categories.yaml'), ` +workflow_categories: + Default: + - simple +show_others_category: true +others_category_name: "Others" +`); + + const config = getWorkflowCategories(testDir); + expect(config).not.toBeNull(); + expect(config!.workflowCategories).toEqual({ Default: ['simple'] }); + }); + + it('should prefer project config over default when workflow_categories is defined', () => { + writeYaml(join(resourcesDir, 'default-categories.yaml'), ` +workflow_categories: + Default: + - simple +`); + + writeYaml(projectConfigPath, ` +workflow_categories: + Project: + - custom +show_others_category: false +`); + + const config = getWorkflowCategories(testDir); + expect(config).not.toBeNull(); + expect(config!.workflowCategories).toEqual({ Project: ['custom'] }); + expect(config!.showOthersCategory).toBe(false); + }); + + it('should prefer user config over project config when workflow_categories is defined', () => { + writeYaml(join(resourcesDir, 'default-categories.yaml'), ` +workflow_categories: + Default: + - simple +`); + + writeYaml(projectConfigPath, ` +workflow_categories: + Project: + - custom +`); + + writeYaml(globalConfigPath, ` +workflow_categories: + User: + - preferred +`); + + const config = getWorkflowCategories(testDir); + expect(config).not.toBeNull(); + expect(config!.workflowCategories).toEqual({ User: ['preferred'] }); + }); + + it('should ignore configs without workflow_categories and fall back to default', () => { + writeYaml(join(resourcesDir, 'default-categories.yaml'), ` +workflow_categories: + Default: + - simple +`); + + writeYaml(globalConfigPath, ` +show_others_category: false +`); + + const config = getWorkflowCategories(testDir); + expect(config).not.toBeNull(); + expect(config!.workflowCategories).toEqual({ Default: ['simple'] }); + }); + + it('should return null when default categories file is missing', () => { + const config = loadDefaultCategories(); + expect(config).toBeNull(); + }); +}); + +describe('buildCategorizedWorkflows', () => { + beforeEach(() => { + }); + + it('should warn for missing workflows and generate Others', () => { + const allWorkflows = createWorkflowMap(['a', 'b', 'c']); + const config = { + workflowCategories: { Cat: ['a', 'missing'] }, + showOthersCategory: true, + othersCategoryName: 'Others', + }; + + const categorized = buildCategorizedWorkflows(allWorkflows, config); + expect(categorized.categories.get('Cat')).toEqual(['a']); + expect(categorized.categories.get('Others')).toEqual(['b', 'c']); + expect(categorized.missingWorkflows).toEqual([ + { categoryName: 'Cat', workflowName: 'missing' }, + ]); + }); + + it('should skip empty categories', () => { + const allWorkflows = createWorkflowMap(['a']); + const config = { + workflowCategories: { Empty: [] }, + showOthersCategory: false, + othersCategoryName: 'Others', + }; + + const categorized = buildCategorizedWorkflows(allWorkflows, config); + expect(categorized.categories.size).toBe(0); + }); + + it('should find categories containing a workflow', () => { + const allWorkflows = createWorkflowMap(['shared']); + const config = { + workflowCategories: { A: ['shared'], B: ['shared'] }, + showOthersCategory: false, + othersCategoryName: 'Others', + }; + + const categorized = buildCategorizedWorkflows(allWorkflows, config); + const categories = findWorkflowCategories('shared', categorized).sort(); + expect(categories).toEqual(['A', 'B']); + }); +}); diff --git a/src/core/models/global-config.ts b/src/core/models/global-config.ts index 5e27666..8c9b9ed 100644 --- a/src/core/models/global-config.ts +++ b/src/core/models/global-config.ts @@ -54,6 +54,14 @@ export interface GlobalConfig { pipeline?: PipelineConfig; /** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */ minimalOutput?: boolean; + /** Bookmarked workflow names for quick access in selection UI */ + bookmarkedWorkflows?: string[]; + /** Workflow category configuration (name -> workflow list) */ + workflowCategories?: Record; + /** Show uncategorized workflows under Others category */ + showOthersCategory?: boolean; + /** Display name for Others category */ + othersCategoryName?: string; } /** Project-level configuration */ diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index 5e9ab2c..658aa54 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -219,6 +219,14 @@ export const GlobalConfigSchema = z.object({ pipeline: PipelineConfigSchema.optional(), /** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */ minimal_output: z.boolean().optional().default(false), + /** Bookmarked workflow names for quick access in selection UI */ + bookmarked_workflows: z.array(z.string()).optional().default([]), + /** Workflow categories (name -> workflow list) */ + workflow_categories: z.record(z.string(), z.array(z.string())).optional(), + /** Show uncategorized workflows under Others category */ + show_others_category: z.boolean().optional(), + /** Display name for Others category */ + others_category_name: z.string().min(1).optional(), }); /** Project config schema */ diff --git a/src/features/config/switchWorkflow.ts b/src/features/config/switchWorkflow.ts index a04ab58..e9ad62e 100644 --- a/src/features/config/switchWorkflow.ts +++ b/src/features/config/switchWorkflow.ts @@ -2,29 +2,101 @@ * Workflow switching command */ -import { listWorkflows, loadWorkflow, getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/index.js'; +import { + listWorkflowEntries, + loadAllWorkflows, + getWorkflowCategories, + buildCategorizedWorkflows, + loadWorkflow, + getCurrentWorkflow, + setCurrentWorkflow, +} from '../../infra/config/index.js'; +import { + getBookmarkedWorkflows, + toggleBookmark, +} from '../../infra/config/global/index.js'; import { info, success, error } from '../../shared/ui/index.js'; import { selectOption } from '../../shared/prompt/index.js'; +import type { SelectOptionItem } from '../../shared/prompt/index.js'; +import { + buildWorkflowSelectionItems, + buildTopLevelSelectOptions, + parseCategorySelection, + buildCategoryWorkflowOptions, + applyBookmarks, + warnMissingWorkflows, + selectWorkflowFromCategorizedWorkflows, + type SelectionOption, +} from '../workflowSelection/index.js'; /** - * Get all available workflow options + * Create an onBookmark callback for workflow selection. + * Toggles the bookmark in global config and returns updated options. */ -function getAllWorkflowOptions(cwd: string): { label: string; value: string }[] { - const current = getCurrentWorkflow(cwd); - const workflows = listWorkflows(cwd); - - const options: { label: string; value: string }[] = []; - - // Add all workflows - for (const name of workflows) { - const isCurrent = name === current; - const label = isCurrent ? `${name} (current)` : name; - options.push({ label, value: name }); - } - - return options; +function createBookmarkCallback( + items: ReturnType, + currentWorkflow: string, +): (value: string) => SelectOptionItem[] { + return (value: string): SelectOptionItem[] => { + const categoryName = parseCategorySelection(value); + if (categoryName) { + return applyBookmarks( + buildTopLevelSelectOptions(items, currentWorkflow), + getBookmarkedWorkflows(), + ); + } + toggleBookmark(value); + return applyBookmarks( + buildTopLevelSelectOptions(items, currentWorkflow), + getBookmarkedWorkflows(), + ); + }; } +/** + * 2-stage workflow selection with directory categories and bookmark support. + */ +async function selectWorkflowWithCategories(cwd: string): Promise { + const current = getCurrentWorkflow(cwd); + const entries = listWorkflowEntries(cwd); + const items = buildWorkflowSelectionItems(entries); + + // Loop until user selects a workflow or cancels at top level + while (true) { + const baseOptions = buildTopLevelSelectOptions(items, current); + const options = applyBookmarks(baseOptions, getBookmarkedWorkflows()); + + const selected = await selectOption('Select workflow:', options, { + onBookmark: createBookmarkCallback(items, current), + }); + if (!selected) return null; + + const categoryName = parseCategorySelection(selected); + if (categoryName) { + const categoryOptions = buildCategoryWorkflowOptions(items, categoryName, current); + if (!categoryOptions) continue; + const bookmarkedInCategory = applyBookmarks(categoryOptions, getBookmarkedWorkflows()); + const workflowSelection = await selectOption(`Select workflow in ${categoryName}:`, bookmarkedInCategory, { + cancelLabel: '← Go back', + onBookmark: (value: string): SelectOptionItem[] => { + toggleBookmark(value); + return applyBookmarks( + buildCategoryWorkflowOptions(items, categoryName, current) as SelectionOption[], + getBookmarkedWorkflows(), + ); + }, + }); + + // If workflow selected, return it. If cancelled (null), go back to top level + if (workflowSelection) return workflowSelection; + continue; + } + + return selected; + } +} + + /** * Switch to a different workflow * @returns true if switch was successful @@ -35,8 +107,21 @@ export async function switchWorkflow(cwd: string, workflowName?: string): Promis const current = getCurrentWorkflow(cwd); info(`Current workflow: ${current}`); - const options = getAllWorkflowOptions(cwd); - const selected = await selectOption('Select workflow:', options); + const categoryConfig = getWorkflowCategories(cwd); + let selected: string | null; + if (categoryConfig) { + const allWorkflows = loadAllWorkflows(cwd); + if (allWorkflows.size === 0) { + info('No workflows found.'); + selected = null; + } else { + const categorized = buildCategorizedWorkflows(allWorkflows, categoryConfig); + warnMissingWorkflows(categorized.missingWorkflows); + selected = await selectWorkflowFromCategorizedWorkflows(categorized, current); + } + } else { + selected = await selectWorkflowWithCategories(cwd); + } if (!selected) { info('Cancelled'); diff --git a/src/features/tasks/execute/selectAndExecute.ts b/src/features/tasks/execute/selectAndExecute.ts index 5a34609..8433c08 100644 --- a/src/features/tasks/execute/selectAndExecute.ts +++ b/src/features/tasks/execute/selectAndExecute.ts @@ -6,8 +6,21 @@ * mixing CLI parsing with business logic. */ -import { getCurrentWorkflow, listWorkflows, isWorkflowPath } from '../../../infra/config/index.js'; -import { selectOptionWithDefault, confirm } from '../../../shared/prompt/index.js'; +import { + getCurrentWorkflow, + listWorkflows, + listWorkflowEntries, + isWorkflowPath, + loadAllWorkflows, + getWorkflowCategories, + buildCategorizedWorkflows, +} from '../../../infra/config/index.js'; +import { + getBookmarkedWorkflows, + toggleBookmark, +} from '../../../infra/config/global/index.js'; +import { selectOption, confirm } from '../../../shared/prompt/index.js'; +import type { SelectOptionItem } 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'; @@ -15,16 +28,25 @@ 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'; +import { + buildWorkflowSelectionItems, + buildTopLevelSelectOptions, + parseCategorySelection, + buildCategoryWorkflowOptions, + applyBookmarks, + warnMissingWorkflows, + selectWorkflowFromCategorizedWorkflows, + type SelectionOption, +} from '../../workflowSelection/index.js'; export type { WorktreeConfirmationResult, SelectAndExecuteOptions }; const log = createLogger('selectAndExecute'); /** - * Select a workflow interactively. - * Returns the selected workflow name, or null if cancelled. + * Select a workflow interactively with directory categories and bookmarks. */ -async function selectWorkflow(cwd: string): Promise { +async function selectWorkflowWithDirectoryCategories(cwd: string): Promise { const availableWorkflows = listWorkflows(cwd); const currentWorkflow = getCurrentWorkflow(cwd); @@ -37,18 +59,91 @@ async function selectWorkflow(cwd: string): Promise { return availableWorkflows[0]; } - const options = availableWorkflows.map((name) => ({ - label: name === currentWorkflow ? `${name} (current)` : name, - value: name, - })); + const entries = listWorkflowEntries(cwd); + const items = buildWorkflowSelectionItems(entries); - const defaultWorkflow = availableWorkflows.includes(currentWorkflow) - ? currentWorkflow - : (availableWorkflows.includes(DEFAULT_WORKFLOW_NAME) - ? DEFAULT_WORKFLOW_NAME - : availableWorkflows[0] || DEFAULT_WORKFLOW_NAME); + const hasCategories = items.some((item) => item.type === 'category'); - return selectOptionWithDefault('Select workflow:', options, defaultWorkflow); + if (!hasCategories) { + const baseOptions: SelectionOption[] = availableWorkflows.map((name) => ({ + label: name === currentWorkflow ? `${name} (current)` : name, + value: name, + })); + + const buildFlatOptions = (): SelectionOption[] => + applyBookmarks(baseOptions, getBookmarkedWorkflows()); + + return selectOption('Select workflow:', buildFlatOptions(), { + onBookmark: (value: string): SelectOptionItem[] => { + toggleBookmark(value); + return buildFlatOptions(); + }, + }); + } + + const createTopLevelBookmarkCallback = (): ((value: string) => SelectOptionItem[]) => { + return (value: string): SelectOptionItem[] => { + if (parseCategorySelection(value)) { + return applyBookmarks(buildTopLevelSelectOptions(items, currentWorkflow), getBookmarkedWorkflows()); + } + toggleBookmark(value); + return applyBookmarks(buildTopLevelSelectOptions(items, currentWorkflow), getBookmarkedWorkflows()); + }; + }; + + // Loop until user selects a workflow or cancels at top level + while (true) { + const baseOptions = buildTopLevelSelectOptions(items, currentWorkflow); + const topLevelOptions = applyBookmarks(baseOptions, getBookmarkedWorkflows()); + + const selected = await selectOption('Select workflow:', topLevelOptions, { + onBookmark: createTopLevelBookmarkCallback(), + }); + if (!selected) return null; + + const categoryName = parseCategorySelection(selected); + if (categoryName) { + const categoryOptions = buildCategoryWorkflowOptions(items, categoryName, currentWorkflow); + if (!categoryOptions) continue; + const bookmarkedCategoryOptions = applyBookmarks(categoryOptions, getBookmarkedWorkflows()); + const workflowSelection = await selectOption(`Select workflow in ${categoryName}:`, bookmarkedCategoryOptions, { + cancelLabel: '← Go back', + onBookmark: (value: string): SelectOptionItem[] => { + toggleBookmark(value); + return applyBookmarks( + buildCategoryWorkflowOptions(items, categoryName, currentWorkflow) as SelectionOption[], + getBookmarkedWorkflows(), + ); + }, + }); + + // If workflow selected, return it. If cancelled (null), go back to top level + if (workflowSelection) return workflowSelection; + continue; + } + + return selected; + } +} + + +/** + * Select a workflow interactively with 2-stage category support. + */ +async function selectWorkflow(cwd: string): Promise { + const categoryConfig = getWorkflowCategories(cwd); + if (categoryConfig) { + const current = getCurrentWorkflow(cwd); + const allWorkflows = loadAllWorkflows(cwd); + if (allWorkflows.size === 0) { + info(`No workflows found. Using default: ${DEFAULT_WORKFLOW_NAME}`); + return DEFAULT_WORKFLOW_NAME; + } + const categorized = buildCategorizedWorkflows(allWorkflows, categoryConfig); + warnMissingWorkflows(categorized.missingWorkflows); + return selectWorkflowFromCategorizedWorkflows(categorized, current); + } + return selectWorkflowWithDirectoryCategories(cwd); } /** @@ -60,11 +155,9 @@ async function selectWorkflow(cwd: string): Promise { */ async function determineWorkflow(cwd: string, override?: string): Promise { if (override) { - // Path-based: skip name validation (loader handles existence check) if (isWorkflowPath(override)) { return override; } - // Name-based: validate workflow name exists const availableWorkflows = listWorkflows(cwd); const knownWorkflows = availableWorkflows.length === 0 ? [DEFAULT_WORKFLOW_NAME] : availableWorkflows; if (!knownWorkflows.includes(override)) { @@ -90,7 +183,6 @@ export async function confirmAndCreateWorktree( return { execCwd: cwd, isWorktree: false }; } - // Summarize task name to English slug using AI info('Generating branch name...'); const taskSlug = await summarizeTaskName(task, { cwd }); @@ -144,7 +236,6 @@ export async function selectAndExecuteTask( error(`Auto-commit failed: ${commitResult.message}`); } - // PR creation: --auto-pr → create automatically, otherwise ask if (commitResult.success && commitResult.commitHash && branch) { const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false); if (shouldCreatePr) { diff --git a/src/features/workflowSelection/index.ts b/src/features/workflowSelection/index.ts new file mode 100644 index 0000000..47cd92c --- /dev/null +++ b/src/features/workflowSelection/index.ts @@ -0,0 +1,235 @@ +/** + * Workflow selection helpers (UI layer). + */ + +import { selectOption } from '../../shared/prompt/index.js'; +import type { SelectOptionItem } from '../../shared/prompt/index.js'; +import { info, warn } from '../../shared/ui/index.js'; +import { + getBookmarkedWorkflows, + toggleBookmark, +} from '../../infra/config/global/index.js'; +import { + findWorkflowCategories, + type WorkflowDirEntry, + type CategorizedWorkflows, + type MissingWorkflow, +} from '../../infra/config/index.js'; + +/** Top-level selection item: either a workflow or a category containing workflows */ +export type WorkflowSelectionItem = + | { type: 'workflow'; name: string } + | { type: 'category'; name: string; workflows: string[] }; + +/** Option item for prompt UI */ +export interface SelectionOption { + label: string; + value: string; +} + +/** + * Build top-level selection items for the workflow chooser UI. + * Root-level workflows and categories are displayed at the same level. + */ +export function buildWorkflowSelectionItems(entries: WorkflowDirEntry[]): WorkflowSelectionItem[] { + const categories = new Map(); + const items: WorkflowSelectionItem[] = []; + + for (const entry of entries) { + if (entry.category) { + let workflows = categories.get(entry.category); + if (!workflows) { + workflows = []; + categories.set(entry.category, workflows); + } + workflows.push(entry.name); + } else { + items.push({ type: 'workflow', name: entry.name }); + } + } + + for (const [name, workflows] of categories) { + items.push({ type: 'category', name, workflows: workflows.sort() }); + } + + return items.sort((a, b) => a.name.localeCompare(b.name)); +} + +const CATEGORY_VALUE_PREFIX = '__category__:'; + +/** + * Build top-level select options from WorkflowSelectionItems. + * Categories are encoded with a prefix in the value field. + */ +export function buildTopLevelSelectOptions( + items: WorkflowSelectionItem[], + currentWorkflow: string, +): SelectionOption[] { + return items.map((item) => { + if (item.type === 'workflow') { + const isCurrent = item.name === currentWorkflow; + const label = isCurrent ? `${item.name} (current)` : item.name; + return { label, value: item.name }; + } + const containsCurrent = item.workflows.some((w) => w === currentWorkflow); + const label = containsCurrent ? `📁 ${item.name}/ (current)` : `📁 ${item.name}/`; + return { label, value: `${CATEGORY_VALUE_PREFIX}${item.name}` }; + }); +} + +/** + * Parse a top-level selection result. + * Returns the category name if a category was selected, or null if a workflow was selected directly. + */ +export function parseCategorySelection(selected: string): string | null { + if (selected.startsWith(CATEGORY_VALUE_PREFIX)) { + return selected.slice(CATEGORY_VALUE_PREFIX.length); + } + return null; +} + +/** + * Build select options for workflows within a category. + */ +export function buildCategoryWorkflowOptions( + items: WorkflowSelectionItem[], + categoryName: string, + currentWorkflow: string, +): SelectionOption[] | null { + const categoryItem = items.find( + (item) => item.type === 'category' && item.name === categoryName, + ); + if (!categoryItem || categoryItem.type !== 'category') return null; + + return categoryItem.workflows.map((qualifiedName) => { + const displayName = qualifiedName.split('/').pop()!; + const isCurrent = qualifiedName === currentWorkflow; + const label = isCurrent ? `${displayName} (current)` : displayName; + return { label, value: qualifiedName }; + }); +} + +const BOOKMARK_MARK = '★ '; + +/** + * Sort options with bookmarked items first and add ★ prefix. + * Pure function — does not mutate inputs. + */ +export function applyBookmarks( + options: SelectionOption[], + bookmarkedWorkflows: string[], +): SelectionOption[] { + const bookmarkedSet = new Set(bookmarkedWorkflows); + const bookmarked: SelectionOption[] = []; + const rest: SelectionOption[] = []; + + for (const opt of options) { + if (bookmarkedSet.has(opt.value)) { + bookmarked.push({ ...opt, label: `${BOOKMARK_MARK}${opt.label}` }); + } else { + rest.push(opt); + } + } + + return [...bookmarked, ...rest]; +} + +/** + * Warn about missing workflows referenced by categories. + */ +export function warnMissingWorkflows(missing: MissingWorkflow[]): void { + for (const { categoryName, workflowName } of missing) { + warn(`Workflow "${workflowName}" in category "${categoryName}" not found`); + } +} + +function buildCategorySelectOptions( + categorized: CategorizedWorkflows, + currentWorkflow: string, +): SelectOptionItem[] { + const entries = Array.from(categorized.categories.entries()) + .map(([categoryName, workflows]) => ({ categoryName, workflows })); + + return entries.map(({ categoryName, workflows }) => { + const containsCurrent = workflows.includes(currentWorkflow); + const label = containsCurrent + ? `${categoryName} (${workflows.length} workflows, current)` + : `${categoryName} (${workflows.length} workflows)`; + return { label, value: categoryName }; + }); +} + +function buildWorkflowOptionsForCategory( + categorized: CategorizedWorkflows, + categoryName: string, + currentWorkflow: string, +): SelectOptionItem[] | null { + const workflows = categorized.categories.get(categoryName); + if (!workflows) return null; + + return workflows.map((workflowName) => { + const alsoIn = findWorkflowCategories(workflowName, categorized) + .filter((name) => name !== categoryName); + const isCurrent = workflowName === currentWorkflow; + const alsoInLabel = alsoIn.length > 0 ? `also in ${alsoIn.join(', ')}` : ''; + + let label = workflowName; + if (isCurrent && alsoInLabel) { + label = `${workflowName} (current, ${alsoInLabel})`; + } else if (isCurrent) { + label = `${workflowName} (current)`; + } else if (alsoInLabel) { + label = `${workflowName} (${alsoInLabel})`; + } + + return { label, value: workflowName }; + }); +} + +/** + * Select workflow from categorized workflows (2-stage UI). + */ +export async function selectWorkflowFromCategorizedWorkflows( + categorized: CategorizedWorkflows, + currentWorkflow: string, +): Promise { + const categoryOptions = buildCategorySelectOptions(categorized, currentWorkflow); + + if (categoryOptions.length === 0) { + info('No workflows available for configured categories.'); + return null; + } + + // Loop until user selects a workflow or cancels at category level + while (true) { + const selectedCategory = await selectOption('Select workflow category:', categoryOptions); + if (!selectedCategory) return null; + + const buildWorkflowOptions = (): SelectOptionItem[] | null => + buildWorkflowOptionsForCategory(categorized, selectedCategory, currentWorkflow); + + const baseWorkflowOptions = buildWorkflowOptions(); + if (!baseWorkflowOptions) continue; + + const applyWorkflowBookmarks = (options: SelectOptionItem[]): SelectOptionItem[] => { + return applyBookmarks(options, getBookmarkedWorkflows()) as SelectOptionItem[]; + }; + + const selectedWorkflow = await selectOption( + `Select workflow in ${selectedCategory}:`, + applyWorkflowBookmarks(baseWorkflowOptions), + { + cancelLabel: '← Go back', + onBookmark: (value: string): SelectOptionItem[] => { + toggleBookmark(value); + const updatedOptions = buildWorkflowOptions(); + if (!updatedOptions) return []; + return applyWorkflowBookmarks(updatedOptions); + }, + }, + ); + + // If workflow selected, return it. If cancelled (null), go back to category selection + if (selectedWorkflow) return selectedWorkflow; + } +} diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index 84a41ed..bc29d4c 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -86,6 +86,10 @@ export class GlobalConfigManager { prBodyTemplate: parsed.pipeline.pr_body_template, } : undefined, minimalOutput: parsed.minimal_output, + bookmarkedWorkflows: parsed.bookmarked_workflows, + workflowCategories: parsed.workflow_categories, + showOthersCategory: parsed.show_others_category, + othersCategoryName: parsed.others_category_name, }; this.cachedConfig = config; return config; @@ -134,6 +138,18 @@ export class GlobalConfigManager { if (config.minimalOutput !== undefined) { raw.minimal_output = config.minimalOutput; } + if (config.bookmarkedWorkflows && config.bookmarkedWorkflows.length > 0) { + raw.bookmarked_workflows = config.bookmarkedWorkflows; + } + if (config.workflowCategories !== undefined) { + raw.workflow_categories = config.workflowCategories; + } + if (config.showOthersCategory !== undefined) { + raw.show_others_category = config.showOthersCategory; + } + if (config.othersCategoryName !== undefined) { + raw.others_category_name = config.othersCategoryName; + } writeFileSync(configPath, stringifyYaml(raw), 'utf-8'); this.invalidateCache(); } @@ -270,3 +286,26 @@ export function getEffectiveDebugConfig(projectDir?: string): DebugConfig | unde return debugConfig; } + +/** Get bookmarked workflow names */ +export function getBookmarkedWorkflows(): string[] { + const config = loadGlobalConfig(); + return config.bookmarkedWorkflows ?? []; +} + +/** + * Toggle a workflow bookmark (add if not present, remove if present). + * Persists to ~/.takt/config.yaml and returns the updated bookmarks list. + */ +export function toggleBookmark(workflowName: string): string[] { + const config = loadGlobalConfig(); + const bookmarks = [...(config.bookmarkedWorkflows ?? [])]; + const index = bookmarks.indexOf(workflowName); + if (index >= 0) { + bookmarks.splice(index, 1); + } else { + bookmarks.push(workflowName); + } + saveGlobalConfig({ ...config, bookmarkedWorkflows: bookmarks }); + return bookmarks; +} diff --git a/src/infra/config/global/index.ts b/src/infra/config/global/index.ts index b73b2e5..c7a0461 100644 --- a/src/infra/config/global/index.ts +++ b/src/infra/config/global/index.ts @@ -17,6 +17,8 @@ export { resolveOpenaiApiKey, loadProjectDebugConfig, getEffectiveDebugConfig, + getBookmarkedWorkflows, + toggleBookmark, } from './globalConfig.js'; export { diff --git a/src/infra/config/loaders/index.ts b/src/infra/config/loaders/index.ts index ed57c13..3541737 100644 --- a/src/infra/config/loaders/index.ts +++ b/src/infra/config/loaders/index.ts @@ -9,8 +9,20 @@ export { isWorkflowPath, loadAllWorkflows, listWorkflows, + listWorkflowEntries, + type WorkflowDirEntry, } from './workflowLoader.js'; +export { + loadDefaultCategories, + getWorkflowCategories, + buildCategorizedWorkflows, + findWorkflowCategories, + type CategoryConfig, + type CategorizedWorkflows, + type MissingWorkflow, +} from './workflowCategories.js'; + export { loadAgentsFromDir, loadCustomAgents, diff --git a/src/infra/config/loaders/workflowCategories.ts b/src/infra/config/loaders/workflowCategories.ts new file mode 100644 index 0000000..caee643 --- /dev/null +++ b/src/infra/config/loaders/workflowCategories.ts @@ -0,0 +1,168 @@ +/** + * Workflow category configuration loader and helpers. + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { parse as parseYaml } from 'yaml'; +import { z } from 'zod/v4'; +import type { WorkflowConfig } from '../../../core/models/index.js'; +import { getGlobalConfigPath, getProjectConfigPath } from '../paths.js'; +import { getLanguage } from '../global/globalConfig.js'; +import { getLanguageResourcesDir } from '../../resources/index.js'; + +const CategoryConfigSchema = z.object({ + workflow_categories: z.record(z.string(), z.array(z.string())).optional(), + show_others_category: z.boolean().optional(), + others_category_name: z.string().min(1).optional(), +}).passthrough(); + +export interface CategoryConfig { + workflowCategories: Record; + showOthersCategory: boolean; + othersCategoryName: string; +} + +export interface CategorizedWorkflows { + categories: Map; + allWorkflows: Map; + missingWorkflows: MissingWorkflow[]; +} + +export interface MissingWorkflow { + categoryName: string; + workflowName: string; +} + +interface RawCategoryConfig { + workflow_categories?: Record; + show_others_category?: boolean; + others_category_name?: string; +} + +function parseCategoryConfig(raw: unknown, sourceLabel: string): CategoryConfig | null { + if (!raw || typeof raw !== 'object') { + return null; + } + + const hasWorkflowCategories = Object.prototype.hasOwnProperty.call(raw, 'workflow_categories'); + if (!hasWorkflowCategories) { + return null; + } + + const parsed = CategoryConfigSchema.parse(raw) as RawCategoryConfig; + if (!parsed.workflow_categories) { + throw new Error(`workflow_categories is required in ${sourceLabel}`); + } + + const showOthersCategory = parsed.show_others_category === undefined + ? true + : parsed.show_others_category; + + const othersCategoryName = parsed.others_category_name === undefined + ? 'Others' + : parsed.others_category_name; + + return { + workflowCategories: parsed.workflow_categories, + showOthersCategory, + othersCategoryName, + }; +} + +function loadCategoryConfigFromPath(path: string, sourceLabel: string): CategoryConfig | null { + if (!existsSync(path)) { + return null; + } + const content = readFileSync(path, 'utf-8'); + const raw = parseYaml(content); + return parseCategoryConfig(raw, sourceLabel); +} + +/** + * Load default categories from builtin resource file. + * Returns null if file doesn't exist or has no workflow_categories. + */ +export function loadDefaultCategories(): CategoryConfig | null { + const lang = getLanguage(); + const filePath = join(getLanguageResourcesDir(lang), 'default-categories.yaml'); + return loadCategoryConfigFromPath(filePath, filePath); +} + +/** + * Get effective workflow categories configuration. + * Priority: user config -> project config -> default categories. + */ +export function getWorkflowCategories(cwd: string): CategoryConfig | null { + const userConfig = loadCategoryConfigFromPath(getGlobalConfigPath(), 'global config'); + if (userConfig) { + return userConfig; + } + + const projectConfig = loadCategoryConfigFromPath(getProjectConfigPath(cwd), 'project config'); + if (projectConfig) { + return projectConfig; + } + + return loadDefaultCategories(); +} + +/** + * Build categorized workflows map from configuration. + */ +export function buildCategorizedWorkflows( + allWorkflows: Map, + config: CategoryConfig, +): CategorizedWorkflows { + const categories = new Map(); + const categorized = new Set(); + const missingWorkflows: MissingWorkflow[] = []; + + for (const [categoryName, workflowNames] of Object.entries(config.workflowCategories)) { + const validWorkflows: string[] = []; + + for (const workflowName of workflowNames) { + if (allWorkflows.has(workflowName)) { + validWorkflows.push(workflowName); + categorized.add(workflowName); + } else { + missingWorkflows.push({ categoryName, workflowName }); + } + } + + if (validWorkflows.length > 0) { + categories.set(categoryName, validWorkflows); + } + } + + if (config.showOthersCategory) { + const uncategorized: string[] = []; + for (const workflowName of allWorkflows.keys()) { + if (!categorized.has(workflowName)) { + uncategorized.push(workflowName); + } + } + + if (uncategorized.length > 0 && !categories.has(config.othersCategoryName)) { + categories.set(config.othersCategoryName, uncategorized); + } + } + + return { categories, allWorkflows, missingWorkflows }; +} + +/** + * Find which categories contain a given workflow (for duplicate indication). + */ +export function findWorkflowCategories( + workflow: string, + categorized: CategorizedWorkflows, +): string[] { + const result: string[] = []; + for (const [categoryName, workflows] of categorized.categories) { + if (workflows.includes(workflow)) { + result.push(categoryName); + } + } + return result; +} diff --git a/src/infra/config/loaders/workflowLoader.ts b/src/infra/config/loaders/workflowLoader.ts index b8834b6..6eec02b 100644 --- a/src/infra/config/loaders/workflowLoader.ts +++ b/src/infra/config/loaders/workflowLoader.ts @@ -17,4 +17,6 @@ export { loadWorkflowByIdentifier, loadAllWorkflows, listWorkflows, + listWorkflowEntries, + type WorkflowDirEntry, } from './workflowResolver.js'; diff --git a/src/infra/config/loaders/workflowResolver.ts b/src/infra/config/loaders/workflowResolver.ts index 9d81af1..80960e4 100644 --- a/src/infra/config/loaders/workflowResolver.ts +++ b/src/infra/config/loaders/workflowResolver.ts @@ -58,8 +58,22 @@ function loadWorkflowFromPath( return loadWorkflowFromFile(resolvedPath); } +/** + * Resolve a workflow YAML file path by trying both .yaml and .yml extensions. + * For category/name identifiers (e.g. "frontend/react"), resolves to + * {workflowsDir}/frontend/react.yaml (or .yml). + */ +function resolveWorkflowFile(workflowsDir: string, name: string): string | null { + for (const ext of ['.yaml', '.yml']) { + const filePath = join(workflowsDir, `${name}${ext}`); + if (existsSync(filePath)) return filePath; + } + return null; +} + /** * Load workflow by name (name-based loading only, no path detection). + * Supports category/name identifiers (e.g. "frontend/react"). * * Priority: * 1. Project-local workflows → .takt/workflows/{name}.yaml @@ -71,15 +85,15 @@ export function loadWorkflow( projectCwd: string, ): WorkflowConfig | null { const projectWorkflowsDir = join(getProjectConfigDir(projectCwd), 'workflows'); - const projectWorkflowPath = join(projectWorkflowsDir, `${name}.yaml`); - if (existsSync(projectWorkflowPath)) { - return loadWorkflowFromFile(projectWorkflowPath); + const projectMatch = resolveWorkflowFile(projectWorkflowsDir, name); + if (projectMatch) { + return loadWorkflowFromFile(projectMatch); } const globalWorkflowsDir = getGlobalWorkflowsDir(); - const workflowYamlPath = join(globalWorkflowsDir, `${name}.yaml`); - if (existsSync(workflowYamlPath)) { - return loadWorkflowFromFile(workflowYamlPath); + const globalMatch = resolveWorkflowFile(globalWorkflowsDir, name); + if (globalMatch) { + return loadWorkflowFromFile(globalMatch); } return getBuiltinWorkflow(name); @@ -113,13 +127,18 @@ export function loadWorkflowByIdentifier( } /** Entry for a workflow file found in a directory */ -interface WorkflowDirEntry { +export interface WorkflowDirEntry { + /** Workflow name (e.g. "react") */ name: string; + /** Full file path */ path: string; + /** Category (subdirectory name), undefined for root-level workflows */ + category?: string; } /** - * Iterate workflow YAML files in a directory, yielding name and path. + * Iterate workflow YAML files in a directory, yielding name, path, and category. + * Scans root-level files (no category) and 1-level subdirectories (category = dir name). * Shared by both loadAllWorkflows and listWorkflows to avoid DRY violation. */ function* iterateWorkflowDir( @@ -128,12 +147,29 @@ function* iterateWorkflowDir( ): Generator { if (!existsSync(dir)) return; for (const entry of readdirSync(dir)) { - if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; const entryPath = join(dir, entry); - if (!statSync(entryPath).isFile()) continue; - const workflowName = entry.replace(/\.ya?ml$/, ''); - if (disabled?.includes(workflowName)) continue; - yield { name: workflowName, path: entryPath }; + const stat = statSync(entryPath); + + if (stat.isFile() && (entry.endsWith('.yaml') || entry.endsWith('.yml'))) { + const workflowName = entry.replace(/\.ya?ml$/, ''); + if (disabled?.includes(workflowName)) continue; + yield { name: workflowName, path: entryPath }; + continue; + } + + // 1-level subdirectory scan: directory name becomes the category + if (stat.isDirectory()) { + const category = entry; + for (const subEntry of readdirSync(entryPath)) { + if (!subEntry.endsWith('.yaml') && !subEntry.endsWith('.yml')) continue; + const subEntryPath = join(entryPath, subEntry); + if (!statSync(subEntryPath).isFile()) continue; + const workflowName = subEntry.replace(/\.ya?ml$/, ''); + const qualifiedName = `${category}/${workflowName}`; + if (disabled?.includes(qualifiedName)) continue; + yield { name: qualifiedName, path: subEntryPath, category }; + } + } } } @@ -174,6 +210,7 @@ export function loadAllWorkflows(cwd: string): Map { /** * List available workflow names (builtin + user + project-local, excluding disabled). + * Category workflows use qualified names like "frontend/react". */ export function listWorkflows(cwd: string): string[] { const workflows = new Set(); @@ -186,3 +223,23 @@ export function listWorkflows(cwd: string): string[] { return Array.from(workflows).sort(); } + +/** + * List available workflows with category information for UI display. + * Returns entries grouped by category for 2-stage selection. + * + * Root-level workflows (no category) and category names are presented + * at the same level. Selecting a category drills into its workflows. + */ +export function listWorkflowEntries(cwd: string): WorkflowDirEntry[] { + // Later entries override earlier (project-local > user > builtin) + const workflows = new Map(); + + for (const { dir, disabled } of getWorkflowDirs(cwd)) { + for (const entry of iterateWorkflowDir(dir, disabled)) { + workflows.set(entry.name, entry); + } + } + + return Array.from(workflows.values()); +} diff --git a/src/infra/config/types.ts b/src/infra/config/types.ts index e8dd2e9..d04bad2 100644 --- a/src/infra/config/types.ts +++ b/src/infra/config/types.ts @@ -23,6 +23,12 @@ export interface ProjectLocalConfig { permissionMode?: PermissionMode; /** Verbose output mode */ verbose?: boolean; + /** Workflow categories (name -> workflow list) */ + workflow_categories?: Record; + /** Show uncategorized workflows under Others category */ + show_others_category?: boolean; + /** Display name for Others category */ + others_category_name?: string; /** Custom settings */ [key: string]: unknown; } diff --git a/src/shared/prompt/index.ts b/src/shared/prompt/index.ts index caa56cc..02101d2 100644 --- a/src/shared/prompt/index.ts +++ b/src/shared/prompt/index.ts @@ -8,6 +8,7 @@ export { type SelectOptionItem, + type InteractiveSelectCallbacks, renderMenu, countRenderedLines, type KeyInputResult, diff --git a/src/shared/prompt/select.ts b/src/shared/prompt/select.ts index cdb24b9..ebdfeaf 100644 --- a/src/shared/prompt/select.ts +++ b/src/shared/prompt/select.ts @@ -23,6 +23,7 @@ export function renderMenu( options: SelectOptionItem[], selectedIndex: number, hasCancelOption: boolean, + cancelLabel = 'Cancel', ): string[] { const maxWidth = process.stdout.columns || 80; const labelPrefix = 4; @@ -54,7 +55,7 @@ export function renderMenu( if (hasCancelOption) { const isCancelSelected = selectedIndex === options.length; const cursor = isCancelSelected ? chalk.cyan('❯') : ' '; - const label = isCancelSelected ? chalk.cyan.bold('Cancel') : chalk.gray('Cancel'); + const label = isCancelSelected ? chalk.cyan.bold(cancelLabel) : chalk.gray(cancelLabel); lines.push(` ${cursor} ${label}`); } @@ -84,6 +85,7 @@ export type KeyInputResult = | { action: 'move'; newIndex: number } | { action: 'confirm'; selectedIndex: number } | { action: 'cancel'; cancelIndex: number } + | { action: 'bookmark'; selectedIndex: number } | { action: 'exit' } | { action: 'none' }; @@ -113,14 +115,20 @@ export function handleKeyInput( if (key === '\x1B') { return { action: 'cancel', cancelIndex: hasCancelOption ? optionCount : -1 }; } + if (key === 'b') { + return { action: 'bookmark', selectedIndex: currentIndex }; + } return { action: 'none' }; } /** Print the menu header (message + hint). */ -function printHeader(message: string): void { +function printHeader(message: string, showBookmarkHint: boolean): void { console.log(); console.log(chalk.cyan(message)); - console.log(chalk.gray(' (↑↓ to move, Enter to select)')); + const hint = showBookmarkHint + ? ' (↑↓ to move, Enter to select, b to bookmark)' + : ' (↑↓ to move, Enter to select)'; + console.log(chalk.gray(hint)); console.log(); } @@ -145,12 +153,28 @@ function redrawMenu( options: SelectOptionItem[], selectedIndex: number, hasCancelOption: boolean, - totalLines: number, -): void { - process.stdout.write(`\x1B[${totalLines}A`); + prevTotalLines: number, + cancelLabel?: string, +): number { + process.stdout.write(`\x1B[${prevTotalLines}A`); process.stdout.write('\x1B[J'); - const newLines = renderMenu(options, selectedIndex, hasCancelOption); + const newLines = renderMenu(options, selectedIndex, hasCancelOption, cancelLabel); process.stdout.write(newLines.join('\n') + '\n'); + return newLines.length; +} + +/** Callbacks for interactive select behavior */ +export interface InteractiveSelectCallbacks { + /** Called when 'b' key is pressed. Returns updated options for re-render. */ + onBookmark?: (value: T, index: number) => SelectOptionItem[]; + /** Custom label for cancel option (default: "Cancel") */ + cancelLabel?: string; +} + +/** Result of interactive selection */ +interface InteractiveSelectResult { + selectedIndex: number; + finalOptions: SelectOptionItem[]; } /** Interactive cursor-based menu selection. */ @@ -159,22 +183,25 @@ function interactiveSelect( options: SelectOptionItem[], initialIndex: number, hasCancelOption: boolean, -): Promise { + callbacks?: InteractiveSelectCallbacks, +): Promise> { return new Promise((resolve) => { - const totalItems = hasCancelOption ? options.length + 1 : options.length; + let currentOptions = options; + let totalItems = hasCancelOption ? currentOptions.length + 1 : currentOptions.length; let selectedIndex = initialIndex; + const cancelLabel = callbacks?.cancelLabel ?? 'Cancel'; - printHeader(message); + printHeader(message, !!callbacks?.onBookmark); process.stdout.write('\x1B[?7l'); - const totalLines = countRenderedLines(options, hasCancelOption); - const lines = renderMenu(options, selectedIndex, hasCancelOption); + let totalLines = countRenderedLines(currentOptions, hasCancelOption); + const lines = renderMenu(currentOptions, selectedIndex, hasCancelOption, cancelLabel); process.stdout.write(lines.join('\n') + '\n'); if (!process.stdin.isTTY) { process.stdout.write('\x1B[?7h'); - resolve(initialIndex); + resolve({ selectedIndex: initialIndex, finalOptions: currentOptions }); return; } @@ -191,22 +218,38 @@ function interactiveSelect( selectedIndex, totalItems, hasCancelOption, - options.length, + currentOptions.length, ); switch (result.action) { case 'move': selectedIndex = result.newIndex; - redrawMenu(options, selectedIndex, hasCancelOption, totalLines); + totalLines = redrawMenu(currentOptions, selectedIndex, hasCancelOption, totalLines, cancelLabel); break; case 'confirm': cleanup(onKeypress); - resolve(result.selectedIndex); + resolve({ selectedIndex: result.selectedIndex, finalOptions: currentOptions }); break; case 'cancel': cleanup(onKeypress); - resolve(result.cancelIndex); + resolve({ selectedIndex: result.cancelIndex, finalOptions: currentOptions }); break; + case 'bookmark': { + if (!callbacks?.onBookmark) break; + // Only bookmark actual options, not the cancel row + if (result.selectedIndex >= currentOptions.length) break; + const item = currentOptions[result.selectedIndex]; + if (!item) break; + const newOptions = callbacks.onBookmark(item.value, result.selectedIndex); + // Find the same value in the new options to preserve cursor position + const currentValue = item.value; + currentOptions = newOptions; + totalItems = hasCancelOption ? currentOptions.length + 1 : currentOptions.length; + const newIdx = currentOptions.findIndex((o) => o.value === currentValue); + selectedIndex = newIdx >= 0 ? newIdx : Math.min(selectedIndex, currentOptions.length - 1); + totalLines = redrawMenu(currentOptions, selectedIndex, hasCancelOption, totalLines, cancelLabel); + break; + } case 'exit': cleanup(onKeypress); process.exit(130); @@ -222,21 +265,23 @@ function interactiveSelect( /** * Prompt user to select from a list of options using cursor navigation. + * @param callbacks.onBookmark - Called when 'b' key is pressed. Returns updated options for re-render. * @returns Selected option or null if cancelled */ export async function selectOption( message: string, options: SelectOptionItem[], + callbacks?: InteractiveSelectCallbacks, ): Promise { if (options.length === 0) return null; - const selectedIndex = await interactiveSelect(message, options, 0, true); + const { selectedIndex, finalOptions } = await interactiveSelect(message, options, 0, true, callbacks); - if (selectedIndex === options.length || selectedIndex === -1) { + if (selectedIndex === finalOptions.length || selectedIndex === -1) { return null; } - const selected = options[selectedIndex]; + const selected = finalOptions[selectedIndex]; if (selected) { console.log(chalk.green(` ✓ ${selected.label}`)); return selected.value; @@ -264,7 +309,7 @@ export async function selectOptionWithDefault( label: opt.value === defaultValue ? `${opt.label} ${chalk.green('(default)')}` : opt.label, })); - const selectedIndex = await interactiveSelect(message, decoratedOptions, initialIndex, true); + const { selectedIndex } = await interactiveSelect(message, decoratedOptions, initialIndex, true); if (selectedIndex === options.length || selectedIndex === -1) { return null; diff --git a/tools/jsonl-viewer.html b/tools/jsonl-viewer.html new file mode 100644 index 0000000..9590373 --- /dev/null +++ b/tools/jsonl-viewer.html @@ -0,0 +1,391 @@ + + + + + + TAKT JSONL Session Viewer + + + +
+

TAKT JSONL Session Viewer

+ +
+
+ ここにJSONLファイルをドラッグ&ドロップ
+ またはクリックしてファイルを選択 +
+ +
+ +
+
+
+ +
+
+ + + +