カテゴリ分けの作成 #85 workflowの修正

This commit is contained in:
nrslib 2026-02-03 10:23:49 +09:00
parent d8dbcd01ff
commit def50ff4a7
42 changed files with 3641 additions and 1333 deletions

View File

@ -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/cliCLI入口・配線
```
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` へ統合する。

View File

@ -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 APIindex.ts整理ポイント
### 既存 Public API
- `src/core/models/index.ts`
- `src/core/workflow/index.ts`
- `src/features/tasks/index.ts`
- `src/features/pipeline/index.ts`
- `src/features/config/index.ts`
- `src/features/interactive/index.ts`
- `src/infra/config/index.ts`
- `src/infra/task/index.ts`
- `src/infra/providers/index.ts`
- `src/shared/utils/index.ts`
- `src/shared/ui/index.ts`
- `src/index.ts`
### 新設/拡張が必要な Public API
- `src/infra/github/index.ts``issue.ts`, `pr.ts`, `types.ts` の集約)
- `src/infra/fs/index.ts``session.ts` の集約)
- `src/infra/resources/index.ts`resources API の集約)
- `src/infra/config/index.ts` の拡張(`globalConfig`, `projectConfig`, `workflowLoader` などの再エクスポート)
- `src/shared/prompt/index.ts`(共通プロンプトの入口)
- `src/shared/constants.ts`, `src/shared/context.ts`, `src/shared/exitCodes.ts` の Public API 反映
- `src/infra/claude/index.ts`, `src/infra/codex/index.ts`, `src/infra/mock/index.ts`(移動後の入口)
### 深い import 禁止の置換方針
- `core/*``features/*` は Public API`index.ts`)からのみ import。
- `features` から `infra` の deep import を廃止し、`infra/*/index.ts` 経由に置換。
- `app/cli` から `infra` への direct import は必要最小限に限定し、可能なら `features` Public API に集約。
---
## 影響範囲一覧
### CLI エントリ
- `src/app/cli/index.ts`
- `src/app/cli/program.ts`
- `src/app/cli/commands.ts`
- `src/app/cli/routing.ts`
- `src/app/cli/helpers.ts`
- `bin/takt`
### features 呼び出し
- `src/features/tasks/*`
- `src/features/pipeline/*`
- `src/features/config/*`
- `src/features/interactive/*`
### docs 参照更新対象
- `docs/data-flow.md`
- `docs/data-flow-diagrams.md`
- `docs/agents.md`
- `docs/workflows.md`
- `docs/README.ja.md`
### テスト
- `src/__tests__/*`
---
## 実施手順(推奨順序)
### 1. core
- `core/workflow``core/models` の Public API を点検し、外部参照を `index.ts` 経由に統一。
- `core` 内での `shared` 依存を整理する(ログ/エラー/レポート生成の配置を明確化)。
- `agents` 依存の扱いを決定し、依存方向を破らない構成に合わせて移動計画を確定する。
### 2. infra
- `infra/github``infra/fs``index.ts` を新設し、deep import を解消する前提の API を定義。
- `infra/config/index.ts` の再エクスポート対象を拡張し、`globalConfig``projectConfig``workflowLoader` 等を Public API 化。
- `claude/codex/mock/resources``infra` 配下に移動し、参照を更新する。
### 3. features
- `features` から `infra` への deep import を Public API 経由に置換。
- `prompt` の移動に合わせ、`features` 内の import を `shared/prompt` に変更。
- `constants/context/exitCodes` の移動に合わせて参照を更新。
### 4. app
- `app/cli` から `features` Public API のみを使用する形に整理。
- `app/cli` から `infra` へ直接参照している箇所は、必要に応じて `features` 経由に寄せる。
### 5. Public API
- `src/index.ts` の再エクスポート対象を新パスに合わせて更新。
- 新設した `index.ts` のエクスポート整合を確認する。
### 6. docs
- `docs/data-flow.md` など、`src/` 参照を新パスに合わせて更新。
- 参照パスが `index.ts` の Public API 方針に沿っているか点検。
---
## 判断ポイント
- `src/models/workflow.ts` が追加される場合、
- **廃止**するか、
- **`core/models/index.ts` へ統合**するかを決める。
---
## 再開指示2026-02-02 時点の差分観測ベース)
### 現在のブランチ
- `refactoring`
### 進捗(差分ベースの整理)
#### core
- `src/core/models/*``src/core/workflow/*` が広範囲に変更されている。
#### infra
- 既存: `src/infra/config/*`, `src/infra/providers/*`, `src/infra/task/*`, `src/infra/github/*`, `src/infra/fs/session.ts` が更新されている。
- 追加: `src/infra/claude/*`, `src/infra/codex/*`, `src/infra/mock/*`, `src/infra/resources/*` が新規追加されている。
- 追加: `src/infra/github/index.ts`, `src/infra/fs/index.ts` が新規追加されている。
#### features
- `src/features/*` が広範囲に変更されている。
#### app
- `src/app/cli/*` が変更されている。
#### shared
- `src/shared/utils/index.ts``src/shared/ui/StreamDisplay.ts` が更新されている。
- `src/shared/prompt/*`, `src/shared/constants.ts`, `src/shared/context.ts`, `src/shared/exitCodes.ts` が新規追加されている。
#### 削除された旧パス
- `src/claude/*`, `src/codex/*`, `src/mock/*`, `src/prompt/*`, `src/resources/index.ts`
- `src/constants.ts`, `src/context.ts`, `src/exitCodes.ts`
#### tests
- `src/__tests__/*` が広範囲に更新されている。
#### resources
- `resources/global/{en,ja}/*` に更新があるため、移行作業とは独立して取り扱う。
#### docs
- `docs/vertical-slice-migration-plan.md` が未追跡ファイルとして存在する。
---
## 未完了セクション(要確認事項)
以下は差分観測のみでは断定できないため、再開時に確認する。
### core
- `core` から外部層(`shared` / `agents`)への依存が残っていないか確認する。
- `core/models``core/workflow` の Public API が `index.ts` 経由に統一されているか点検する。
### infra
- `infra/github/index.ts`, `infra/fs/index.ts`, `infra/resources/index.ts` の再エクスポート範囲を確定する。
- `infra/config/index.ts` の再エクスポート対象(`globalConfig`, `projectConfig`, `workflowLoader` 等)が揃っているか確認する。
- `infra/claude`, `infra/codex`, `infra/mock` の Public API が `index.ts` に統一されているか確認する。
### features
- `features` から `infra` への deep import が残っていないか確認する。
- `shared/prompt`, `shared/constants`, `shared/context`, `shared/exitCodes` への参照統一が完了しているか確認する。
### app
- `app/cli``features` Public API 経由に統一されているか確認する。
- `app/cli` から `infra` への direct import が残っていないか確認する。
### Public API
- `src/index.ts` の再エクスポートが新パスに揃っているか確認する。
- `infra`/`shared`/`features``index.ts` 追加分を反映できているか点検する。
### docs
- `docs/*` の参照パスを新構成Public APIへ更新する。
---
## 判断ポイント(再掲)
- `src/models/workflow.ts` は直近コミットで削除されているため、
- 廃止のまま進めるか、
- `core/models/index.ts` へ統合して復活させるかを確定する。
---
## 参照更新の対象一覧docs
- `docs/data-flow.md`
- `docs/data-flow-diagrams.md`
- `docs/agents.md`
- `docs/workflows.md`
- `docs/README.ja.md`
---
## 付記
- ここに記載した移動は、既存の機能追加なしで行うこと。
- 実装時は `core -> infra -> features -> app -> Public API -> docs` の順序を厳守する。

View File

@ -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

View File

@ -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 import33箇所
- テストコードはモジュール内部を直接テストする性質上、deep import は許容範囲
- **今回は修正対象外とする**(機能追加しない制約に基づき、テストの構造変更は行わない)
## 実装手順
### Step 1: core/workflow の Public API 修正
1. `src/core/workflow/index.ts` を確認し、`PermissionResult` をエクスポートに追加
2. `src/infra/claude/types.ts` の import パスを `../../core/workflow/index.js` に変更
3. `src/shared/ui/StreamDisplay.ts` の import パスを `../../core/workflow/index.js` に変更
### Step 2: infra/config の Public API 修正
1. `src/infra/config/index.ts` を確認し、`PermissionMode` をエクスポートに追加
2. `src/features/config/switchConfig.ts` の import/export パスを `../../infra/config/index.js` に変更
### Step 3: 最終確認
1. `npm run build` でビルド確認
2. `npm test` でテスト確認
## 制約の確認
- ✅ 機能追加は行わないimport パスとエクスポートの整理のみ)
- ✅ 変更対象は `src/` のみ(`docs/` は変更不要)

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View 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}

View 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}

View File

@ -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
```
```

View 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: "その他"

View File

@ -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

View File

@ -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

View File

@ -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

View 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: |
監督者からの指摘を修正してください。
監督者は全体を俯瞰した視点から問題を指摘しています。
優先度の高い項目から順に対応してください。
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
## 証拠
- {確認したファイル/検索/差分/ログの要点を列挙}

View 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: |
監督者からの指摘を修正してください。
監督者は全体を俯瞰した視点から問題を指摘しています。
優先度の高い項目から順に対応してください。
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
## 証拠
- {確認したファイル/検索/差分/ログの要点を列挙}

View File

@ -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

View File

@ -3,6 +3,7 @@ logs/
reports/ reports/
completed/ completed/
tasks/ tasks/
worktrees/
worktree-meta/ worktree-meta/
clone-meta/ clone-meta/
worktree-sessions/ worktree-sessions/

View 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']);
});
});

View File

@ -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);
}); });
}); });

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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)', () => {

View 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();
});
});
});

View 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']);
});
});

View File

@ -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 */

View File

@ -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 */

View File

@ -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(
items: ReturnType<typeof buildWorkflowSelectionItems>,
currentWorkflow: string,
): (value: string) => SelectOptionItem<string>[] {
return (value: string): SelectOptionItem<string>[] => {
const categoryName = parseCategorySelection(value);
if (categoryName) {
return applyBookmarks(
buildTopLevelSelectOptions(items, currentWorkflow),
getBookmarkedWorkflows(),
);
}
toggleBookmark(value);
return applyBookmarks(
buildTopLevelSelectOptions(items, currentWorkflow),
getBookmarkedWorkflows(),
);
};
}
/**
* 2-stage workflow selection with directory categories and bookmark support.
*/
async function selectWorkflowWithCategories(cwd: string): Promise<string | null> {
const current = getCurrentWorkflow(cwd); const current = getCurrentWorkflow(cwd);
const workflows = listWorkflows(cwd); const entries = listWorkflowEntries(cwd);
const items = buildWorkflowSelectionItems(entries);
const options: { label: string; value: string }[] = []; // Loop until user selects a workflow or cancels at top level
while (true) {
const baseOptions = buildTopLevelSelectOptions(items, current);
const options = applyBookmarks(baseOptions, getBookmarkedWorkflows());
// Add all workflows const selected = await selectOption<string>('Select workflow:', options, {
for (const name of workflows) { onBookmark: createBookmarkCallback(items, current),
const isCurrent = name === current; });
const label = isCurrent ? `${name} (current)` : name; if (!selected) return null;
options.push({ label, value: name });
const categoryName = parseCategorySelection(selected);
if (categoryName) {
const categoryOptions = buildCategoryWorkflowOptions(items, categoryName, current);
if (!categoryOptions) continue;
const bookmarkedInCategory = applyBookmarks(categoryOptions, getBookmarkedWorkflows());
const workflowSelection = await selectOption<string>(`Select workflow in ${categoryName}:`, bookmarkedInCategory, {
cancelLabel: '← Go back',
onBookmark: (value: string): SelectOptionItem<string>[] => {
toggleBookmark(value);
return applyBookmarks(
buildCategoryWorkflowOptions(items, categoryName, current) as SelectionOption[],
getBookmarkedWorkflows(),
);
},
});
// If workflow selected, return it. If cancelled (null), go back to top level
if (workflowSelection) return workflowSelection;
continue;
} }
return options; return selected;
}
} }
/** /**
* Switch to a different workflow * 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');

View File

@ -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);
const items = buildWorkflowSelectionItems(entries);
const hasCategories = items.some((item) => item.type === 'category');
if (!hasCategories) {
const baseOptions: SelectionOption[] = availableWorkflows.map((name) => ({
label: name === currentWorkflow ? `${name} (current)` : name, label: name === currentWorkflow ? `${name} (current)` : name,
value: name, value: name,
})); }));
const defaultWorkflow = availableWorkflows.includes(currentWorkflow) const buildFlatOptions = (): SelectionOption[] =>
? currentWorkflow applyBookmarks(baseOptions, getBookmarkedWorkflows());
: (availableWorkflows.includes(DEFAULT_WORKFLOW_NAME)
? DEFAULT_WORKFLOW_NAME
: availableWorkflows[0] || DEFAULT_WORKFLOW_NAME);
return selectOptionWithDefault('Select workflow:', options, defaultWorkflow); return selectOption<string>('Select workflow:', buildFlatOptions(), {
onBookmark: (value: string): SelectOptionItem<string>[] => {
toggleBookmark(value);
return buildFlatOptions();
},
});
}
const createTopLevelBookmarkCallback = (): ((value: string) => SelectOptionItem<string>[]) => {
return (value: string): SelectOptionItem<string>[] => {
if (parseCategorySelection(value)) {
return applyBookmarks(buildTopLevelSelectOptions(items, currentWorkflow), getBookmarkedWorkflows());
}
toggleBookmark(value);
return applyBookmarks(buildTopLevelSelectOptions(items, currentWorkflow), getBookmarkedWorkflows());
};
};
// Loop until user selects a workflow or cancels at top level
while (true) {
const baseOptions = buildTopLevelSelectOptions(items, currentWorkflow);
const topLevelOptions = applyBookmarks(baseOptions, getBookmarkedWorkflows());
const selected = await selectOption<string>('Select workflow:', topLevelOptions, {
onBookmark: createTopLevelBookmarkCallback(),
});
if (!selected) return null;
const categoryName = parseCategorySelection(selected);
if (categoryName) {
const categoryOptions = buildCategoryWorkflowOptions(items, categoryName, currentWorkflow);
if (!categoryOptions) continue;
const bookmarkedCategoryOptions = applyBookmarks(categoryOptions, getBookmarkedWorkflows());
const workflowSelection = await selectOption<string>(`Select workflow in ${categoryName}:`, bookmarkedCategoryOptions, {
cancelLabel: '← Go back',
onBookmark: (value: string): SelectOptionItem<string>[] => {
toggleBookmark(value);
return applyBookmarks(
buildCategoryWorkflowOptions(items, categoryName, currentWorkflow) as SelectionOption[],
getBookmarkedWorkflows(),
);
},
});
// If workflow selected, return it. If cancelled (null), go back to top level
if (workflowSelection) return workflowSelection;
continue;
}
return selected;
}
}
/**
* Select a workflow interactively with 2-stage category support.
*/
async function selectWorkflow(cwd: string): Promise<string | null> {
const categoryConfig = getWorkflowCategories(cwd);
if (categoryConfig) {
const current = getCurrentWorkflow(cwd);
const allWorkflows = loadAllWorkflows(cwd);
if (allWorkflows.size === 0) {
info(`No workflows found. Using default: ${DEFAULT_WORKFLOW_NAME}`);
return DEFAULT_WORKFLOW_NAME;
}
const categorized = buildCategorizedWorkflows(allWorkflows, categoryConfig);
warnMissingWorkflows(categorized.missingWorkflows);
return selectWorkflowFromCategorizedWorkflows(categorized, current);
}
return selectWorkflowWithDirectoryCategories(cwd);
} }
/** /**
@ -60,11 +155,9 @@ async function selectWorkflow(cwd: string): Promise<string | null> {
*/ */
async function determineWorkflow(cwd: string, override?: string): Promise<string | null> { 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) {

View 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;
}
}

View File

@ -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;
}

View File

@ -17,6 +17,8 @@ export {
resolveOpenaiApiKey, resolveOpenaiApiKey,
loadProjectDebugConfig, loadProjectDebugConfig,
getEffectiveDebugConfig, getEffectiveDebugConfig,
getBookmarkedWorkflows,
toggleBookmark,
} from './globalConfig.js'; } from './globalConfig.js';
export { export {

View File

@ -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,

View 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;
}

View File

@ -17,4 +17,6 @@ export {
loadWorkflowByIdentifier, loadWorkflowByIdentifier,
loadAllWorkflows, loadAllWorkflows,
listWorkflows, listWorkflows,
listWorkflowEntries,
type WorkflowDirEntry,
} from './workflowResolver.js'; } from './workflowResolver.js';

View File

@ -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);
if (stat.isFile() && (entry.endsWith('.yaml') || entry.endsWith('.yml'))) {
const workflowName = entry.replace(/\.ya?ml$/, ''); const workflowName = entry.replace(/\.ya?ml$/, '');
if (disabled?.includes(workflowName)) continue; if (disabled?.includes(workflowName)) continue;
yield { name: workflowName, path: entryPath }; 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());
}

View File

@ -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;
} }

View File

@ -8,6 +8,7 @@
export { export {
type SelectOptionItem, type SelectOptionItem,
type InteractiveSelectCallbacks,
renderMenu, renderMenu,
countRenderedLines, countRenderedLines,
type KeyInputResult, type KeyInputResult,

View File

@ -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
View 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
function toggleInstruction(id) {
const element = document.getElementById(id);
element.classList.toggle('collapsed');
}
</script>
</body>
</html>