カテゴリ分けの作成 #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)
|
- `~/.takt/workflows/` — User workflows (override builtins with the same name)
|
||||||
- Use `takt eject <workflow>` to copy a builtin to `~/.takt/workflows/` for customization
|
- Use `takt eject <workflow>` to copy a builtin to `~/.takt/workflows/` for customization
|
||||||
|
|
||||||
|
## Workflow Categories
|
||||||
|
|
||||||
|
ワークフローの選択 UI をカテゴリ分けしたい場合は、`workflow_categories` を設定します。
|
||||||
|
詳細は `docs/workflow-categories.md` を参照してください。
|
||||||
|
|
||||||
## Workflow Schema
|
## Workflow Schema
|
||||||
|
|
||||||
```yaml
|
```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
|
- name: implement
|
||||||
edit: true
|
edit: true
|
||||||
agent: ../agents/default/coder.md
|
agent: ../agents/default/coder.md
|
||||||
|
session: refresh
|
||||||
report:
|
report:
|
||||||
- Scope: 01-coder-scope.md
|
- Scope: 01-coder-scope.md
|
||||||
- Decisions: 02-coder-decisions.md
|
- Decisions: 02-coder-decisions.md
|
||||||
|
|||||||
@ -86,6 +86,7 @@ steps:
|
|||||||
- name: implement
|
- name: implement
|
||||||
edit: true
|
edit: true
|
||||||
agent: ../agents/default/coder.md
|
agent: ../agents/default/coder.md
|
||||||
|
session: refresh
|
||||||
report:
|
report:
|
||||||
- Scope: 01-coder-scope.md
|
- Scope: 01-coder-scope.md
|
||||||
- Decisions: 02-coder-decisions.md
|
- Decisions: 02-coder-decisions.md
|
||||||
|
|||||||
@ -98,6 +98,7 @@ steps:
|
|||||||
- name: implement
|
- name: implement
|
||||||
edit: true
|
edit: true
|
||||||
agent: ../agents/default/coder.md
|
agent: ../agents/default/coder.md
|
||||||
|
session: refresh
|
||||||
report:
|
report:
|
||||||
- Scope: 01-coder-scope.md
|
- Scope: 01-coder-scope.md
|
||||||
- Decisions: 02-coder-decisions.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
|
- name: implement
|
||||||
edit: true
|
edit: true
|
||||||
agent: ../agents/default/coder.md
|
agent: ../agents/default/coder.md
|
||||||
|
session: refresh
|
||||||
report:
|
report:
|
||||||
- Scope: 01-coder-scope.md
|
- Scope: 01-coder-scope.md
|
||||||
- Decisions: 02-coder-decisions.md
|
- Decisions: 02-coder-decisions.md
|
||||||
|
|||||||
@ -95,6 +95,7 @@ steps:
|
|||||||
- name: implement
|
- name: implement
|
||||||
edit: true
|
edit: true
|
||||||
agent: ../agents/default/coder.md
|
agent: ../agents/default/coder.md
|
||||||
|
session: refresh
|
||||||
report:
|
report:
|
||||||
- Scope: 01-coder-scope.md
|
- Scope: 01-coder-scope.md
|
||||||
- Decisions: 02-coder-decisions.md
|
- Decisions: 02-coder-decisions.md
|
||||||
|
|||||||
@ -86,6 +86,7 @@ steps:
|
|||||||
- name: implement
|
- name: implement
|
||||||
edit: true
|
edit: true
|
||||||
agent: ../agents/default/coder.md
|
agent: ../agents/default/coder.md
|
||||||
|
session: refresh
|
||||||
report:
|
report:
|
||||||
- Scope: 01-coder-scope.md
|
- Scope: 01-coder-scope.md
|
||||||
- Decisions: 02-coder-decisions.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/
|
reports/
|
||||||
completed/
|
completed/
|
||||||
tasks/
|
tasks/
|
||||||
|
worktrees/
|
||||||
worktree-meta/
|
worktree-meta/
|
||||||
clone-meta/
|
clone-meta/
|
||||||
worktree-sessions/
|
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();
|
expect(workflow).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load builtin workflows like simple, research', () => {
|
it('should load builtin workflows like minimal, research', () => {
|
||||||
const simple = loadWorkflow('simple', process.cwd());
|
const minimal = loadWorkflow('minimal', process.cwd());
|
||||||
expect(simple).not.toBeNull();
|
expect(minimal).not.toBeNull();
|
||||||
expect(simple!.name).toBe('simple');
|
expect(minimal!.name).toBe('minimal');
|
||||||
|
|
||||||
const research = loadWorkflow('research', process.cwd());
|
const research = loadWorkflow('research', process.cwd());
|
||||||
expect(research).not.toBeNull();
|
expect(research).not.toBeNull();
|
||||||
@ -236,7 +236,7 @@ describe('listWorkflows (builtin fallback)', () => {
|
|||||||
it('should include builtin workflows', () => {
|
it('should include builtin workflows', () => {
|
||||||
const workflows = listWorkflows(testDir);
|
const workflows = listWorkflows(testDir);
|
||||||
expect(workflows).toContain('default');
|
expect(workflows).toContain('default');
|
||||||
expect(workflows).toContain('simple');
|
expect(workflows).toContain('minimal');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return sorted list', () => {
|
it('should return sorted list', () => {
|
||||||
@ -263,7 +263,7 @@ describe('loadAllWorkflows (builtin fallback)', () => {
|
|||||||
it('should include builtin workflows in the map', () => {
|
it('should include builtin workflows in the map', () => {
|
||||||
const workflows = loadAllWorkflows(testDir);
|
const workflows = loadAllWorkflows(testDir);
|
||||||
expect(workflows.has('default')).toBe(true);
|
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 });
|
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([
|
setMockScenario([
|
||||||
{ agent: 'planner', status: 'done', content: '[PLAN:1]\n\nRequirements are clear.' },
|
|
||||||
{ agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' },
|
{ agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' },
|
||||||
{ agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:1]\n\nNo issues.' },
|
{ agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:0]\n\nNo AI-specific issues.' },
|
||||||
{ agent: 'architecture-reviewer', status: 'done', content: '[REVIEW:1]\n\nNo issues found.' },
|
{ agent: 'supervisor', status: 'done', content: '[SUPERVISE:0]\n\nAll checks passed.' },
|
||||||
{ agent: 'supervisor', status: 'done', content: '[SUPERVISE:1]\n\nAll checks passed.' },
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const exitCode = await executePipeline({
|
const exitCode = await executePipeline({
|
||||||
task: 'Add a feature',
|
task: 'Add a feature',
|
||||||
workflow: 'simple',
|
workflow: 'minimal',
|
||||||
autoPr: false,
|
autoPr: false,
|
||||||
skipGit: true,
|
skipGit: true,
|
||||||
cwd: testDir,
|
cwd: testDir,
|
||||||
|
|||||||
@ -221,20 +221,18 @@ describe('Pipeline Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should complete pipeline with workflow name + skip-git + mock scenario', async () => {
|
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)
|
// agent field: extractAgentName result (from .md filename)
|
||||||
// tag in content: [STEP_NAME:N] where STEP_NAME is the step name uppercased
|
// tag in content: [STEP_NAME:N] where STEP_NAME is the step name uppercased
|
||||||
setMockScenario([
|
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: '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: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:0]\n\nNo AI-specific issues.' },
|
||||||
{ agent: 'architecture-reviewer', status: 'done', content: '[REVIEW:1]\n\nNo issues found.' },
|
{ agent: 'supervisor', status: 'done', content: '[SUPERVISE:0]\n\nAll checks passed.' },
|
||||||
{ agent: 'supervisor', status: 'done', content: '[SUPERVISE:1]\n\nAll checks passed.' },
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const exitCode = await executePipeline({
|
const exitCode = await executePipeline({
|
||||||
task: 'Add a hello world function',
|
task: 'Add a hello world function',
|
||||||
workflow: 'simple',
|
workflow: 'minimal',
|
||||||
autoPr: false,
|
autoPr: false,
|
||||||
skipGit: true,
|
skipGit: true,
|
||||||
cwd: testDir,
|
cwd: testDir,
|
||||||
|
|||||||
@ -44,7 +44,7 @@ describe('Workflow Loader IT: builtin workflow loading', () => {
|
|||||||
rmSync(testDir, { recursive: true, force: true });
|
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) {
|
for (const name of builtinNames) {
|
||||||
it(`should load builtin workflow: ${name}`, () => {
|
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', () => {
|
it('should resolve relative agent paths from workflow YAML location', () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
|
|
||||||
for (const step of config!.steps) {
|
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', () => {
|
it('should parse standard rules with next step', () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
|
|
||||||
const planStep = config!.steps.find((s) => s.name === 'plan');
|
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', () => {
|
it('should set max_iterations from YAML', () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(typeof config!.maxIterations).toBe('number');
|
expect(typeof config!.maxIterations).toBe('number');
|
||||||
expect(config!.maxIterations).toBeGreaterThan(0);
|
expect(config!.maxIterations).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set initial_step from YAML', () => {
|
it('should set initial_step from YAML', () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(typeof config!.initialStep).toBe('string');
|
expect(typeof config!.initialStep).toBe('string');
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ describe('Workflow Loader IT: workflow config validation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set passPreviousResponse from YAML', () => {
|
it('should set passPreviousResponse from YAML', () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
|
|
||||||
// At least some steps should have passPreviousResponse set
|
// 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', () => {
|
it('should load single report config', () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
|
|
||||||
// simple workflow: plan step has a report config
|
// 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;
|
let testDir: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -83,30 +83,28 @@ describe('Workflow Patterns IT: simple workflow', () => {
|
|||||||
rmSync(testDir, { recursive: true, force: true });
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should complete: plan → implement → ai_review → review → supervise → COMPLETE', async () => {
|
it('should complete: implement → reviewers (parallel: ai_review + supervise) → COMPLETE', async () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
|
|
||||||
setMockScenario([
|
setMockScenario([
|
||||||
{ agent: 'planner', status: 'done', content: '[PLAN:1]\n\nRequirements are clear.' },
|
{ agent: 'coder', status: 'done', content: '[IMPLEMENT:0]\n\nImplementation complete.' },
|
||||||
{ agent: 'coder', status: 'done', content: '[IMPLEMENT:1]\n\nImplementation complete.' },
|
{ agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:0]\n\nNo AI-specific issues.' },
|
||||||
{ agent: 'ai-antipattern-reviewer', status: 'done', content: '[AI_REVIEW:1]\n\nNo AI-specific issues.' },
|
{ agent: 'supervisor', status: 'done', content: '[SUPERVISE:0]\n\nAll checks passed.' },
|
||||||
{ agent: 'architecture-reviewer', status: 'done', content: '[REVIEW:1]\n\nNo issues found.' },
|
|
||||||
{ agent: 'supervisor', status: 'done', content: '[SUPERVISE:1]\n\nAll checks passed.' },
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const engine = createEngine(config!, testDir, 'Test task');
|
const engine = createEngine(config!, testDir, 'Test task');
|
||||||
const state = await engine.run();
|
const state = await engine.run();
|
||||||
|
|
||||||
expect(state.status).toBe('completed');
|
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 () => {
|
it('should ABORT when implement cannot proceed', async () => {
|
||||||
const config = loadWorkflow('simple', testDir);
|
const config = loadWorkflow('minimal', testDir);
|
||||||
|
|
||||||
setMockScenario([
|
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');
|
const engine = createEngine(config!, testDir, 'Vague task');
|
||||||
@ -116,19 +114,6 @@ describe('Workflow Patterns IT: simple workflow', () => {
|
|||||||
expect(state.iteration).toBe(1);
|
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)', () => {
|
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;
|
pipeline?: PipelineConfig;
|
||||||
/** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */
|
/** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */
|
||||||
minimalOutput?: boolean;
|
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 */
|
/** Project-level configuration */
|
||||||
|
|||||||
@ -219,6 +219,14 @@ export const GlobalConfigSchema = z.object({
|
|||||||
pipeline: PipelineConfigSchema.optional(),
|
pipeline: PipelineConfigSchema.optional(),
|
||||||
/** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */
|
/** Minimal output mode for CI - suppress AI output to prevent sensitive information leaks */
|
||||||
minimal_output: z.boolean().optional().default(false),
|
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 */
|
/** Project config schema */
|
||||||
|
|||||||
@ -2,29 +2,101 @@
|
|||||||
* Workflow switching command
|
* 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 { info, success, error } from '../../shared/ui/index.js';
|
||||||
import { selectOption } from '../../shared/prompt/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(
|
||||||
const current = getCurrentWorkflow(cwd);
|
items: ReturnType<typeof buildWorkflowSelectionItems>,
|
||||||
const workflows = listWorkflows(cwd);
|
currentWorkflow: string,
|
||||||
|
): (value: string) => SelectOptionItem<string>[] {
|
||||||
const options: { label: string; value: string }[] = [];
|
return (value: string): SelectOptionItem<string>[] => {
|
||||||
|
const categoryName = parseCategorySelection(value);
|
||||||
// Add all workflows
|
if (categoryName) {
|
||||||
for (const name of workflows) {
|
return applyBookmarks(
|
||||||
const isCurrent = name === current;
|
buildTopLevelSelectOptions(items, currentWorkflow),
|
||||||
const label = isCurrent ? `${name} (current)` : name;
|
getBookmarkedWorkflows(),
|
||||||
options.push({ label, value: name });
|
);
|
||||||
}
|
}
|
||||||
|
toggleBookmark(value);
|
||||||
return options;
|
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 entries = listWorkflowEntries(cwd);
|
||||||
|
const items = buildWorkflowSelectionItems(entries);
|
||||||
|
|
||||||
|
// Loop until user selects a workflow or cancels at top level
|
||||||
|
while (true) {
|
||||||
|
const baseOptions = buildTopLevelSelectOptions(items, current);
|
||||||
|
const options = applyBookmarks(baseOptions, getBookmarkedWorkflows());
|
||||||
|
|
||||||
|
const selected = await selectOption<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 selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch to a different workflow
|
* Switch to a different workflow
|
||||||
* @returns true if switch was successful
|
* @returns true if switch was successful
|
||||||
@ -35,8 +107,21 @@ export async function switchWorkflow(cwd: string, workflowName?: string): Promis
|
|||||||
const current = getCurrentWorkflow(cwd);
|
const current = getCurrentWorkflow(cwd);
|
||||||
info(`Current workflow: ${current}`);
|
info(`Current workflow: ${current}`);
|
||||||
|
|
||||||
const options = getAllWorkflowOptions(cwd);
|
const categoryConfig = getWorkflowCategories(cwd);
|
||||||
const selected = await selectOption('Select workflow:', options);
|
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) {
|
if (!selected) {
|
||||||
info('Cancelled');
|
info('Cancelled');
|
||||||
|
|||||||
@ -6,8 +6,21 @@
|
|||||||
* mixing CLI parsing with business logic.
|
* mixing CLI parsing with business logic.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentWorkflow, listWorkflows, isWorkflowPath } from '../../../infra/config/index.js';
|
import {
|
||||||
import { selectOptionWithDefault, confirm } from '../../../shared/prompt/index.js';
|
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 { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js';
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js';
|
||||||
import { info, error, success } from '../../../shared/ui/index.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 { createPullRequest, buildPrBody } from '../../../infra/github/index.js';
|
||||||
import { executeTask } from './taskExecution.js';
|
import { executeTask } from './taskExecution.js';
|
||||||
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js';
|
||||||
|
import {
|
||||||
|
buildWorkflowSelectionItems,
|
||||||
|
buildTopLevelSelectOptions,
|
||||||
|
parseCategorySelection,
|
||||||
|
buildCategoryWorkflowOptions,
|
||||||
|
applyBookmarks,
|
||||||
|
warnMissingWorkflows,
|
||||||
|
selectWorkflowFromCategorizedWorkflows,
|
||||||
|
type SelectionOption,
|
||||||
|
} from '../../workflowSelection/index.js';
|
||||||
|
|
||||||
export type { WorktreeConfirmationResult, SelectAndExecuteOptions };
|
export type { WorktreeConfirmationResult, SelectAndExecuteOptions };
|
||||||
|
|
||||||
const log = createLogger('selectAndExecute');
|
const log = createLogger('selectAndExecute');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a workflow interactively.
|
* Select a workflow interactively with directory categories and bookmarks.
|
||||||
* Returns the selected workflow name, or null if cancelled.
|
|
||||||
*/
|
*/
|
||||||
async function selectWorkflow(cwd: string): Promise<string | null> {
|
async function selectWorkflowWithDirectoryCategories(cwd: string): Promise<string | null> {
|
||||||
const availableWorkflows = listWorkflows(cwd);
|
const availableWorkflows = listWorkflows(cwd);
|
||||||
const currentWorkflow = getCurrentWorkflow(cwd);
|
const currentWorkflow = getCurrentWorkflow(cwd);
|
||||||
|
|
||||||
@ -37,18 +59,91 @@ async function selectWorkflow(cwd: string): Promise<string | null> {
|
|||||||
return availableWorkflows[0];
|
return availableWorkflows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = availableWorkflows.map((name) => ({
|
const entries = listWorkflowEntries(cwd);
|
||||||
label: name === currentWorkflow ? `${name} (current)` : name,
|
const items = buildWorkflowSelectionItems(entries);
|
||||||
value: name,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const defaultWorkflow = availableWorkflows.includes(currentWorkflow)
|
const hasCategories = items.some((item) => item.type === 'category');
|
||||||
? currentWorkflow
|
|
||||||
: (availableWorkflows.includes(DEFAULT_WORKFLOW_NAME)
|
|
||||||
? DEFAULT_WORKFLOW_NAME
|
|
||||||
: availableWorkflows[0] || DEFAULT_WORKFLOW_NAME);
|
|
||||||
|
|
||||||
return selectOptionWithDefault('Select workflow:', options, defaultWorkflow);
|
if (!hasCategories) {
|
||||||
|
const baseOptions: SelectionOption[] = availableWorkflows.map((name) => ({
|
||||||
|
label: name === currentWorkflow ? `${name} (current)` : name,
|
||||||
|
value: name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const buildFlatOptions = (): SelectionOption[] =>
|
||||||
|
applyBookmarks(baseOptions, getBookmarkedWorkflows());
|
||||||
|
|
||||||
|
return selectOption<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> {
|
async function determineWorkflow(cwd: string, override?: string): Promise<string | null> {
|
||||||
if (override) {
|
if (override) {
|
||||||
// Path-based: skip name validation (loader handles existence check)
|
|
||||||
if (isWorkflowPath(override)) {
|
if (isWorkflowPath(override)) {
|
||||||
return override;
|
return override;
|
||||||
}
|
}
|
||||||
// Name-based: validate workflow name exists
|
|
||||||
const availableWorkflows = listWorkflows(cwd);
|
const availableWorkflows = listWorkflows(cwd);
|
||||||
const knownWorkflows = availableWorkflows.length === 0 ? [DEFAULT_WORKFLOW_NAME] : availableWorkflows;
|
const knownWorkflows = availableWorkflows.length === 0 ? [DEFAULT_WORKFLOW_NAME] : availableWorkflows;
|
||||||
if (!knownWorkflows.includes(override)) {
|
if (!knownWorkflows.includes(override)) {
|
||||||
@ -90,7 +183,6 @@ export async function confirmAndCreateWorktree(
|
|||||||
return { execCwd: cwd, isWorktree: false };
|
return { execCwd: cwd, isWorktree: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summarize task name to English slug using AI
|
|
||||||
info('Generating branch name...');
|
info('Generating branch name...');
|
||||||
const taskSlug = await summarizeTaskName(task, { cwd });
|
const taskSlug = await summarizeTaskName(task, { cwd });
|
||||||
|
|
||||||
@ -144,7 +236,6 @@ export async function selectAndExecuteTask(
|
|||||||
error(`Auto-commit failed: ${commitResult.message}`);
|
error(`Auto-commit failed: ${commitResult.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PR creation: --auto-pr → create automatically, otherwise ask
|
|
||||||
if (commitResult.success && commitResult.commitHash && branch) {
|
if (commitResult.success && commitResult.commitHash && branch) {
|
||||||
const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false);
|
const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false);
|
||||||
if (shouldCreatePr) {
|
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,
|
prBodyTemplate: parsed.pipeline.pr_body_template,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
minimalOutput: parsed.minimal_output,
|
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;
|
this.cachedConfig = config;
|
||||||
return config;
|
return config;
|
||||||
@ -134,6 +138,18 @@ export class GlobalConfigManager {
|
|||||||
if (config.minimalOutput !== undefined) {
|
if (config.minimalOutput !== undefined) {
|
||||||
raw.minimal_output = config.minimalOutput;
|
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');
|
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
||||||
this.invalidateCache();
|
this.invalidateCache();
|
||||||
}
|
}
|
||||||
@ -270,3 +286,26 @@ export function getEffectiveDebugConfig(projectDir?: string): DebugConfig | unde
|
|||||||
|
|
||||||
return debugConfig;
|
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,
|
resolveOpenaiApiKey,
|
||||||
loadProjectDebugConfig,
|
loadProjectDebugConfig,
|
||||||
getEffectiveDebugConfig,
|
getEffectiveDebugConfig,
|
||||||
|
getBookmarkedWorkflows,
|
||||||
|
toggleBookmark,
|
||||||
} from './globalConfig.js';
|
} from './globalConfig.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -9,8 +9,20 @@ export {
|
|||||||
isWorkflowPath,
|
isWorkflowPath,
|
||||||
loadAllWorkflows,
|
loadAllWorkflows,
|
||||||
listWorkflows,
|
listWorkflows,
|
||||||
|
listWorkflowEntries,
|
||||||
|
type WorkflowDirEntry,
|
||||||
} from './workflowLoader.js';
|
} from './workflowLoader.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
loadDefaultCategories,
|
||||||
|
getWorkflowCategories,
|
||||||
|
buildCategorizedWorkflows,
|
||||||
|
findWorkflowCategories,
|
||||||
|
type CategoryConfig,
|
||||||
|
type CategorizedWorkflows,
|
||||||
|
type MissingWorkflow,
|
||||||
|
} from './workflowCategories.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
loadAgentsFromDir,
|
loadAgentsFromDir,
|
||||||
loadCustomAgents,
|
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,
|
loadWorkflowByIdentifier,
|
||||||
loadAllWorkflows,
|
loadAllWorkflows,
|
||||||
listWorkflows,
|
listWorkflows,
|
||||||
|
listWorkflowEntries,
|
||||||
|
type WorkflowDirEntry,
|
||||||
} from './workflowResolver.js';
|
} from './workflowResolver.js';
|
||||||
|
|||||||
@ -58,8 +58,22 @@ function loadWorkflowFromPath(
|
|||||||
return loadWorkflowFromFile(resolvedPath);
|
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).
|
* Load workflow by name (name-based loading only, no path detection).
|
||||||
|
* Supports category/name identifiers (e.g. "frontend/react").
|
||||||
*
|
*
|
||||||
* Priority:
|
* Priority:
|
||||||
* 1. Project-local workflows → .takt/workflows/{name}.yaml
|
* 1. Project-local workflows → .takt/workflows/{name}.yaml
|
||||||
@ -71,15 +85,15 @@ export function loadWorkflow(
|
|||||||
projectCwd: string,
|
projectCwd: string,
|
||||||
): WorkflowConfig | null {
|
): WorkflowConfig | null {
|
||||||
const projectWorkflowsDir = join(getProjectConfigDir(projectCwd), 'workflows');
|
const projectWorkflowsDir = join(getProjectConfigDir(projectCwd), 'workflows');
|
||||||
const projectWorkflowPath = join(projectWorkflowsDir, `${name}.yaml`);
|
const projectMatch = resolveWorkflowFile(projectWorkflowsDir, name);
|
||||||
if (existsSync(projectWorkflowPath)) {
|
if (projectMatch) {
|
||||||
return loadWorkflowFromFile(projectWorkflowPath);
|
return loadWorkflowFromFile(projectMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalWorkflowsDir = getGlobalWorkflowsDir();
|
const globalWorkflowsDir = getGlobalWorkflowsDir();
|
||||||
const workflowYamlPath = join(globalWorkflowsDir, `${name}.yaml`);
|
const globalMatch = resolveWorkflowFile(globalWorkflowsDir, name);
|
||||||
if (existsSync(workflowYamlPath)) {
|
if (globalMatch) {
|
||||||
return loadWorkflowFromFile(workflowYamlPath);
|
return loadWorkflowFromFile(globalMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBuiltinWorkflow(name);
|
return getBuiltinWorkflow(name);
|
||||||
@ -113,13 +127,18 @@ export function loadWorkflowByIdentifier(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Entry for a workflow file found in a directory */
|
/** Entry for a workflow file found in a directory */
|
||||||
interface WorkflowDirEntry {
|
export interface WorkflowDirEntry {
|
||||||
|
/** Workflow name (e.g. "react") */
|
||||||
name: string;
|
name: string;
|
||||||
|
/** Full file path */
|
||||||
path: string;
|
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.
|
* Shared by both loadAllWorkflows and listWorkflows to avoid DRY violation.
|
||||||
*/
|
*/
|
||||||
function* iterateWorkflowDir(
|
function* iterateWorkflowDir(
|
||||||
@ -128,12 +147,29 @@ function* iterateWorkflowDir(
|
|||||||
): Generator<WorkflowDirEntry> {
|
): Generator<WorkflowDirEntry> {
|
||||||
if (!existsSync(dir)) return;
|
if (!existsSync(dir)) return;
|
||||||
for (const entry of readdirSync(dir)) {
|
for (const entry of readdirSync(dir)) {
|
||||||
if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue;
|
|
||||||
const entryPath = join(dir, entry);
|
const entryPath = join(dir, entry);
|
||||||
if (!statSync(entryPath).isFile()) continue;
|
const stat = statSync(entryPath);
|
||||||
const workflowName = entry.replace(/\.ya?ml$/, '');
|
|
||||||
if (disabled?.includes(workflowName)) continue;
|
if (stat.isFile() && (entry.endsWith('.yaml') || entry.endsWith('.yml'))) {
|
||||||
yield { name: workflowName, path: entryPath };
|
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).
|
* List available workflow names (builtin + user + project-local, excluding disabled).
|
||||||
|
* Category workflows use qualified names like "frontend/react".
|
||||||
*/
|
*/
|
||||||
export function listWorkflows(cwd: string): string[] {
|
export function listWorkflows(cwd: string): string[] {
|
||||||
const workflows = new Set<string>();
|
const workflows = new Set<string>();
|
||||||
@ -186,3 +223,23 @@ export function listWorkflows(cwd: string): string[] {
|
|||||||
|
|
||||||
return Array.from(workflows).sort();
|
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;
|
permissionMode?: PermissionMode;
|
||||||
/** Verbose output mode */
|
/** Verbose output mode */
|
||||||
verbose?: boolean;
|
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 */
|
/** Custom settings */
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
type SelectOptionItem,
|
type SelectOptionItem,
|
||||||
|
type InteractiveSelectCallbacks,
|
||||||
renderMenu,
|
renderMenu,
|
||||||
countRenderedLines,
|
countRenderedLines,
|
||||||
type KeyInputResult,
|
type KeyInputResult,
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export function renderMenu<T extends string>(
|
|||||||
options: SelectOptionItem<T>[],
|
options: SelectOptionItem<T>[],
|
||||||
selectedIndex: number,
|
selectedIndex: number,
|
||||||
hasCancelOption: boolean,
|
hasCancelOption: boolean,
|
||||||
|
cancelLabel = 'Cancel',
|
||||||
): string[] {
|
): string[] {
|
||||||
const maxWidth = process.stdout.columns || 80;
|
const maxWidth = process.stdout.columns || 80;
|
||||||
const labelPrefix = 4;
|
const labelPrefix = 4;
|
||||||
@ -54,7 +55,7 @@ export function renderMenu<T extends string>(
|
|||||||
if (hasCancelOption) {
|
if (hasCancelOption) {
|
||||||
const isCancelSelected = selectedIndex === options.length;
|
const isCancelSelected = selectedIndex === options.length;
|
||||||
const cursor = isCancelSelected ? chalk.cyan('❯') : ' ';
|
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}`);
|
lines.push(` ${cursor} ${label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +85,7 @@ export type KeyInputResult =
|
|||||||
| { action: 'move'; newIndex: number }
|
| { action: 'move'; newIndex: number }
|
||||||
| { action: 'confirm'; selectedIndex: number }
|
| { action: 'confirm'; selectedIndex: number }
|
||||||
| { action: 'cancel'; cancelIndex: number }
|
| { action: 'cancel'; cancelIndex: number }
|
||||||
|
| { action: 'bookmark'; selectedIndex: number }
|
||||||
| { action: 'exit' }
|
| { action: 'exit' }
|
||||||
| { action: 'none' };
|
| { action: 'none' };
|
||||||
|
|
||||||
@ -113,14 +115,20 @@ export function handleKeyInput(
|
|||||||
if (key === '\x1B') {
|
if (key === '\x1B') {
|
||||||
return { action: 'cancel', cancelIndex: hasCancelOption ? optionCount : -1 };
|
return { action: 'cancel', cancelIndex: hasCancelOption ? optionCount : -1 };
|
||||||
}
|
}
|
||||||
|
if (key === 'b') {
|
||||||
|
return { action: 'bookmark', selectedIndex: currentIndex };
|
||||||
|
}
|
||||||
return { action: 'none' };
|
return { action: 'none' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Print the menu header (message + hint). */
|
/** Print the menu header (message + hint). */
|
||||||
function printHeader(message: string): void {
|
function printHeader(message: string, showBookmarkHint: boolean): void {
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.cyan(message));
|
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();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,12 +153,28 @@ function redrawMenu<T extends string>(
|
|||||||
options: SelectOptionItem<T>[],
|
options: SelectOptionItem<T>[],
|
||||||
selectedIndex: number,
|
selectedIndex: number,
|
||||||
hasCancelOption: boolean,
|
hasCancelOption: boolean,
|
||||||
totalLines: number,
|
prevTotalLines: number,
|
||||||
): void {
|
cancelLabel?: string,
|
||||||
process.stdout.write(`\x1B[${totalLines}A`);
|
): number {
|
||||||
|
process.stdout.write(`\x1B[${prevTotalLines}A`);
|
||||||
process.stdout.write('\x1B[J');
|
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');
|
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. */
|
/** Interactive cursor-based menu selection. */
|
||||||
@ -159,22 +183,25 @@ function interactiveSelect<T extends string>(
|
|||||||
options: SelectOptionItem<T>[],
|
options: SelectOptionItem<T>[],
|
||||||
initialIndex: number,
|
initialIndex: number,
|
||||||
hasCancelOption: boolean,
|
hasCancelOption: boolean,
|
||||||
): Promise<number> {
|
callbacks?: InteractiveSelectCallbacks<T>,
|
||||||
|
): Promise<InteractiveSelectResult<T>> {
|
||||||
return new Promise((resolve) => {
|
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;
|
let selectedIndex = initialIndex;
|
||||||
|
const cancelLabel = callbacks?.cancelLabel ?? 'Cancel';
|
||||||
|
|
||||||
printHeader(message);
|
printHeader(message, !!callbacks?.onBookmark);
|
||||||
|
|
||||||
process.stdout.write('\x1B[?7l');
|
process.stdout.write('\x1B[?7l');
|
||||||
|
|
||||||
const totalLines = countRenderedLines(options, hasCancelOption);
|
let totalLines = countRenderedLines(currentOptions, hasCancelOption);
|
||||||
const lines = renderMenu(options, selectedIndex, hasCancelOption);
|
const lines = renderMenu(currentOptions, selectedIndex, hasCancelOption, cancelLabel);
|
||||||
process.stdout.write(lines.join('\n') + '\n');
|
process.stdout.write(lines.join('\n') + '\n');
|
||||||
|
|
||||||
if (!process.stdin.isTTY) {
|
if (!process.stdin.isTTY) {
|
||||||
process.stdout.write('\x1B[?7h');
|
process.stdout.write('\x1B[?7h');
|
||||||
resolve(initialIndex);
|
resolve({ selectedIndex: initialIndex, finalOptions: currentOptions });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,22 +218,38 @@ function interactiveSelect<T extends string>(
|
|||||||
selectedIndex,
|
selectedIndex,
|
||||||
totalItems,
|
totalItems,
|
||||||
hasCancelOption,
|
hasCancelOption,
|
||||||
options.length,
|
currentOptions.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (result.action) {
|
switch (result.action) {
|
||||||
case 'move':
|
case 'move':
|
||||||
selectedIndex = result.newIndex;
|
selectedIndex = result.newIndex;
|
||||||
redrawMenu(options, selectedIndex, hasCancelOption, totalLines);
|
totalLines = redrawMenu(currentOptions, selectedIndex, hasCancelOption, totalLines, cancelLabel);
|
||||||
break;
|
break;
|
||||||
case 'confirm':
|
case 'confirm':
|
||||||
cleanup(onKeypress);
|
cleanup(onKeypress);
|
||||||
resolve(result.selectedIndex);
|
resolve({ selectedIndex: result.selectedIndex, finalOptions: currentOptions });
|
||||||
break;
|
break;
|
||||||
case 'cancel':
|
case 'cancel':
|
||||||
cleanup(onKeypress);
|
cleanup(onKeypress);
|
||||||
resolve(result.cancelIndex);
|
resolve({ selectedIndex: result.cancelIndex, finalOptions: currentOptions });
|
||||||
break;
|
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':
|
case 'exit':
|
||||||
cleanup(onKeypress);
|
cleanup(onKeypress);
|
||||||
process.exit(130);
|
process.exit(130);
|
||||||
@ -222,21 +265,23 @@ function interactiveSelect<T extends string>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt user to select from a list of options using cursor navigation.
|
* 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
|
* @returns Selected option or null if cancelled
|
||||||
*/
|
*/
|
||||||
export async function selectOption<T extends string>(
|
export async function selectOption<T extends string>(
|
||||||
message: string,
|
message: string,
|
||||||
options: SelectOptionItem<T>[],
|
options: SelectOptionItem<T>[],
|
||||||
|
callbacks?: InteractiveSelectCallbacks<T>,
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
if (options.length === 0) return 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = options[selectedIndex];
|
const selected = finalOptions[selectedIndex];
|
||||||
if (selected) {
|
if (selected) {
|
||||||
console.log(chalk.green(` ✓ ${selected.label}`));
|
console.log(chalk.green(` ✓ ${selected.label}`));
|
||||||
return selected.value;
|
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,
|
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) {
|
if (selectedIndex === options.length || selectedIndex === -1) {
|
||||||
return null;
|
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