カテゴリ分けの作成 #85 workflowの修正
This commit is contained in:
parent
d8dbcd01ff
commit
def50ff4a7
@ -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` へ統合する。
|
||||
@ -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` の順序を厳守する。
|
||||
@ -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 <workflow>` to copy a builtin to `~/.takt/workflows/` for customization
|
||||
|
||||
## Workflow Categories
|
||||
|
||||
ワークフローの選択 UI をカテゴリ分けしたい場合は、`workflow_categories` を設定します。
|
||||
詳細は `docs/workflow-categories.md` を参照してください。
|
||||
|
||||
## Workflow Schema
|
||||
|
||||
```yaml
|
||||
|
||||
@ -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/` は変更不要)
|
||||
25
resources/global/en/default-categories.yaml
Normal file
25
resources/global/en/default-categories.yaml
Normal file
@ -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"
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
427
resources/global/en/workflows/minimal.yaml
Normal file
427
resources/global/en/workflows/minimal.yaml
Normal file
@ -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}
|
||||
427
resources/global/en/workflows/review-fix-minimal.yaml
Normal file
427
resources/global/en/workflows/review-fix-minimal.yaml
Normal file
@ -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}
|
||||
@ -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
|
||||
```
|
||||
```
|
||||
25
resources/global/ja/default-categories.yaml
Normal file
25
resources/global/ja/default-categories.yaml
Normal file
@ -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: "その他"
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
427
resources/global/ja/workflows/minimal.yaml
Normal file
427
resources/global/ja/workflows/minimal.yaml
Normal file
@ -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: |
|
||||
監督者からの指摘を修正してください。
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
427
resources/global/ja/workflows/review-fix-minimal.yaml
Normal file
427
resources/global/ja/workflows/review-fix-minimal.yaml
Normal file
@ -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: |
|
||||
監督者からの指摘を修正してください。
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
@ -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
|
||||
@ -3,6 +3,7 @@ logs/
|
||||
reports/
|
||||
completed/
|
||||
tasks/
|
||||
worktrees/
|
||||
worktree-meta/
|
||||
clone-meta/
|
||||
worktree-sessions/
|
||||
|
||||
104
src/__tests__/bookmark.test.ts
Normal file
104
src/__tests__/bookmark.test.ts
Normal file
@ -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']);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)', () => {
|
||||
|
||||
301
src/__tests__/workflow-categories.test.ts
Normal file
301
src/__tests__/workflow-categories.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
214
src/__tests__/workflow-category-config.test.ts
Normal file
214
src/__tests__/workflow-category-config.test.ts
Normal file
@ -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<string, unknown>;
|
||||
return {
|
||||
...original,
|
||||
getGlobalConfigPath: () => pathsState.globalConfigPath,
|
||||
getProjectConfigPath: () => pathsState.projectConfigPath,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../infra/resources/index.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
return {
|
||||
...original,
|
||||
getLanguageResourcesDir: () => pathsState.resourcesDir,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
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<string, WorkflowConfig> {
|
||||
const workflows = new Map<string, WorkflowConfig>();
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@ -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<string, string[]>;
|
||||
/** Show uncategorized workflows under Others category */
|
||||
showOthersCategory?: boolean;
|
||||
/** Display name for Others category */
|
||||
othersCategoryName?: string;
|
||||
}
|
||||
|
||||
/** Project-level configuration */
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -2,28 +2,100 @@
|
||||
* 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 }[] {
|
||||
function createBookmarkCallback(
|
||||
items: ReturnType<typeof buildWorkflowSelectionItems>,
|
||||
currentWorkflow: string,
|
||||
): (value: string) => SelectOptionItem<string>[] {
|
||||
return (value: string): SelectOptionItem<string>[] => {
|
||||
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<string | null> {
|
||||
const current = getCurrentWorkflow(cwd);
|
||||
const workflows = listWorkflows(cwd);
|
||||
const entries = listWorkflowEntries(cwd);
|
||||
const items = buildWorkflowSelectionItems(entries);
|
||||
|
||||
const options: { label: string; value: string }[] = [];
|
||||
// Loop until user selects a workflow or cancels at top level
|
||||
while (true) {
|
||||
const baseOptions = buildTopLevelSelectOptions(items, current);
|
||||
const options = applyBookmarks(baseOptions, getBookmarkedWorkflows());
|
||||
|
||||
// Add all workflows
|
||||
for (const name of workflows) {
|
||||
const isCurrent = name === current;
|
||||
const label = isCurrent ? `${name} (current)` : name;
|
||||
options.push({ label, value: name });
|
||||
const selected = await selectOption<string>('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<string>(`Select workflow in ${categoryName}:`, bookmarkedInCategory, {
|
||||
cancelLabel: '← Go back',
|
||||
onBookmark: (value: string): SelectOptionItem<string>[] => {
|
||||
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 options;
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Switch to a different workflow
|
||||
@ -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');
|
||||
|
||||
@ -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<string | null> {
|
||||
async function selectWorkflowWithDirectoryCategories(cwd: string): Promise<string | null> {
|
||||
const availableWorkflows = listWorkflows(cwd);
|
||||
const currentWorkflow = getCurrentWorkflow(cwd);
|
||||
|
||||
@ -37,18 +59,91 @@ async function selectWorkflow(cwd: string): Promise<string | null> {
|
||||
return availableWorkflows[0];
|
||||
}
|
||||
|
||||
const options = availableWorkflows.map((name) => ({
|
||||
const entries = listWorkflowEntries(cwd);
|
||||
const items = buildWorkflowSelectionItems(entries);
|
||||
|
||||
const hasCategories = items.some((item) => item.type === 'category');
|
||||
|
||||
if (!hasCategories) {
|
||||
const baseOptions: SelectionOption[] = availableWorkflows.map((name) => ({
|
||||
label: name === currentWorkflow ? `${name} (current)` : name,
|
||||
value: name,
|
||||
}));
|
||||
|
||||
const defaultWorkflow = availableWorkflows.includes(currentWorkflow)
|
||||
? currentWorkflow
|
||||
: (availableWorkflows.includes(DEFAULT_WORKFLOW_NAME)
|
||||
? DEFAULT_WORKFLOW_NAME
|
||||
: availableWorkflows[0] || DEFAULT_WORKFLOW_NAME);
|
||||
const buildFlatOptions = (): SelectionOption[] =>
|
||||
applyBookmarks(baseOptions, getBookmarkedWorkflows());
|
||||
|
||||
return selectOptionWithDefault('Select workflow:', options, defaultWorkflow);
|
||||
return selectOption<string>('Select workflow:', buildFlatOptions(), {
|
||||
onBookmark: (value: string): SelectOptionItem<string>[] => {
|
||||
toggleBookmark(value);
|
||||
return buildFlatOptions();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const createTopLevelBookmarkCallback = (): ((value: string) => SelectOptionItem<string>[]) => {
|
||||
return (value: string): SelectOptionItem<string>[] => {
|
||||
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<string>('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<string>(`Select workflow in ${categoryName}:`, bookmarkedCategoryOptions, {
|
||||
cancelLabel: '← Go back',
|
||||
onBookmark: (value: string): SelectOptionItem<string>[] => {
|
||||
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<string | null> {
|
||||
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<string | null> {
|
||||
*/
|
||||
async function determineWorkflow(cwd: string, override?: string): Promise<string | null> {
|
||||
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) {
|
||||
|
||||
235
src/features/workflowSelection/index.ts
Normal file
235
src/features/workflowSelection/index.ts
Normal file
@ -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<string, string[]>();
|
||||
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<string>[] {
|
||||
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<string>[] | 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<string | null> {
|
||||
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<string>('Select workflow category:', categoryOptions);
|
||||
if (!selectedCategory) return null;
|
||||
|
||||
const buildWorkflowOptions = (): SelectOptionItem<string>[] | null =>
|
||||
buildWorkflowOptionsForCategory(categorized, selectedCategory, currentWorkflow);
|
||||
|
||||
const baseWorkflowOptions = buildWorkflowOptions();
|
||||
if (!baseWorkflowOptions) continue;
|
||||
|
||||
const applyWorkflowBookmarks = (options: SelectOptionItem<string>[]): SelectOptionItem<string>[] => {
|
||||
return applyBookmarks(options, getBookmarkedWorkflows()) as SelectOptionItem<string>[];
|
||||
};
|
||||
|
||||
const selectedWorkflow = await selectOption<string>(
|
||||
`Select workflow in ${selectedCategory}:`,
|
||||
applyWorkflowBookmarks(baseWorkflowOptions),
|
||||
{
|
||||
cancelLabel: '← Go back',
|
||||
onBookmark: (value: string): SelectOptionItem<string>[] => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ export {
|
||||
resolveOpenaiApiKey,
|
||||
loadProjectDebugConfig,
|
||||
getEffectiveDebugConfig,
|
||||
getBookmarkedWorkflows,
|
||||
toggleBookmark,
|
||||
} from './globalConfig.js';
|
||||
|
||||
export {
|
||||
|
||||
@ -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,
|
||||
|
||||
168
src/infra/config/loaders/workflowCategories.ts
Normal file
168
src/infra/config/loaders/workflowCategories.ts
Normal file
@ -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<string, string[]>;
|
||||
showOthersCategory: boolean;
|
||||
othersCategoryName: string;
|
||||
}
|
||||
|
||||
export interface CategorizedWorkflows {
|
||||
categories: Map<string, string[]>;
|
||||
allWorkflows: Map<string, WorkflowConfig>;
|
||||
missingWorkflows: MissingWorkflow[];
|
||||
}
|
||||
|
||||
export interface MissingWorkflow {
|
||||
categoryName: string;
|
||||
workflowName: string;
|
||||
}
|
||||
|
||||
interface RawCategoryConfig {
|
||||
workflow_categories?: Record<string, string[]>;
|
||||
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<string, WorkflowConfig>,
|
||||
config: CategoryConfig,
|
||||
): CategorizedWorkflows {
|
||||
const categories = new Map<string, string[]>();
|
||||
const categorized = new Set<string>();
|
||||
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;
|
||||
}
|
||||
@ -17,4 +17,6 @@ export {
|
||||
loadWorkflowByIdentifier,
|
||||
loadAllWorkflows,
|
||||
listWorkflows,
|
||||
listWorkflowEntries,
|
||||
type WorkflowDirEntry,
|
||||
} from './workflowResolver.js';
|
||||
|
||||
@ -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<WorkflowDirEntry> {
|
||||
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 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<string, WorkflowConfig> {
|
||||
|
||||
/**
|
||||
* 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<string>();
|
||||
@ -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<string, WorkflowDirEntry>();
|
||||
|
||||
for (const { dir, disabled } of getWorkflowDirs(cwd)) {
|
||||
for (const entry of iterateWorkflowDir(dir, disabled)) {
|
||||
workflows.set(entry.name, entry);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(workflows.values());
|
||||
}
|
||||
|
||||
@ -23,6 +23,12 @@ export interface ProjectLocalConfig {
|
||||
permissionMode?: PermissionMode;
|
||||
/** Verbose output mode */
|
||||
verbose?: boolean;
|
||||
/** Workflow categories (name -> workflow list) */
|
||||
workflow_categories?: Record<string, string[]>;
|
||||
/** Show uncategorized workflows under Others category */
|
||||
show_others_category?: boolean;
|
||||
/** Display name for Others category */
|
||||
others_category_name?: string;
|
||||
/** Custom settings */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
export {
|
||||
type SelectOptionItem,
|
||||
type InteractiveSelectCallbacks,
|
||||
renderMenu,
|
||||
countRenderedLines,
|
||||
type KeyInputResult,
|
||||
|
||||
@ -23,6 +23,7 @@ export function renderMenu<T extends string>(
|
||||
options: SelectOptionItem<T>[],
|
||||
selectedIndex: number,
|
||||
hasCancelOption: boolean,
|
||||
cancelLabel = 'Cancel',
|
||||
): string[] {
|
||||
const maxWidth = process.stdout.columns || 80;
|
||||
const labelPrefix = 4;
|
||||
@ -54,7 +55,7 @@ export function renderMenu<T extends string>(
|
||||
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<T extends string>(
|
||||
options: SelectOptionItem<T>[],
|
||||
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<T extends string> {
|
||||
/** Called when 'b' key is pressed. Returns updated options for re-render. */
|
||||
onBookmark?: (value: T, index: number) => SelectOptionItem<T>[];
|
||||
/** Custom label for cancel option (default: "Cancel") */
|
||||
cancelLabel?: string;
|
||||
}
|
||||
|
||||
/** Result of interactive selection */
|
||||
interface InteractiveSelectResult<T extends string> {
|
||||
selectedIndex: number;
|
||||
finalOptions: SelectOptionItem<T>[];
|
||||
}
|
||||
|
||||
/** Interactive cursor-based menu selection. */
|
||||
@ -159,22 +183,25 @@ function interactiveSelect<T extends string>(
|
||||
options: SelectOptionItem<T>[],
|
||||
initialIndex: number,
|
||||
hasCancelOption: boolean,
|
||||
): Promise<number> {
|
||||
callbacks?: InteractiveSelectCallbacks<T>,
|
||||
): Promise<InteractiveSelectResult<T>> {
|
||||
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<T extends string>(
|
||||
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<T extends string>(
|
||||
|
||||
/**
|
||||
* 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<T extends string>(
|
||||
message: string,
|
||||
options: SelectOptionItem<T>[],
|
||||
callbacks?: InteractiveSelectCallbacks<T>,
|
||||
): Promise<T | null> {
|
||||
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<T extends string>(
|
||||
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;
|
||||
|
||||
391
tools/jsonl-viewer.html
Normal file
391
tools/jsonl-viewer.html
Normal file
@ -0,0 +1,391 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TAKT JSONL Session Viewer</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
border: 2px dashed #007acc;
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
background: #252526;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.drop-zone:hover,
|
||||
.drop-zone.drag-over {
|
||||
background: #2d2d30;
|
||||
border-color: #0098ff;
|
||||
}
|
||||
|
||||
.drop-zone-text {
|
||||
font-size: 16px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
background: #252526;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-info.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.records {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.record {
|
||||
background: #252526;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #007acc;
|
||||
}
|
||||
|
||||
.record.workflow_start {
|
||||
border-left-color: #4ec9b0;
|
||||
}
|
||||
|
||||
.record.step_start {
|
||||
border-left-color: #dcdcaa;
|
||||
}
|
||||
|
||||
.record.step_complete {
|
||||
border-left-color: #608b4e;
|
||||
}
|
||||
|
||||
.record.workflow_complete {
|
||||
border-left-color: #4ec9b0;
|
||||
}
|
||||
|
||||
.record.workflow_abort {
|
||||
border-left-color: #f48771;
|
||||
}
|
||||
|
||||
.record.done {
|
||||
border-left-color: #608b4e;
|
||||
}
|
||||
|
||||
.record.blocked {
|
||||
border-left-color: #f48771;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.record-type {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.record-timestamp {
|
||||
font-size: 12px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.record-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.record-field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
color: #9cdcfe;
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.instruction {
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.toggle-instruction {
|
||||
color: #007acc;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toggle-instruction:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.instruction.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #f48771;
|
||||
color: #1e1e1e;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3e3e42;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e52;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>TAKT JSONL Session Viewer</h1>
|
||||
|
||||
<div class="drop-zone" id="dropZone">
|
||||
<div class="drop-zone-text">
|
||||
ここにJSONLファイルをドラッグ&ドロップ<br>
|
||||
またはクリックしてファイルを選択
|
||||
</div>
|
||||
<input type="file" id="fileInput" accept=".jsonl,.json" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div class="file-info" id="fileInfo">
|
||||
<div class="stats" id="stats"></div>
|
||||
</div>
|
||||
|
||||
<div class="records" id="records"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const statsDiv = document.getElementById('stats');
|
||||
const recordsDiv = document.getElementById('records');
|
||||
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) handleFile(file);
|
||||
});
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) handleFile(file);
|
||||
});
|
||||
|
||||
function handleFile(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target.result;
|
||||
const lines = content.trim().split('\n');
|
||||
const records = lines.map(line => JSON.parse(line));
|
||||
displayRecords(records);
|
||||
} catch (err) {
|
||||
recordsDiv.innerHTML = `<div class="error">Error parsing JSONL: ${err.message}</div>`;
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function displayRecords(records) {
|
||||
// Calculate stats
|
||||
const stats = {
|
||||
total: records.length,
|
||||
steps: records.filter(r => r.type === 'step_start').length,
|
||||
completed: records.filter(r => r.type === 'step_complete' || r.status === 'done').length,
|
||||
};
|
||||
|
||||
statsDiv.innerHTML = `
|
||||
<div class="stat">
|
||||
<span class="stat-label">Total Records</span>
|
||||
<span class="stat-value">${stats.total}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Steps Started</span>
|
||||
<span class="stat-value">${stats.steps}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Steps Completed</span>
|
||||
<span class="stat-value">${stats.completed}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fileInfo.classList.add('active');
|
||||
|
||||
// Display records
|
||||
recordsDiv.innerHTML = records.map((record, index) => {
|
||||
const hasInstruction = record.instruction && record.instruction.length > 0;
|
||||
const instructionId = `instruction-${index}`;
|
||||
const recordType = record.type || record.status || 'unknown';
|
||||
const recordClass = record.type || record.status || 'unknown';
|
||||
|
||||
return `
|
||||
<div class="record ${recordClass}">
|
||||
<div class="record-header">
|
||||
<span class="record-type">${recordType}</span>
|
||||
<span class="record-timestamp">${record.timestamp || ''}</span>
|
||||
</div>
|
||||
<div class="record-content">
|
||||
${formatRecordContent(record)}
|
||||
${hasInstruction ? `
|
||||
<div class="toggle-instruction" onclick="toggleInstruction('${instructionId}')">
|
||||
📄 Show instruction (${record.instruction.length} chars)
|
||||
</div>
|
||||
<div class="instruction collapsed" id="${instructionId}">${escapeHtml(record.instruction)}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function formatRecordContent(record) {
|
||||
const fields = [];
|
||||
|
||||
if (record.workflow) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Workflow:</span><span class="field-value">${escapeHtml(record.workflow)}</span></div>`);
|
||||
}
|
||||
if (record.step) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Step:</span><span class="field-value">${escapeHtml(record.step)}</span></div>`);
|
||||
}
|
||||
if (record.iteration !== undefined) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Iteration:</span><span class="field-value">${record.iteration}</span></div>`);
|
||||
}
|
||||
if (record.status) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Status:</span><span class="field-value">${escapeHtml(record.status)}</span></div>`);
|
||||
}
|
||||
if (record.matched_condition) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Matched Condition:</span><span class="field-value">${escapeHtml(record.matched_condition)}</span></div>`);
|
||||
}
|
||||
if (record.next_step) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Next Step:</span><span class="field-value">${escapeHtml(record.next_step)}</span></div>`);
|
||||
}
|
||||
if (record.match_method) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Match Method:</span><span class="field-value">${escapeHtml(record.match_method)}</span></div>`);
|
||||
}
|
||||
if (record.reason) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Reason:</span><span class="field-value">${escapeHtml(record.reason)}</span></div>`);
|
||||
}
|
||||
if (record.task) {
|
||||
fields.push(`<div class="record-field"><span class="field-label">Task:</span><span class="field-value">${escapeHtml(record.task.substring(0, 200))}${record.task.length > 200 ? '...' : ''}</span></div>`);
|
||||
}
|
||||
|
||||
return fields.join('');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (typeof text !== 'string') return String(text);
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
function toggleInstruction(id) {
|
||||
const element = document.getElementById(id);
|
||||
element.classList.toggle('collapsed');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user