Merge pull request #115 from nrslib/release/v0.7.0-alpha.1
Release v0.7.0-alpha.1
This commit is contained in:
commit
9ea8c16906
20
.github/workflows/cleanup-skipped-runs.yml
vendored
Normal file
20
.github/workflows/cleanup-skipped-runs.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: Cleanup Skipped Runs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # 毎週日曜 UTC 0:00
|
||||
workflow_dispatch: # 手動実行も可能
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Delete skipped TAKT Action runs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
gh run list --workflow=takt-action.yml --status=skipped --limit=100 --json databaseId --jq '.[].databaseId' | \
|
||||
xargs -I {} gh run delete {}
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.7.0-alpha.1] - 2026-02-06
|
||||
|
||||
### Added
|
||||
|
||||
- Hybrid Codex ピース: 全主要ピース(default, minimal, expert, expert-cqrs, passthrough, review-fix-minimal, coding)の Codex バリアントを追加
|
||||
- coder エージェントを Codex プロバイダーで実行するハイブリッド構成
|
||||
- en/ja 両対応
|
||||
- `passthrough` ピース: タスクをそのまま coder に渡す最小構成ピース
|
||||
- `takt export-cc` コマンド: ビルトインピース・エージェントを Claude Code Skill としてデプロイ
|
||||
- `takt list` に delete アクション追加、non-interactive モード分離
|
||||
- AI 相談アクション: `takt add` / インタラクティブモードで GitHub Issue 作成・タスクファイル保存が可能に
|
||||
- サイクル検出: ai_review ↔ ai_fix 間の無限ループを検出する `CycleDetector` を追加 (#102)
|
||||
- 修正不要時の裁定ステップ(`ai_no_fix`)を default ピースに追加
|
||||
- CI: skipped な TAKT Action ランを週次で自動削除するワークフローを追加
|
||||
- ピースカテゴリに Hybrid Codex サブカテゴリを追加(en/ja)
|
||||
|
||||
### Changed
|
||||
|
||||
- カテゴリ設定を簡素化: `default-categories.yaml` を `piece-categories.yaml` に統合し、ユーザーディレクトリへの自動コピー方式に変更
|
||||
- ピース選択UIのサブカテゴリナビゲーションを修正(再帰的な階層表示が正しく動作するように)
|
||||
- Claude Code Skill を Agent Team ベースに刷新
|
||||
- `console.log` を `info()` に統一(list コマンド)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Hybrid Codex ピースの description に含まれるコロンが YAML パースエラーを起こす問題を修正
|
||||
- サブカテゴリ選択時に `selectPieceFromCategoryTree` に不正な引数が渡される問題を修正
|
||||
|
||||
### Internal
|
||||
|
||||
- `list` コマンドのリファクタリング: `listNonInteractive.ts`, `taskDeleteActions.ts` を分離
|
||||
- `cycle-detector.ts` を追加、`PieceEngine` にサイクル検出を統合
|
||||
- ピースカテゴリローダーのリファクタリング(`pieceCategories.ts`, `pieceSelection/index.ts`)
|
||||
- テスト追加: cycle-detector, engine-loop-monitors, piece-selection, listNonInteractive, taskDeleteActions, createIssue, saveTaskFile
|
||||
|
||||
## [0.6.0] - 2026-02-05
|
||||
|
||||
RC1/RC2 の内容を正式リリース。機能変更なし。
|
||||
|
||||
@ -235,6 +235,9 @@ takt eject
|
||||
# Clear agent conversation sessions
|
||||
takt clear
|
||||
|
||||
# Deploy builtin pieces/agents as Claude Code Skill
|
||||
takt export-cc
|
||||
|
||||
# Configure permission mode
|
||||
takt config
|
||||
```
|
||||
@ -391,8 +394,11 @@ TAKT includes multiple builtin pieces:
|
||||
| `expert` | Full-stack development piece: architecture, frontend, security, QA reviews with fix loops. |
|
||||
| `expert-cqrs` | Full-stack development piece (CQRS+ES specialized): CQRS+ES, frontend, security, QA reviews with fix loops. |
|
||||
| `magi` | Deliberation system inspired by Evangelion. Three AI personas (MELCHIOR, BALTHASAR, CASPER) analyze and vote. |
|
||||
| `passthrough` | Thinnest wrapper. Pass task directly to coder as-is. No review. |
|
||||
| `review-only` | Read-only code review piece that makes no changes. |
|
||||
|
||||
**Hybrid Codex variants** (`*-hybrid-codex`): Each major piece has a Codex variant where the coder agent runs on Codex while reviewers use Claude. Available for: default, minimal, expert, expert-cqrs, passthrough, review-fix-minimal, coding.
|
||||
|
||||
Use `takt switch` to switch pieces.
|
||||
|
||||
## Builtin Agents
|
||||
|
||||
@ -231,6 +231,9 @@ takt eject
|
||||
# エージェントの会話セッションをクリア
|
||||
takt clear
|
||||
|
||||
# ビルトインピース・エージェントを Claude Code Skill としてデプロイ
|
||||
takt export-cc
|
||||
|
||||
# パーミッションモードを設定
|
||||
takt config
|
||||
```
|
||||
@ -387,8 +390,11 @@ TAKTには複数のビルトインピースが同梱されています:
|
||||
| `expert` | フルスタック開発ピース: アーキテクチャ、フロントエンド、セキュリティ、QA レビューと修正ループ。 |
|
||||
| `expert-cqrs` | フルスタック開発ピース(CQRS+ES特化): CQRS+ES、フロントエンド、セキュリティ、QA レビューと修正ループ。 |
|
||||
| `magi` | エヴァンゲリオンにインスパイアされた審議システム。3つの AI ペルソナ(MELCHIOR、BALTHASAR、CASPER)が分析し投票。 |
|
||||
| `passthrough` | 最小構成。タスクをそのまま coder に渡す薄いラッパー。レビューなし。 |
|
||||
| `review-only` | 変更を加えない読み取り専用のコードレビューピース。 |
|
||||
|
||||
**Hybrid Codex バリアント** (`*-hybrid-codex`): 主要ピースごとに、coder エージェントを Codex で実行しレビュアーは Claude を使うハイブリッド構成が用意されています。対象: default, minimal, expert, expert-cqrs, passthrough, review-fix-minimal, coding。
|
||||
|
||||
`takt switch` でピースを切り替えられます。
|
||||
|
||||
## ビルトインエージェント
|
||||
|
||||
@ -38,7 +38,7 @@ sequenceDiagram
|
||||
|
||||
User->>Interactive: /go コマンド
|
||||
Interactive->>Interactive: buildTaskFromHistory()
|
||||
Interactive-->>CLI: { confirmed: true, task: string }
|
||||
Interactive-->>CLI: { action: InteractiveModeAction, task: string }
|
||||
|
||||
CLI->>Orchestration: selectAndExecuteTask(cwd, task)
|
||||
|
||||
|
||||
@ -355,7 +355,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
|
||||
|
||||
**データ出力**:
|
||||
- `InteractiveModeResult`:
|
||||
- `confirmed: boolean`
|
||||
- `action: InteractiveModeAction` (`'execute' | 'save_task' | 'create_issue' | 'cancel'`)
|
||||
- `task: string` (会話履歴全体を結合した文字列)
|
||||
|
||||
---
|
||||
|
||||
190
e2e/specs/eject.e2e.ts
Normal file
190
e2e/specs/eject.e2e.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { existsSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createIsolatedEnv, type IsolatedEnv } from '../helpers/isolated-env';
|
||||
import { runTakt } from '../helpers/takt-runner';
|
||||
|
||||
/**
|
||||
* Create a minimal local git repository for eject tests.
|
||||
* No GitHub access needed — just a local git init.
|
||||
*/
|
||||
function createLocalRepo(): { path: string; cleanup: () => void } {
|
||||
const repoPath = mkdtempSync(join(tmpdir(), 'takt-eject-e2e-'));
|
||||
execFileSync('git', ['init'], { cwd: repoPath, stdio: 'pipe' });
|
||||
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: repoPath, stdio: 'pipe' });
|
||||
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: repoPath, stdio: 'pipe' });
|
||||
// Create initial commit so branch exists
|
||||
writeFileSync(join(repoPath, 'README.md'), '# test\n');
|
||||
execFileSync('git', ['add', '.'], { cwd: repoPath, stdio: 'pipe' });
|
||||
execFileSync('git', ['commit', '-m', 'init'], { cwd: repoPath, stdio: 'pipe' });
|
||||
return {
|
||||
path: repoPath,
|
||||
cleanup: () => {
|
||||
try {
|
||||
rmSync(repoPath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// E2E更新時は docs/testing/e2e.md も更新すること
|
||||
describe('E2E: Eject builtin pieces (takt eject)', () => {
|
||||
let isolatedEnv: IsolatedEnv;
|
||||
let repo: { path: string; cleanup: () => void };
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
repo = createLocalRepo();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
repo.cleanup();
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
try {
|
||||
isolatedEnv.cleanup();
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
});
|
||||
|
||||
it('should list available builtin pieces when no name given', () => {
|
||||
const result = runTakt({
|
||||
args: ['eject'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('default');
|
||||
expect(result.stdout).toContain('Available builtin pieces');
|
||||
});
|
||||
|
||||
it('should eject piece to project .takt/ by default', () => {
|
||||
const result = runTakt({
|
||||
args: ['eject', 'default'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
// Piece YAML should be in project .takt/pieces/
|
||||
const piecePath = join(repo.path, '.takt', 'pieces', 'default.yaml');
|
||||
expect(existsSync(piecePath)).toBe(true);
|
||||
|
||||
// Agents should be in project .takt/agents/
|
||||
const agentsDir = join(repo.path, '.takt', 'agents', 'default');
|
||||
expect(existsSync(agentsDir)).toBe(true);
|
||||
expect(existsSync(join(agentsDir, 'coder.md'))).toBe(true);
|
||||
expect(existsSync(join(agentsDir, 'planner.md'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve relative agent paths in ejected piece (no rewriting)', () => {
|
||||
runTakt({
|
||||
args: ['eject', 'default'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
const piecePath = join(repo.path, '.takt', 'pieces', 'default.yaml');
|
||||
const content = readFileSync(piecePath, 'utf-8');
|
||||
|
||||
// Relative paths should be preserved as ../agents/
|
||||
expect(content).toContain('agent: ../agents/default/');
|
||||
// Should NOT contain rewritten absolute paths
|
||||
expect(content).not.toContain('agent: ~/.takt/agents/');
|
||||
});
|
||||
|
||||
it('should eject piece to global ~/.takt/ with --global flag', () => {
|
||||
const result = runTakt({
|
||||
args: ['eject', 'default', '--global'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
// Piece YAML should be in global dir (TAKT_CONFIG_DIR from isolated env)
|
||||
const piecePath = join(isolatedEnv.taktDir, 'pieces', 'default.yaml');
|
||||
expect(existsSync(piecePath)).toBe(true);
|
||||
|
||||
// Agents should be in global agents dir
|
||||
const agentsDir = join(isolatedEnv.taktDir, 'agents', 'default');
|
||||
expect(existsSync(agentsDir)).toBe(true);
|
||||
expect(existsSync(join(agentsDir, 'coder.md'))).toBe(true);
|
||||
|
||||
// Should NOT be in project dir
|
||||
const projectPiecePath = join(repo.path, '.takt', 'pieces', 'default.yaml');
|
||||
expect(existsSync(projectPiecePath)).toBe(false);
|
||||
});
|
||||
|
||||
it('should warn and skip when piece already exists', () => {
|
||||
// First eject
|
||||
runTakt({
|
||||
args: ['eject', 'default'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
// Second eject — should skip
|
||||
const result = runTakt({
|
||||
args: ['eject', 'default'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('already exists');
|
||||
});
|
||||
|
||||
it('should report error for non-existent builtin', () => {
|
||||
const result = runTakt({
|
||||
args: ['eject', 'nonexistent-piece-xyz'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('not found');
|
||||
});
|
||||
|
||||
it('should correctly eject agents for pieces with unique agents', () => {
|
||||
const result = runTakt({
|
||||
args: ['eject', 'magi'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
// MAGI piece should have its own agents
|
||||
const magiDir = join(repo.path, '.takt', 'agents', 'magi');
|
||||
expect(existsSync(join(magiDir, 'melchior.md'))).toBe(true);
|
||||
expect(existsSync(join(magiDir, 'balthasar.md'))).toBe(true);
|
||||
expect(existsSync(join(magiDir, 'casper.md'))).toBe(true);
|
||||
|
||||
// Should NOT have default agents mixed in
|
||||
expect(existsSync(join(repo.path, '.takt', 'agents', 'default'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve relative paths for global eject too', () => {
|
||||
runTakt({
|
||||
args: ['eject', 'magi', '--global'],
|
||||
cwd: repo.path,
|
||||
env: isolatedEnv.env,
|
||||
});
|
||||
|
||||
const piecePath = join(isolatedEnv.taktDir, 'pieces', 'magi.yaml');
|
||||
const content = readFileSync(piecePath, 'utf-8');
|
||||
|
||||
expect(content).toContain('agent: ../agents/magi/');
|
||||
expect(content).not.toContain('agent: ~/.takt/agents/');
|
||||
});
|
||||
});
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "takt",
|
||||
"version": "0.6.0-rc1",
|
||||
"version": "0.7.0-alpha.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "takt",
|
||||
"version": "0.6.0-rc1",
|
||||
"version": "0.7.0-alpha.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "takt",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0-alpha.1",
|
||||
"description": "TAKT: Task Agent Koordination Tool - AI Agent Piece Orchestration",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
piece_categories:
|
||||
"🚀 Quick Start":
|
||||
pieces:
|
||||
- minimal
|
||||
- default
|
||||
- passthrough
|
||||
- coding
|
||||
- minimal
|
||||
|
||||
"🔍 Review & Fix":
|
||||
pieces:
|
||||
@ -20,6 +22,20 @@ piece_categories:
|
||||
- expert
|
||||
- expert-cqrs
|
||||
|
||||
"🔀 Hybrid (Codex Coding)":
|
||||
"🚀 Quick Start":
|
||||
pieces:
|
||||
- default-hybrid-codex
|
||||
- passthrough-hybrid-codex
|
||||
- minimal-hybrid-codex
|
||||
"🔍 Review & Fix":
|
||||
pieces:
|
||||
- review-fix-minimal-hybrid-codex
|
||||
"🔧 Expert":
|
||||
pieces:
|
||||
- expert-hybrid-codex
|
||||
- expert-cqrs-hybrid-codex
|
||||
|
||||
"Others":
|
||||
pieces:
|
||||
- research
|
||||
631
resources/global/en/pieces/default-hybrid-codex.yaml
Normal file
631
resources/global/en/pieces/default-hybrid-codex.yaml
Normal file
@ -0,0 +1,631 @@
|
||||
# Default TAKT Piece
|
||||
# Plan -> Architect -> Implement -> AI Review -> Reviewers (parallel: Architect + QA) -> Supervisor Approval
|
||||
#
|
||||
# Boilerplate sections (Piece Context, User Request, Previous Response,
|
||||
# Additional User Inputs, Instructions heading) are auto-injected by buildInstruction().
|
||||
# Only movement-specific content belongs in instruction_template.
|
||||
#
|
||||
# Template Variables (available in instruction_template):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {previous_response} - Output from the previous movement (only when pass_previous_response: true)
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
#
|
||||
# Movement-level Fields:
|
||||
# report: - Report file(s) for the movement (auto-injected as Report File/Files in Piece Context)
|
||||
# Single: report: 00-plan.md
|
||||
# Multiple: report:
|
||||
# - Scope: 01-coder-scope.md
|
||||
# - Decisions: 02-coder-decisions.md
|
||||
|
||||
name: default-hybrid-codex
|
||||
description: Standard development piece with planning and specialized reviews
|
||||
|
||||
max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
loop_monitors:
|
||||
- cycle: [ai_review, ai_fix]
|
||||
threshold: 3
|
||||
judge:
|
||||
agent: ../agents/default/supervisor.md
|
||||
instruction_template: |
|
||||
The ai_review ↔ ai_fix loop has repeated {cycle_count} times.
|
||||
|
||||
Review the reports from each cycle and determine whether this loop
|
||||
is healthy (making progress) or unproductive (repeating the same issues).
|
||||
|
||||
**Reports to reference:**
|
||||
- AI Review results: {report:04-ai-review.md}
|
||||
|
||||
**Judgment criteria:**
|
||||
- Are new issues being found/fixed in each cycle?
|
||||
- Are the same findings being repeated?
|
||||
- Are fixes actually being applied?
|
||||
rules:
|
||||
- condition: Healthy (making progress)
|
||||
next: ai_review
|
||||
- condition: Unproductive (no improvement)
|
||||
next: reviewers
|
||||
|
||||
movements:
|
||||
- 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
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: Requirements are clear and implementable
|
||||
next: architect
|
||||
- condition: User is asking a question (not an implementation task)
|
||||
next: COMPLETE
|
||||
- condition: Requirements unclear, insufficient info
|
||||
next: ABORT
|
||||
appendix: |
|
||||
Clarifications needed:
|
||||
- {Question 1}
|
||||
- {Question 2}
|
||||
instruction_template: |
|
||||
Analyze the task and create an implementation plan.
|
||||
|
||||
**Note:** If returned from implement movement (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: architect
|
||||
edit: false
|
||||
agent: ../agents/default/architect.md
|
||||
report:
|
||||
name: 01-architecture.md
|
||||
format: |
|
||||
```markdown
|
||||
# Architecture Design
|
||||
|
||||
## Task Size
|
||||
Small / Medium / Large
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### File Structure
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/example.ts` | Summary |
|
||||
|
||||
### Technology Selection
|
||||
- {Selected technologies/libraries and reasoning}
|
||||
|
||||
### Design Patterns
|
||||
- {Patterns to adopt and where to apply}
|
||||
|
||||
## Implementation Guidelines
|
||||
- {Guidelines for Coder to follow during implementation}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: Small task (no design needed)
|
||||
next: implement
|
||||
- condition: Design complete
|
||||
next: implement
|
||||
- condition: Insufficient info, cannot proceed
|
||||
next: ABORT
|
||||
instruction_template: |
|
||||
Read the plan report ({report:00-plan.md}) and perform architecture design.
|
||||
|
||||
**Small task criteria:**
|
||||
- Only 1-2 files to modify
|
||||
- Can follow existing patterns
|
||||
- No technology selection needed
|
||||
|
||||
For small tasks, skip the design report and use the "Small task (no design needed)" rule.
|
||||
|
||||
**Tasks requiring design:**
|
||||
- 3+ files to modify
|
||||
- Adding new modules/features
|
||||
- Technology selection needed
|
||||
- Architecture pattern decisions needed
|
||||
|
||||
**Tasks:**
|
||||
1. Evaluate task size
|
||||
2. Decide file structure
|
||||
3. Select technology (if needed)
|
||||
4. Choose design patterns
|
||||
5. Create implementation guidelines for Coder
|
||||
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-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 movement and the design from the architect movement.
|
||||
|
||||
**Reports to reference:**
|
||||
- Plan: {report:00-plan.md}
|
||||
- Design: {report:01-architecture.md} (if exists)
|
||||
|
||||
Use only the Report Directory files shown in Piece Context. Do not search or open reports outside that directory.
|
||||
|
||||
**Important:** Do not make design decisions; follow the design determined in the architect movement.
|
||||
Report if you encounter unclear points or need design changes.
|
||||
|
||||
**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)**
|
||||
|
||||
- name: ai_review
|
||||
edit: false
|
||||
agent: ../agents/default/ai-antipattern-reviewer.md
|
||||
report:
|
||||
name: 04-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
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: No AI-specific issues
|
||||
next: reviewers
|
||||
- condition: AI-specific issues found
|
||||
next: ai_fix
|
||||
instruction_template: |
|
||||
**This is AI Review iteration {movement_iteration}.**
|
||||
|
||||
For the 1st iteration, review thoroughly and report all issues at once.
|
||||
For iteration 2+, prioritize verifying that previously REJECTed items have been fixed.
|
||||
|
||||
Review the code for AI-specific issues:
|
||||
- Assumption validation
|
||||
- Plausible but wrong patterns
|
||||
- Context fit with existing codebase
|
||||
- Scope creep detection
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI issues fixed
|
||||
next: ai_review
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
next: ai_no_fix
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: ai_no_fix
|
||||
instruction_template: |
|
||||
**This is AI Review iteration {movement_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 (`./gradlew :backend:test` etc.)
|
||||
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
|
||||
|
||||
**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 "Unable to proceed with fixes"
|
||||
- When "no fix needed", output the tag for "Unable to proceed with fixes" 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}
|
||||
- Judging based on assumptions
|
||||
- Leaving problems that AI Reviewer REJECTED
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_review's findings are valid (fix required)
|
||||
next: ai_fix
|
||||
- condition: ai_fix's judgment is valid (no fix needed)
|
||||
next: reviewers
|
||||
instruction_template: |
|
||||
ai_review (reviewer) and ai_fix (coder) disagree.
|
||||
|
||||
- ai_review found issues and REJECTed
|
||||
- ai_fix verified and determined "no fix needed"
|
||||
|
||||
Review both outputs and arbitrate which judgment is correct.
|
||||
|
||||
**Reports to reference:**
|
||||
- AI Review results: {report:04-ai-review.md}
|
||||
|
||||
**Judgment criteria:**
|
||||
- Are ai_review's findings specific and pointing to real issues in the code?
|
||||
- Does ai_fix's rebuttal have evidence (file verification, test results)?
|
||||
- Are the findings non-blocking (record-only) or do they require actual fixes?
|
||||
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
report:
|
||||
name: 05-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)
|
||||
| # | Scope | Location | Issue | Fix |
|
||||
|---|-------|----------|-------|-----|
|
||||
| 1 | In-scope | `src/file.ts:42` | Issue description | Fix method |
|
||||
|
||||
Scope: "In-scope" (fixable now) / "Out-of-scope" (existing issue, non-blocking)
|
||||
|
||||
## Existing Issues (informational, non-blocking)
|
||||
- {Record of existing issues unrelated to current change}
|
||||
```
|
||||
|
||||
**Cognitive load reduction rules:**
|
||||
- APPROVE -> Summary only (5 lines or less)
|
||||
- REJECT -> Issues in table format (30 lines or less)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
**Verify that the implementation follows the design from the architect movement.**
|
||||
Do NOT review AI-specific issues (that's the ai_review movement).
|
||||
|
||||
**Reports to reference:**
|
||||
- Design: {report:01-architecture.md} (if exists)
|
||||
- Implementation scope: {report:02-coder-scope.md}
|
||||
|
||||
**Review perspectives:**
|
||||
- Design consistency (does it follow the file structure and patterns defined by architect?)
|
||||
- Code quality
|
||||
- Change scope appropriateness
|
||||
- Test coverage
|
||||
- Dead code
|
||||
- Call chain verification
|
||||
|
||||
**Note:** For small tasks that skipped the architect movement, review design validity as usual.
|
||||
|
||||
- name: qa-review
|
||||
edit: false
|
||||
agent: ../agents/default/qa-reviewer.md
|
||||
report:
|
||||
name: 06-qa-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# QA Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Summary
|
||||
{1-2 sentences summarizing result}
|
||||
|
||||
## Reviewed Perspectives
|
||||
| Perspective | Result | Notes |
|
||||
|-------------|--------|-------|
|
||||
| Test Coverage | ✅ | - |
|
||||
| Test Quality | ✅ | - |
|
||||
| Error Handling | ✅ | - |
|
||||
| Documentation | ✅ | - |
|
||||
| Maintainability | ✅ | - |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Category | Issue | Fix |
|
||||
|---|----------|-------|-----|
|
||||
| 1 | Testing | Issue description | Fix method |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Review the changes from the quality assurance perspective.
|
||||
|
||||
**Review Criteria:**
|
||||
- Test coverage and quality
|
||||
- Test strategy (unit/integration/E2E)
|
||||
- Error handling
|
||||
- Logging and monitoring
|
||||
- Maintainability
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: plan
|
||||
instruction_template: |
|
||||
Address the feedback from the reviewers.
|
||||
The "Original User Request" is reference information, not the latest instruction.
|
||||
Review the session conversation history and fix the issues raised by the reviewers.
|
||||
|
||||
|
||||
**Required output (include headings)**
|
||||
## Work done
|
||||
- {summary of work performed}
|
||||
## Changes made
|
||||
- {summary of code changes}
|
||||
## Test results
|
||||
- {command and outcome}
|
||||
## Evidence
|
||||
- {key files/grep/diff/log evidence you verified}
|
||||
|
||||
- name: supervise
|
||||
edit: false
|
||||
agent: ../agents/default/supervisor.md
|
||||
report:
|
||||
- Validation: 07-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: All checks passed
|
||||
next: COMPLETE
|
||||
- condition: Requirements unmet, tests failing, build errors
|
||||
next: plan
|
||||
instruction_template: |
|
||||
Run tests, verify the build, and perform final approval.
|
||||
|
||||
**Piece Overall Review:**
|
||||
1. Does the implementation match the plan ({report:00-plan.md}) and design ({report:01-architecture.md}, if exists)?
|
||||
2. Were all review movement 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 |
|
||||
|--------|--------|
|
||||
| Architecture Design | ✅ Complete |
|
||||
| AI Review | ✅ APPROVE |
|
||||
| Architect Review | ✅ APPROVE |
|
||||
| QA | ✅ APPROVE |
|
||||
| Supervisor | ✅ APPROVE |
|
||||
|
||||
## Verification Commands
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
@ -26,6 +26,30 @@ max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
loop_monitors:
|
||||
- cycle: [ai_review, ai_fix]
|
||||
threshold: 3
|
||||
judge:
|
||||
agent: ../agents/default/supervisor.md
|
||||
instruction_template: |
|
||||
The ai_review ↔ ai_fix loop has repeated {cycle_count} times.
|
||||
|
||||
Review the reports from each cycle and determine whether this loop
|
||||
is healthy (making progress) or unproductive (repeating the same issues).
|
||||
|
||||
**Reports to reference:**
|
||||
- AI Review results: {report:04-ai-review.md}
|
||||
|
||||
**Judgment criteria:**
|
||||
- Are new issues being found/fixed in each cycle?
|
||||
- Are the same findings being repeated?
|
||||
- Are fixes actually being applied?
|
||||
rules:
|
||||
- condition: Healthy (making progress)
|
||||
next: ai_review
|
||||
- condition: Unproductive (no improvement)
|
||||
next: reviewers
|
||||
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
|
||||
687
resources/global/en/pieces/expert-cqrs-hybrid-codex.yaml
Normal file
687
resources/global/en/pieces/expert-cqrs-hybrid-codex.yaml
Normal file
@ -0,0 +1,687 @@
|
||||
# Expert CQRS Review Piece
|
||||
# Review piece with CQRS+ES, Frontend, Security, and QA experts
|
||||
#
|
||||
# Flow:
|
||||
# plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE
|
||||
# ↓ ├─ cqrs-es-review ↓
|
||||
# ai_fix ├─ frontend-review fix_supervisor
|
||||
# ├─ security-review
|
||||
# └─ qa-review
|
||||
# any("needs_fix") → fix → reviewers
|
||||
#
|
||||
# Template Variables:
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {task} - Original user request
|
||||
# {previous_response} - Output from the previous movement
|
||||
# {user_inputs} - Accumulated user inputs during piece
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
|
||||
name: expert-cqrs-hybrid-codex
|
||||
description: CQRS+ES, Frontend, Security, QA Expert Review
|
||||
|
||||
max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
movements:
|
||||
# ===========================================
|
||||
# Movement 0: Planning
|
||||
# ===========================================
|
||||
- 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
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
Analyze the task and create an implementation plan.
|
||||
|
||||
**Note:** If returned from implement movement (Previous Response exists),
|
||||
review and revise the plan based on that feedback (replan).
|
||||
|
||||
**Tasks:**
|
||||
1. Understand the requirements
|
||||
2. Identify impact scope
|
||||
3. Decide implementation approach
|
||||
rules:
|
||||
- condition: Task analysis and planning is complete
|
||||
next: implement
|
||||
- condition: Requirements are unclear and planning cannot proceed
|
||||
next: ABORT
|
||||
|
||||
# ===========================================
|
||||
# Movement 1: Implementation
|
||||
# ===========================================
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
Follow the plan from the plan movement and implement.
|
||||
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
||||
Use only the Report Directory files shown in Piece 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 is complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: ai_review
|
||||
- condition: Cannot proceed with implementation
|
||||
next: ai_review
|
||||
- condition: User input required
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
|
||||
# ===========================================
|
||||
# Movement 2: AI Review
|
||||
# ===========================================
|
||||
- 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
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**This is AI Review iteration {movement_iteration}.**
|
||||
|
||||
For the 1st iteration, review thoroughly and report all issues at once.
|
||||
For iteration 2+, prioritize verifying that previously REJECTed items have been fixed.
|
||||
|
||||
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 found
|
||||
next: reviewers
|
||||
- condition: AI-specific issues detected
|
||||
next: ai_fix
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**This is AI Review iteration {movement_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 (`./gradlew :backend:test` etc.)
|
||||
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 "Unable to proceed with fixes"
|
||||
- When "no fix needed", output the tag for "Unable to proceed with fixes" 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}
|
||||
|
||||
**No-implementation handling (required)**
|
||||
rules:
|
||||
- condition: AI Reviewer's issues have been fixed
|
||||
next: ai_review
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
next: ai_no_fix
|
||||
- condition: Unable to proceed with fixes
|
||||
next: ai_no_fix
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_review's findings are valid (fix required)
|
||||
next: ai_fix
|
||||
- condition: ai_fix's judgment is valid (no fix needed)
|
||||
next: reviewers
|
||||
instruction_template: |
|
||||
ai_review (reviewer) and ai_fix (coder) disagree.
|
||||
|
||||
- ai_review found issues and REJECTed
|
||||
- ai_fix verified and determined "no fix needed"
|
||||
|
||||
Review both outputs and arbitrate which judgment is correct.
|
||||
|
||||
**Reports to reference:**
|
||||
- AI Review results: {report:03-ai-review.md}
|
||||
|
||||
**Judgment criteria:**
|
||||
- Are ai_review's findings specific and pointing to real issues in the code?
|
||||
- Does ai_fix's rebuttal have evidence (file verification, test results)?
|
||||
- Are the findings non-blocking (record-only) or do they require actual fixes?
|
||||
|
||||
# ===========================================
|
||||
# Movement 3: Expert Reviews (Parallel)
|
||||
# ===========================================
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: cqrs-es-review
|
||||
edit: false
|
||||
agent: ../agents/expert-cqrs/cqrs-es-reviewer.md
|
||||
report:
|
||||
name: 04-cqrs-es-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# CQRS+ES Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Summary
|
||||
{1-2 sentences summarizing result}
|
||||
|
||||
## Reviewed Perspectives
|
||||
| Perspective | Result | Notes |
|
||||
|-------------|--------|-------|
|
||||
| Aggregate Design | ✅ | - |
|
||||
| Event Design | ✅ | - |
|
||||
| Command/Query Separation | ✅ | - |
|
||||
| Projections | ✅ | - |
|
||||
| Eventual Consistency | ✅ | - |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Scope | Location | Issue | Fix |
|
||||
|---|-------|----------|-------|-----|
|
||||
| 1 | In-scope | `src/file.ts:42` | Issue description | Fix method |
|
||||
|
||||
Scope: "In-scope" (fixable now) / "Out-of-scope" (existing issue, non-blocking)
|
||||
|
||||
## Existing Issues (informational, non-blocking)
|
||||
- {Record of existing issues unrelated to current change}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Review the changes from the CQRS (Command Query Responsibility Segregation)
|
||||
and Event Sourcing perspective. Do NOT review AI-specific issues (that's the ai_review movement).
|
||||
|
||||
**Review Criteria:**
|
||||
- Aggregate design validity
|
||||
- Event design (granularity, naming, schema)
|
||||
- Command/Query separation
|
||||
- Projection design
|
||||
- Eventual consistency considerations
|
||||
|
||||
**Note**: If this project does not use CQRS+ES patterns,
|
||||
review from a general domain design perspective.
|
||||
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
agent: ../agents/expert/frontend-reviewer.md
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# Frontend Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Summary
|
||||
{1-2 sentences summarizing result}
|
||||
|
||||
## Reviewed Perspectives
|
||||
| Perspective | Result | Notes |
|
||||
|-------------|--------|-------|
|
||||
| Component Design | ✅ | - |
|
||||
| State Management | ✅ | - |
|
||||
| Performance | ✅ | - |
|
||||
| Accessibility | ✅ | - |
|
||||
| Type Safety | ✅ | - |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Location | Issue | Fix |
|
||||
|---|----------|-------|-----|
|
||||
| 1 | `src/file.tsx:42` | Issue description | Fix method |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Review the changes from the frontend development perspective.
|
||||
|
||||
**Review Criteria:**
|
||||
- Component design (separation of concerns, granularity)
|
||||
- State management (local/global decisions)
|
||||
- Performance (re-rendering, memoization)
|
||||
- Accessibility (keyboard support, ARIA)
|
||||
- Data fetching patterns
|
||||
- TypeScript type safety
|
||||
|
||||
**Note**: If this project does not include frontend code,
|
||||
approve and proceed to the next movement.
|
||||
|
||||
- name: security-review
|
||||
edit: false
|
||||
agent: ../agents/expert/security-reviewer.md
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# Security Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Severity: None / Low / Medium / High / Critical
|
||||
|
||||
## Check Results
|
||||
| Category | Result | Notes |
|
||||
|----------|--------|-------|
|
||||
| Injection | ✅ | - |
|
||||
| Auth/Authz | ✅ | - |
|
||||
| Data Protection | ✅ | - |
|
||||
| Dependencies | ✅ | - |
|
||||
|
||||
## Vulnerabilities (if REJECT)
|
||||
| # | Severity | Type | Location | Fix |
|
||||
|---|----------|------|----------|-----|
|
||||
| 1 | High | SQLi | `src/db.ts:42` | Use parameterized query |
|
||||
|
||||
## Warnings (non-blocking)
|
||||
- {Security recommendations}
|
||||
```
|
||||
|
||||
**Cognitive load reduction rules:**
|
||||
- No issues -> Check table only (10 lines or less)
|
||||
- Warnings -> + Warnings 1-2 lines (15 lines or less)
|
||||
- Vulnerabilities -> + Table format (30 lines or less)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Perform security review on the changes. Check for vulnerabilities including:
|
||||
- Injection attacks (SQL, Command, XSS)
|
||||
- Authentication/Authorization issues
|
||||
- Data exposure risks
|
||||
- Cryptographic weaknesses
|
||||
|
||||
- name: qa-review
|
||||
edit: false
|
||||
agent: ../agents/expert/qa-reviewer.md
|
||||
report:
|
||||
name: 07-qa-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# QA Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Summary
|
||||
{1-2 sentences summarizing result}
|
||||
|
||||
## Reviewed Perspectives
|
||||
| Perspective | Result | Notes |
|
||||
|-------------|--------|-------|
|
||||
| Test Coverage | ✅ | - |
|
||||
| Test Quality | ✅ | - |
|
||||
| Error Handling | ✅ | - |
|
||||
| Documentation | ✅ | - |
|
||||
| Maintainability | ✅ | - |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Category | Issue | Fix |
|
||||
|---|----------|-------|-----|
|
||||
| 1 | Testing | Issue description | Fix method |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Review the changes from the quality assurance perspective.
|
||||
|
||||
**Review Criteria:**
|
||||
- Test coverage and quality
|
||||
- Test strategy (unit/integration/E2E)
|
||||
- Documentation (in-code and external)
|
||||
- Error handling
|
||||
- Logging and monitoring
|
||||
- Maintainability
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: plan
|
||||
instruction_template: |
|
||||
Address the feedback from the reviewers.
|
||||
The "Original User Request" is reference information, not the latest instruction.
|
||||
Review the session conversation history and fix the issues raised by the reviewers.
|
||||
|
||||
|
||||
**Required output (include headings)**
|
||||
## Work done
|
||||
- {summary of work performed}
|
||||
## Changes made
|
||||
- {summary of code changes}
|
||||
## Test results
|
||||
- {command and outcome}
|
||||
## Evidence
|
||||
- {key files/grep/diff/log evidence you verified}
|
||||
|
||||
# ===========================================
|
||||
# Movement 4: Supervision
|
||||
# ===========================================
|
||||
- name: supervise
|
||||
edit: false
|
||||
agent: ../agents/expert/supervisor.md
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Previous Reviews Summary
|
||||
Reaching this movement means all the following reviews have been APPROVED:
|
||||
- AI Review: APPROVED
|
||||
- CQRS+ES Review: APPROVED
|
||||
- Frontend Review: APPROVED
|
||||
- Security Review: APPROVED
|
||||
- QA Review: APPROVED
|
||||
|
||||
Run tests, verify the build, and perform final approval.
|
||||
|
||||
**Piece Overall Review:**
|
||||
1. Does the implementation match the plan ({report:00-plan.md})?
|
||||
2. Were all review movement 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 |
|
||||
| CQRS+ES | ✅ APPROVE |
|
||||
| Frontend | ✅ APPROVE |
|
||||
| Security | ✅ APPROVE |
|
||||
| QA | ✅ APPROVE |
|
||||
| Supervisor | ✅ APPROVE |
|
||||
|
||||
## Verification Commands
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
rules:
|
||||
- condition: All validations pass and ready to merge
|
||||
next: COMPLETE
|
||||
- condition: Issues detected during final review
|
||||
next: fix_supervisor
|
||||
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
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: Supervisor's issues have been fixed
|
||||
next: supervise
|
||||
- condition: Unable to proceed with fixes
|
||||
next: plan
|
||||
700
resources/global/en/pieces/expert-hybrid-codex.yaml
Normal file
700
resources/global/en/pieces/expert-hybrid-codex.yaml
Normal file
@ -0,0 +1,700 @@
|
||||
# Expert Review Piece
|
||||
# Review piece with Architecture, Frontend, Security, and QA experts
|
||||
#
|
||||
# Flow:
|
||||
# plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE
|
||||
# ↓ ├─ arch-review ↓
|
||||
# ai_fix ├─ frontend-review fix_supervisor
|
||||
# ├─ security-review
|
||||
# └─ qa-review
|
||||
# any("needs_fix") → fix → reviewers
|
||||
#
|
||||
# AI review runs immediately after implementation to catch AI-specific issues early,
|
||||
# before expert reviews begin.
|
||||
#
|
||||
# Boilerplate sections (Piece Context, User Request, Previous Response,
|
||||
# Additional User Inputs, Instructions heading) are auto-injected by buildInstruction().
|
||||
# Only movement-specific content belongs in instruction_template.
|
||||
#
|
||||
# Template Variables (available in instruction_template):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {previous_response} - Output from the previous movement (only when pass_previous_response: true)
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
#
|
||||
# Movement-level Fields:
|
||||
# report: - Report file(s) for the movement (auto-injected as Report File/Files in Piece Context)
|
||||
# Single: report: 00-plan.md
|
||||
# Multiple: report:
|
||||
# - Scope: 01-coder-scope.md
|
||||
# - Decisions: 02-coder-decisions.md
|
||||
|
||||
name: expert-hybrid-codex
|
||||
description: Architecture, Frontend, Security, QA Expert Review
|
||||
|
||||
max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
movements:
|
||||
# ===========================================
|
||||
# Movement 0: Planning
|
||||
# ===========================================
|
||||
- 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
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
Analyze the task and create an implementation plan.
|
||||
|
||||
**Note:** If returned from implement movement (Previous Response exists),
|
||||
review and revise the plan based on that feedback (replan).
|
||||
|
||||
**Tasks:**
|
||||
1. Understand the requirements
|
||||
2. Identify impact scope
|
||||
3. Decide implementation approach
|
||||
rules:
|
||||
- condition: Task analysis and planning is complete
|
||||
next: implement
|
||||
- condition: Requirements are unclear and planning cannot proceed
|
||||
next: ABORT
|
||||
|
||||
# ===========================================
|
||||
# Movement 1: Implementation
|
||||
# ===========================================
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
Follow the plan from the plan movement and implement.
|
||||
Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
|
||||
Use only the Report Directory files shown in Piece 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 is complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: ai_review
|
||||
- condition: Cannot proceed with implementation
|
||||
next: ai_review
|
||||
- condition: User input required
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
|
||||
# ===========================================
|
||||
# Movement 2: AI Review
|
||||
# ===========================================
|
||||
- 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
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**This is AI Review iteration {movement_iteration}.**
|
||||
|
||||
For the 1st iteration, review thoroughly and report all issues at once.
|
||||
For iteration 2+, prioritize verifying that previously REJECTed items have been fixed.
|
||||
|
||||
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 found
|
||||
next: reviewers
|
||||
- condition: AI-specific issues detected
|
||||
next: ai_fix
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**This is AI Review iteration {movement_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 (`./gradlew :backend:test` etc.)
|
||||
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
|
||||
- Removing scope creep
|
||||
|
||||
**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 "Unable to proceed with fixes"
|
||||
- When "no fix needed", output the tag for "Unable to proceed with fixes" 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}
|
||||
|
||||
**No-implementation handling (required)**
|
||||
rules:
|
||||
- condition: AI Reviewer's issues have been fixed
|
||||
next: ai_review
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
next: ai_no_fix
|
||||
- condition: Unable to proceed with fixes
|
||||
next: ai_no_fix
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_review's findings are valid (fix required)
|
||||
next: ai_fix
|
||||
- condition: ai_fix's judgment is valid (no fix needed)
|
||||
next: reviewers
|
||||
instruction_template: |
|
||||
ai_review (reviewer) and ai_fix (coder) disagree.
|
||||
|
||||
- ai_review found issues and REJECTed
|
||||
- ai_fix verified and determined "no fix needed"
|
||||
|
||||
Review both outputs and arbitrate which judgment is correct.
|
||||
|
||||
**Reports to reference:**
|
||||
- AI Review results: {report:03-ai-review.md}
|
||||
|
||||
**Judgment criteria:**
|
||||
- Are ai_review's findings specific and pointing to real issues in the code?
|
||||
- Does ai_fix's rebuttal have evidence (file verification, test results)?
|
||||
- Are the findings non-blocking (record-only) or do they require actual fixes?
|
||||
|
||||
# ===========================================
|
||||
# Movement 3: Expert Reviews (Parallel)
|
||||
# ===========================================
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-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 Aspects
|
||||
- [x] Structure/Design
|
||||
- [x] Code Quality
|
||||
- [x] Change Scope
|
||||
- [x] Test Coverage
|
||||
- [x] Dead Code
|
||||
- [x] Call Chain Verification
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Scope | Location | Issue | Fix |
|
||||
|---|-------|----------|-------|-----|
|
||||
| 1 | In-scope | `src/file.ts:42` | Issue description | Fix method |
|
||||
|
||||
Scope: "In-scope" (fixable now) / "Out-of-scope" (existing issue, non-blocking)
|
||||
|
||||
## Existing Issues (informational, non-blocking)
|
||||
- {Record of existing issues unrelated to current change}
|
||||
```
|
||||
|
||||
**Cognitive load reduction rules:**
|
||||
- APPROVE -> Summary only (5 lines or less)
|
||||
- REJECT -> Issues in table format (30 lines or less)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Focus on **architecture and design** review. Do NOT review AI-specific issues (that's the ai_review movement).
|
||||
|
||||
**Review Criteria:**
|
||||
- Structure/design validity
|
||||
- Code quality
|
||||
- Change scope appropriateness
|
||||
- Test coverage
|
||||
- Dead code
|
||||
- Call chain verification
|
||||
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
agent: ../agents/expert/frontend-reviewer.md
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# Frontend Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Summary
|
||||
{1-2 sentences summarizing result}
|
||||
|
||||
## Reviewed Perspectives
|
||||
| Perspective | Result | Notes |
|
||||
|-------------|--------|-------|
|
||||
| Component Design | ✅ | - |
|
||||
| State Management | ✅ | - |
|
||||
| Performance | ✅ | - |
|
||||
| Accessibility | ✅ | - |
|
||||
| Type Safety | ✅ | - |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Location | Issue | Fix |
|
||||
|---|----------|-------|-----|
|
||||
| 1 | `src/file.tsx:42` | Issue description | Fix method |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Review the changes from the frontend development perspective.
|
||||
|
||||
**Review Criteria:**
|
||||
- Component design (separation of concerns, granularity)
|
||||
- State management (local/global decisions)
|
||||
- Performance (re-rendering, memoization)
|
||||
- Accessibility (keyboard support, ARIA)
|
||||
- Data fetching patterns
|
||||
- TypeScript type safety
|
||||
|
||||
**Note**: If this project does not include frontend code,
|
||||
approve and proceed to the next movement.
|
||||
|
||||
- name: security-review
|
||||
edit: false
|
||||
agent: ../agents/expert/security-reviewer.md
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# Security Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Severity: None / Low / Medium / High / Critical
|
||||
|
||||
## Check Results
|
||||
| Category | Result | Notes |
|
||||
|----------|--------|-------|
|
||||
| Injection | ✅ | - |
|
||||
| Auth/Authz | ✅ | - |
|
||||
| Data Protection | ✅ | - |
|
||||
| Dependencies | ✅ | - |
|
||||
|
||||
## Vulnerabilities (if REJECT)
|
||||
| # | Severity | Type | Location | Fix |
|
||||
|---|----------|------|----------|-----|
|
||||
| 1 | High | SQLi | `src/db.ts:42` | Use parameterized query |
|
||||
|
||||
## Warnings (non-blocking)
|
||||
- {Security recommendations}
|
||||
```
|
||||
|
||||
**Cognitive load reduction rules:**
|
||||
- No issues -> Check table only (10 lines or less)
|
||||
- Warnings -> + Warnings 1-2 lines (15 lines or less)
|
||||
- Vulnerabilities -> + Table format (30 lines or less)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Perform security review on the changes. Check for vulnerabilities including:
|
||||
- Injection attacks (SQL, Command, XSS)
|
||||
- Authentication/Authorization issues
|
||||
- Data exposure risks
|
||||
- Cryptographic weaknesses
|
||||
|
||||
- name: qa-review
|
||||
edit: false
|
||||
agent: ../agents/expert/qa-reviewer.md
|
||||
report:
|
||||
name: 07-qa-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# QA Review
|
||||
|
||||
## Result: APPROVE / REJECT
|
||||
|
||||
## Summary
|
||||
{1-2 sentences summarizing result}
|
||||
|
||||
## Reviewed Perspectives
|
||||
| Perspective | Result | Notes |
|
||||
|-------------|--------|-------|
|
||||
| Test Coverage | ✅ | - |
|
||||
| Test Quality | ✅ | - |
|
||||
| Error Handling | ✅ | - |
|
||||
| Documentation | ✅ | - |
|
||||
| Maintainability | ✅ | - |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Category | Issue | Fix |
|
||||
|---|----------|-------|-----|
|
||||
| 1 | Testing | Issue description | Fix method |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
Review the changes from the quality assurance perspective.
|
||||
|
||||
**Review Criteria:**
|
||||
- Test coverage and quality
|
||||
- Test strategy (unit/integration/E2E)
|
||||
- Documentation (in-code and external)
|
||||
- Error handling
|
||||
- Logging and monitoring
|
||||
- Maintainability
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: plan
|
||||
instruction_template: |
|
||||
Address the feedback from the reviewers.
|
||||
The "Original User Request" is reference information, not the latest instruction.
|
||||
Review the session conversation history and fix the issues raised by the reviewers.
|
||||
|
||||
|
||||
**Required output (include headings)**
|
||||
## Work done
|
||||
- {summary of work performed}
|
||||
## Changes made
|
||||
- {summary of code changes}
|
||||
## Test results
|
||||
- {command and outcome}
|
||||
## Evidence
|
||||
- {key files/grep/diff/log evidence you verified}
|
||||
|
||||
# ===========================================
|
||||
# Movement 4: Supervision
|
||||
# ===========================================
|
||||
- name: supervise
|
||||
edit: false
|
||||
agent: ../agents/expert/supervisor.md
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Previous Reviews Summary
|
||||
Reaching this movement means all the following reviews have been APPROVED:
|
||||
- Architecture Review: APPROVED
|
||||
- Frontend Review: APPROVED
|
||||
- AI Review: APPROVED
|
||||
- Security Review: APPROVED
|
||||
- QA Review: APPROVED
|
||||
|
||||
Run tests, verify the build, and perform final approval.
|
||||
|
||||
**Piece Overall Review:**
|
||||
1. Does the implementation match the plan ({report:00-plan.md})?
|
||||
2. Were all review movement 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 |
|
||||
|--------|--------|
|
||||
| Architecture | ✅ APPROVE |
|
||||
| Frontend | ✅ APPROVE |
|
||||
| AI Review | ✅ APPROVE |
|
||||
| Security | ✅ APPROVE |
|
||||
| QA | ✅ APPROVE |
|
||||
| Supervisor | ✅ APPROVE |
|
||||
|
||||
## Verification Commands
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
rules:
|
||||
- condition: All validations pass and ready to merge
|
||||
next: COMPLETE
|
||||
- condition: Issues detected during final review
|
||||
next: fix_supervisor
|
||||
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
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: Supervisor's issues have been fixed
|
||||
next: supervise
|
||||
- condition: Unable to proceed with fixes
|
||||
next: plan
|
||||
428
resources/global/en/pieces/minimal-hybrid-codex.yaml
Normal file
428
resources/global/en/pieces/minimal-hybrid-codex.yaml
Normal file
@ -0,0 +1,428 @@
|
||||
# Minimal TAKT Piece
|
||||
# Implement -> Parallel Review (AI + Supervisor) -> Fix if needed -> Complete
|
||||
# (Simplest configuration - no plan, no architect review)
|
||||
#
|
||||
# Template Variables (auto-injected):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {task} - Original user request (auto-injected)
|
||||
# {previous_response} - Output from the previous movement (auto-injected)
|
||||
# {user_inputs} - Accumulated user inputs during piece (auto-injected)
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
|
||||
name: minimal-hybrid-codex
|
||||
description: Minimal development piece (implement -> parallel review -> fix if needed -> complete)
|
||||
|
||||
max_iterations: 20
|
||||
|
||||
initial_movement: implement
|
||||
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
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 Piece 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
|
||||
|
||||
- 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
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
Run tests, verify the build, and perform final approval.
|
||||
|
||||
**Piece 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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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 {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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 {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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}
|
||||
43
resources/global/en/pieces/passthrough-hybrid-codex.yaml
Normal file
43
resources/global/en/pieces/passthrough-hybrid-codex.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
# Passthrough TAKT Piece
|
||||
# Thinnest wrapper. Pass task directly to coder as-is.
|
||||
#
|
||||
# Flow:
|
||||
# execute (do the task)
|
||||
# ↓
|
||||
# COMPLETE
|
||||
|
||||
name: passthrough-hybrid-codex
|
||||
description: Single-agent thin wrapper. Pass task directly to coder as-is.
|
||||
|
||||
max_iterations: 10
|
||||
|
||||
initial_movement: execute
|
||||
|
||||
movements:
|
||||
- name: execute
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
report:
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Task complete
|
||||
next: COMPLETE
|
||||
- condition: Cannot proceed
|
||||
next: ABORT
|
||||
- condition: User input required
|
||||
next: execute
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
Do the task.
|
||||
42
resources/global/en/pieces/passthrough.yaml
Normal file
42
resources/global/en/pieces/passthrough.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
# Passthrough TAKT Piece
|
||||
# Thinnest wrapper. Pass task directly to coder as-is.
|
||||
#
|
||||
# Flow:
|
||||
# execute (do the task)
|
||||
# ↓
|
||||
# COMPLETE
|
||||
|
||||
name: passthrough
|
||||
description: Single-agent thin wrapper. Pass task directly to coder as-is.
|
||||
|
||||
max_iterations: 10
|
||||
|
||||
initial_movement: execute
|
||||
|
||||
movements:
|
||||
- name: execute
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
report:
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Task complete
|
||||
next: COMPLETE
|
||||
- condition: Cannot proceed
|
||||
next: ABORT
|
||||
- condition: User input required
|
||||
next: execute
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
Do the task.
|
||||
428
resources/global/en/pieces/review-fix-minimal-hybrid-codex.yaml
Normal file
428
resources/global/en/pieces/review-fix-minimal-hybrid-codex.yaml
Normal file
@ -0,0 +1,428 @@
|
||||
# Review-Fix Minimal TAKT Piece
|
||||
# Review -> Fix (if needed) -> Re-review -> Complete
|
||||
# (Starts with review, no implementation movement)
|
||||
#
|
||||
# Template Variables (auto-injected):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {task} - Original user request (auto-injected)
|
||||
# {previous_response} - Output from the previous movement (auto-injected)
|
||||
# {user_inputs} - Accumulated user inputs during piece (auto-injected)
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
|
||||
name: review-fix-minimal-hybrid-codex
|
||||
description: Review and fix piece for existing code (starts with review, no implementation)
|
||||
|
||||
max_iterations: 20
|
||||
|
||||
initial_movement: reviewers
|
||||
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
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 Piece 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
|
||||
|
||||
- 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
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
Run tests, verify the build, and perform final approval.
|
||||
|
||||
**Piece 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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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 {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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 {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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}
|
||||
@ -2,6 +2,8 @@ piece_categories:
|
||||
"🚀 クイックスタート":
|
||||
pieces:
|
||||
- default
|
||||
- passthrough
|
||||
- coding
|
||||
- minimal
|
||||
|
||||
"🔍 レビュー&修正":
|
||||
@ -19,6 +21,21 @@ piece_categories:
|
||||
- expert
|
||||
- expert-cqrs
|
||||
|
||||
"🔀 ハイブリッド (Codex Coding)":
|
||||
"🚀 クイックスタート":
|
||||
pieces:
|
||||
- default-hybrid-codex
|
||||
- passthrough-hybrid-codex
|
||||
- coding-hybrid-codex
|
||||
- minimal-hybrid-codex
|
||||
"🔍 レビュー&修正":
|
||||
pieces:
|
||||
- review-fix-minimal-hybrid-codex
|
||||
"🔧 フルスタック":
|
||||
pieces:
|
||||
- expert-hybrid-codex
|
||||
- expert-cqrs-hybrid-codex
|
||||
|
||||
"その他":
|
||||
pieces:
|
||||
- research
|
||||
359
resources/global/ja/pieces/coding-hybrid-codex.yaml
Normal file
359
resources/global/ja/pieces/coding-hybrid-codex.yaml
Normal file
@ -0,0 +1,359 @@
|
||||
# Coding TAKT Piece
|
||||
# Architect -> Implement -> Parallel Review (AI + Architecture) -> Fix if needed
|
||||
#
|
||||
# 設計を重視しながらも、planとsuperviseを省略した軽量な開発ピース。
|
||||
# 並列レビュー後、問題がなければ直接完了し、高速なフィードバックループを実現。
|
||||
#
|
||||
# フロー:
|
||||
# architect (設計)
|
||||
# ↓
|
||||
# implement (実装)
|
||||
# ↓
|
||||
# reviewers (並列レビュー)
|
||||
# ├─ ai_review (AI特有問題検出)
|
||||
# └─ arch-review (設計準拠性確認)
|
||||
# ↓
|
||||
# [判定]
|
||||
# ├─ all(approved) → COMPLETE
|
||||
# └─ any(needs_fix) → fix → reviewers (再レビュー)
|
||||
#
|
||||
# Template Variables (auto-injected by buildInstruction):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {task} - Original user request
|
||||
# {previous_response} - Output from the previous movement
|
||||
# {user_inputs} - Accumulated user inputs during piece
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
|
||||
name: coding-hybrid-codex
|
||||
description: Architecture-focused development piece with parallel reviews (architect -> implement -> parallel review -> complete)
|
||||
|
||||
max_iterations: 20
|
||||
|
||||
initial_movement: architect-plan
|
||||
|
||||
movements:
|
||||
- name: architect-plan
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
report:
|
||||
name: architecture-reviewer
|
||||
format: |
|
||||
```markdown
|
||||
# アーキテクチャ設計
|
||||
|
||||
## タスク規模
|
||||
Small / Medium / Large
|
||||
|
||||
## 設計判断
|
||||
|
||||
### ファイル構成
|
||||
| ファイル | 役割 |
|
||||
|---------|------|
|
||||
| `src/example.ts` | 概要 |
|
||||
|
||||
### 技術選定
|
||||
- {選定した技術・ライブラリとその理由}
|
||||
|
||||
### 設計パターン
|
||||
- {採用するパターンと適用箇所}
|
||||
|
||||
## 実装ガイドライン
|
||||
- {Coderが実装時に従うべき指針}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: 小規模タスク(設計不要)
|
||||
next: implement
|
||||
- condition: 設計完了
|
||||
next: implement
|
||||
- condition: 情報不足、判断できない
|
||||
next: ABORT
|
||||
instruction_template: |
|
||||
タスクのアーキテクチャ設計を行ってください。
|
||||
|
||||
**タスク**: {task}
|
||||
**進行状況**: {iteration}/{max_iterations} ターン
|
||||
|
||||
**小規模タスクの判断基準:**
|
||||
- 1-2ファイルの変更のみ
|
||||
- 既存パターンの踏襲で済む
|
||||
- 技術選定が不要
|
||||
|
||||
小規模タスクの場合は設計レポートを作成せず、「小規模タスク(設計不要)」のルールに対応してください。
|
||||
|
||||
**設計が必要なタスク:**
|
||||
- 3ファイル以上の変更
|
||||
- 新しいモジュール・機能の追加
|
||||
- 技術選定が必要
|
||||
- アーキテクチャパターンの決定が必要
|
||||
|
||||
**やること:**
|
||||
1. タスクの規模を評価(Small/Medium/Large)
|
||||
2. 影響を受けるファイル構成を特定
|
||||
3. 必要に応じて技術選定を行う
|
||||
4. 設計パターンの選択
|
||||
5. Coderへの実装ガイドライン作成
|
||||
|
||||
**やらないこと:**
|
||||
- コードの実装(Coderの仕事)
|
||||
- コードレビュー
|
||||
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 実装完了
|
||||
next: reviewers
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: reviewers
|
||||
- condition: 判断できない、情報不足
|
||||
next: reviewers
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
architect-planムーブメントで決定した設計に従って実装してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- 設計: {report:01-architecture.md}(存在する場合)
|
||||
|
||||
Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
|
||||
|
||||
**重要:** 設計判断はせず、architect-planムーブメントで決定された設計に従ってください。
|
||||
不明点や設計の変更が必要な場合は報告してください。
|
||||
|
||||
**重要**: 実装と同時に単体テストを追加してください。
|
||||
- 新規作成したクラス・関数には単体テストを追加
|
||||
- 既存コードを変更した場合は該当するテストを更新
|
||||
- テストファイルの配置: プロジェクトの規約に従う(例: `__tests__/`, `*.test.ts`)
|
||||
- **テスト実行は必須です。** 実装完了後、必ずテストを実行して結果を確認してください。
|
||||
|
||||
**Scopeレポートフォーマット(実装開始時に作成):**
|
||||
```markdown
|
||||
# 変更スコープ宣言
|
||||
|
||||
## タスク
|
||||
{タスクの1行要約}
|
||||
|
||||
## 変更予定
|
||||
| 種別 | ファイル |
|
||||
|------|---------|
|
||||
| 作成 | `src/example.ts` |
|
||||
| 変更 | `src/routes.ts` |
|
||||
|
||||
## 推定規模
|
||||
Small / Medium / Large
|
||||
|
||||
## 影響範囲
|
||||
- {影響するモジュールや機能}
|
||||
```
|
||||
|
||||
**Decisionsレポートフォーマット(実装完了時、決定がある場合のみ):**
|
||||
```markdown
|
||||
# 決定ログ
|
||||
|
||||
## 1. {決定内容}
|
||||
- **背景**: {なぜ決定が必要だったか}
|
||||
- **検討した選択肢**: {選択肢リスト}
|
||||
- **理由**: {選んだ理由}
|
||||
```
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: ai_review
|
||||
edit: false
|
||||
agent: ../agents/default/ai-antipattern-reviewer.md
|
||||
report:
|
||||
name: 04-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
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: AI特有の問題なし
|
||||
- condition: AI特有の問題あり
|
||||
instruction_template: |
|
||||
AI特有の問題についてコードをレビューしてください:
|
||||
- 仮定の検証
|
||||
- もっともらしいが間違っているパターン
|
||||
- 既存コードベースとの適合性
|
||||
- スコープクリープの検出
|
||||
|
||||
**参照するレポート:**
|
||||
- 実装スコープ: {report:02-coder-scope.md}
|
||||
- 決定ログ: {report:03-coder-decisions.md}(存在する場合)
|
||||
|
||||
- name: arch-review
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
report:
|
||||
name: 05-architect-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# アーキテクチャレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
- [x] 構造・設計
|
||||
- [x] コード品質
|
||||
- [x] 変更スコープ
|
||||
- [x] テストカバレッジ
|
||||
- [x] デッドコード
|
||||
- [x] 呼び出しチェーン検証
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | スコープ | 場所 | 問題 | 修正案 |
|
||||
|---|---------|------|------|--------|
|
||||
| 1 | スコープ内 | `src/file.ts:42` | 問題の説明 | 修正方法 |
|
||||
|
||||
スコープ: 「スコープ内」(今回修正可能)/ 「スコープ外」(既存問題・非ブロッキング)
|
||||
|
||||
## 既存問題(参考・非ブロッキング)
|
||||
- {既存問題の記録。今回の変更と無関係な問題}
|
||||
```
|
||||
|
||||
**認知負荷軽減ルール:**
|
||||
- APPROVE → サマリーのみ(5行以内)
|
||||
- REJECT → 問題点を表形式で(30行以内)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
**実装がarchitectムーブメントの設計に従っているか**を確認してください。
|
||||
AI特有の問題はレビューしないでください(ai_reviewムーブメントで行います)。
|
||||
|
||||
**参照するレポート:**
|
||||
- 設計: {report:01-architecture.md}(存在する場合)
|
||||
- 実装スコープ: {report:02-coder-scope.md}
|
||||
|
||||
**レビュー観点:**
|
||||
- 設計との整合性(architectが定めたファイル構成・パターンに従っているか)
|
||||
- コード品質(DRY、YAGNI、Fail Fast、イディオマティック)
|
||||
- 変更スコープの適切性
|
||||
- テストカバレッジ
|
||||
- デッドコード
|
||||
- 呼び出しチェーン検証
|
||||
|
||||
**注意:** architectムーブメントをスキップした小規模タスクの場合は、従来通り設計の妥当性も確認してください。
|
||||
|
||||
rules:
|
||||
- condition: all("AI特有の問題なし", "approved")
|
||||
next: COMPLETE
|
||||
- condition: any("AI特有の問題あり", "needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正完了
|
||||
next: reviewers
|
||||
- condition: 判断できない、情報不足
|
||||
next: ABORT
|
||||
instruction_template: |
|
||||
レビュアーのフィードバックに対応してください。
|
||||
|
||||
**両方のレビュー結果を確認してください:**
|
||||
- AI Review: {report:04-ai-review.md}
|
||||
- Architecture Review: {report:05-architect-review.md}
|
||||
|
||||
**重要:** 両方のレビューで指摘された問題を全て修正してください。
|
||||
- AI Reviewの指摘: 幻覚API、仮定の妥当性、スコープクリープ等
|
||||
- Architecture Reviewの指摘: 設計との整合性、コード品質、テストカバレッジ等
|
||||
|
||||
**必須アクション:**
|
||||
1. 指摘された全ファイルを Read tool で開く
|
||||
2. 問題箇所を確認する
|
||||
3. Edit tool で修正する
|
||||
4. **テストを実行して検証する(必須)**
|
||||
5. 修正内容を具体的に報告する
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
628
resources/global/ja/pieces/default-hybrid-codex.yaml
Normal file
628
resources/global/ja/pieces/default-hybrid-codex.yaml
Normal file
@ -0,0 +1,628 @@
|
||||
# Default TAKT Piece
|
||||
# Plan -> Architect -> Implement -> AI Review -> Reviewers (parallel: Architect + QA) -> Supervisor Approval
|
||||
#
|
||||
# Template Variables (auto-injected by buildInstruction):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {task} - Original user request
|
||||
# {previous_response} - Output from the previous movement
|
||||
# {user_inputs} - Accumulated user inputs during piece
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
|
||||
name: default-hybrid-codex
|
||||
description: Standard development piece with planning and specialized reviews
|
||||
|
||||
max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
loop_monitors:
|
||||
- cycle: [ai_review, ai_fix]
|
||||
threshold: 3
|
||||
judge:
|
||||
agent: ../agents/default/supervisor.md
|
||||
instruction_template: |
|
||||
ai_review と ai_fix のループが {cycle_count} 回繰り返されました。
|
||||
|
||||
各サイクルのレポートを確認し、このループが健全(進捗がある)か、
|
||||
非生産的(同じ問題を繰り返している)かを判断してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- AIレビュー結果: {report:04-ai-review.md}
|
||||
|
||||
**判断基準:**
|
||||
- 各サイクルで新しい問題が発見・修正されているか
|
||||
- 同じ指摘が繰り返されていないか
|
||||
- 修正が実際に反映されているか
|
||||
rules:
|
||||
- condition: 健全(進捗あり)
|
||||
next: ai_review
|
||||
- condition: 非生産的(改善なし)
|
||||
next: reviewers
|
||||
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
agent: ../agents/default/planner.md
|
||||
report:
|
||||
name: 00-plan.md
|
||||
format: |
|
||||
```markdown
|
||||
# タスク計画
|
||||
|
||||
## 元の要求
|
||||
{ユーザーの要求をそのまま記載}
|
||||
|
||||
## 分析結果
|
||||
|
||||
### 目的
|
||||
{達成すべきこと}
|
||||
|
||||
### スコープ
|
||||
{影響範囲}
|
||||
|
||||
### 実装アプローチ
|
||||
{どう進めるか}
|
||||
|
||||
## 確認事項(あれば)
|
||||
- {不明点や確認が必要な点}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: 要件が明確で実装可能
|
||||
next: architect
|
||||
- condition: ユーザーが質問をしている(実装タスクではない)
|
||||
next: COMPLETE
|
||||
- condition: 要件が不明確、情報不足
|
||||
next: ABORT
|
||||
appendix: |
|
||||
確認事項:
|
||||
- {質問1}
|
||||
- {質問2}
|
||||
instruction_template: |
|
||||
タスクを分析し、実装方針を立ててください。
|
||||
|
||||
**注意:** Previous Responseがある場合は差し戻しのため、
|
||||
その内容を踏まえて計画を見直してください(replan)。
|
||||
|
||||
**やること(実装タスクの場合):**
|
||||
1. タスクの要件を理解する
|
||||
2. 影響範囲を特定する
|
||||
3. 実装アプローチを決める
|
||||
|
||||
- name: architect
|
||||
edit: false
|
||||
agent: ../agents/default/architect.md
|
||||
report:
|
||||
name: 01-architecture.md
|
||||
format: |
|
||||
```markdown
|
||||
# アーキテクチャ設計
|
||||
|
||||
## タスク規模
|
||||
Small / Medium / Large
|
||||
|
||||
## 設計判断
|
||||
|
||||
### ファイル構成
|
||||
| ファイル | 役割 |
|
||||
|---------|------|
|
||||
| `src/example.ts` | 概要 |
|
||||
|
||||
### 技術選定
|
||||
- {選定した技術・ライブラリとその理由}
|
||||
|
||||
### 設計パターン
|
||||
- {採用するパターンと適用箇所}
|
||||
|
||||
## 実装ガイドライン
|
||||
- {Coderが実装時に従うべき指針}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: 小規模タスク(設計不要)
|
||||
next: implement
|
||||
- condition: 設計完了
|
||||
next: implement
|
||||
- condition: 情報不足、判断できない
|
||||
next: ABORT
|
||||
instruction_template: |
|
||||
計画レポート({report:00-plan.md})を読み、アーキテクチャ設計を行ってください。
|
||||
|
||||
**小規模タスクの判断基準:**
|
||||
- 1-2ファイルの変更のみ
|
||||
- 既存パターンの踏襲で済む
|
||||
- 技術選定が不要
|
||||
|
||||
小規模タスクの場合は設計レポートを作成せず、「小規模タスク(設計不要)」のルールに対応してください。
|
||||
|
||||
**設計が必要なタスク:**
|
||||
- 3ファイル以上の変更
|
||||
- 新しいモジュール・機能の追加
|
||||
- 技術選定が必要
|
||||
- アーキテクチャパターンの決定が必要
|
||||
|
||||
**やること:**
|
||||
1. タスクの規模を評価
|
||||
2. ファイル構成を決定
|
||||
3. 技術選定(必要な場合)
|
||||
4. 設計パターンの選択
|
||||
5. Coderへの実装ガイドライン作成
|
||||
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 実装完了
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: ai_review
|
||||
- condition: 判断できない、情報不足
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
planムーブメントで立てた計画と、architectムーブメントで決定した設計に従って実装してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- 計画: {report:00-plan.md}
|
||||
- 設計: {report:01-architecture.md}(存在する場合)
|
||||
|
||||
Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
|
||||
|
||||
**重要:** 設計判断はせず、architectムーブメントで決定された設計に従ってください。
|
||||
不明点や設計の変更が必要な場合は報告してください。
|
||||
|
||||
**重要**: 実装と同時に単体テストを追加してください。
|
||||
- 新規作成したクラス・関数には単体テストを追加
|
||||
- 既存コードを変更した場合は該当するテストを更新
|
||||
- テストファイルの配置: プロジェクトの規約に従う(例: `__tests__/`, `*.test.ts`)
|
||||
|
||||
**Scopeレポートフォーマット(実装開始時に作成):**
|
||||
```markdown
|
||||
# 変更スコープ宣言
|
||||
|
||||
## タスク
|
||||
{タスクの1行要約}
|
||||
|
||||
## 変更予定
|
||||
| 種別 | ファイル |
|
||||
|------|---------|
|
||||
| 作成 | `src/example.ts` |
|
||||
| 変更 | `src/routes.ts` |
|
||||
|
||||
## 推定規模
|
||||
Small / Medium / Large
|
||||
|
||||
## 影響範囲
|
||||
- {影響するモジュールや機能}
|
||||
```
|
||||
|
||||
**Decisionsレポートフォーマット(実装完了時、決定がある場合のみ):**
|
||||
```markdown
|
||||
# 決定ログ
|
||||
|
||||
## 1. {決定内容}
|
||||
- **背景**: {なぜ決定が必要だったか}
|
||||
- **検討した選択肢**: {選択肢リスト}
|
||||
- **理由**: {選んだ理由}
|
||||
```
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
|
||||
|
||||
- name: ai_review
|
||||
edit: false
|
||||
agent: ../agents/default/ai-antipattern-reviewer.md
|
||||
report:
|
||||
name: 04-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
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: AI特有の問題なし
|
||||
next: reviewers
|
||||
- condition: AI特有の問題あり
|
||||
next: ai_fix
|
||||
instruction_template: |
|
||||
**これは {movement_iteration} 回目のAI Reviewです。**
|
||||
|
||||
初回は網羅的にレビューし、指摘すべき問題をすべて出し切ってください。
|
||||
2回目以降は、前回REJECTした項目が修正されたかの確認を優先してください。
|
||||
|
||||
AI特有の問題についてコードをレビューしてください:
|
||||
- 仮定の検証
|
||||
- もっともらしいが間違っているパターン
|
||||
- 既存コードベースとの適合性
|
||||
- スコープクリープの検出
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
next: ai_review
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: ai_no_fix
|
||||
- condition: 判断できない、情報不足
|
||||
next: ai_no_fix
|
||||
instruction_template: |
|
||||
**これは {movement_iteration} 回目の AI Review です。**
|
||||
|
||||
2回目以降は、前回の修正が実際には行われていなかったということです。
|
||||
**あなたの「修正済み」という認識が間違っています。**
|
||||
|
||||
**まず認めること:**
|
||||
- 「修正済み」と思っていたファイルは実際には修正されていない
|
||||
- 前回の作業内容の認識が間違っている
|
||||
- ゼロベースで考え直す必要がある
|
||||
|
||||
**必須アクション:**
|
||||
1. 指摘された全ファイルを Read tool で開く(思い込みを捨てて事実確認)
|
||||
2. 問題箇所を grep で検索して実在を確認する
|
||||
3. 確認した問題を Edit tool で修正する
|
||||
4. テストを実行して検証する(`./gradlew :backend:test` 等)
|
||||
5. 「何を確認して、何を修正したか」を具体的に報告する
|
||||
|
||||
**報告フォーマット:**
|
||||
- ❌ 「既に修正されています」
|
||||
- ✅ 「ファイルXのL123を確認した結果、問題Yが存在したため、Zに修正しました」
|
||||
|
||||
**絶対に禁止:**
|
||||
- ファイルを開かずに「修正済み」と報告
|
||||
|
||||
**修正不要の扱い(必須)**
|
||||
- AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない
|
||||
- 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「判断できない、情報不足」に対応するタグを出力する
|
||||
- 修正不要の場合は「判断できない、情報不足」に対応するタグを出力し、理由と確認範囲を明記する
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 確認したファイル
|
||||
- {ファイルパス:行番号}
|
||||
## 実行した検索
|
||||
- {コマンドと要約}
|
||||
## 修正内容
|
||||
- {変更内容}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
- 思い込みで判断
|
||||
- AI Reviewer が REJECT した問題の放置
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_reviewの指摘が妥当(修正すべき)
|
||||
next: ai_fix
|
||||
- condition: ai_fixの判断が妥当(修正不要)
|
||||
next: reviewers
|
||||
instruction_template: |
|
||||
ai_review(レビュアー)と ai_fix(コーダー)の意見が食い違っています。
|
||||
|
||||
- ai_review は問題を指摘し REJECT しました
|
||||
- ai_fix は確認の上「修正不要」と判断しました
|
||||
|
||||
両者の出力を確認し、どちらの判断が妥当か裁定してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- AIレビュー結果: {report:04-ai-review.md}
|
||||
|
||||
**判断基準:**
|
||||
- ai_review の指摘が具体的で、コード上の実在する問題を指しているか
|
||||
- ai_fix の反論に根拠(ファイル確認結果、テスト結果)があるか
|
||||
- 指摘が非ブロッキング(記録のみ)レベルか、実際に修正が必要か
|
||||
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
report:
|
||||
name: 05-architect-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# アーキテクチャレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
- [x] 構造・設計
|
||||
- [x] コード品質
|
||||
- [x] 変更スコープ
|
||||
- [x] テストカバレッジ
|
||||
- [x] デッドコード
|
||||
- [x] 呼び出しチェーン検証
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | スコープ | 場所 | 問題 | 修正案 |
|
||||
|---|---------|------|------|--------|
|
||||
| 1 | スコープ内 | `src/file.ts:42` | 問題の説明 | 修正方法 |
|
||||
|
||||
スコープ: 「スコープ内」(今回修正可能)/ 「スコープ外」(既存問題・非ブロッキング)
|
||||
|
||||
## 既存問題(参考・非ブロッキング)
|
||||
- {既存問題の記録。今回の変更と無関係な問題}
|
||||
```
|
||||
|
||||
**認知負荷軽減ルール:**
|
||||
- APPROVE → サマリーのみ(5行以内)
|
||||
- REJECT → 問題点を表形式で(30行以内)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
**実装がarchitectムーブメントの設計に従っているか**を確認してください。
|
||||
AI特有の問題はレビューしないでください(ai_reviewムーブメントで行います)。
|
||||
|
||||
**参照するレポート:**
|
||||
- 設計: {report:01-architecture.md}(存在する場合)
|
||||
- 実装スコープ: {report:02-coder-scope.md}
|
||||
|
||||
**レビュー観点:**
|
||||
- 設計との整合性(architectが定めたファイル構成・パターンに従っているか)
|
||||
- コード品質
|
||||
- 変更スコープの適切性
|
||||
- テストカバレッジ
|
||||
- デッドコード
|
||||
- 呼び出しチェーン検証
|
||||
|
||||
**注意:** architectムーブメントをスキップした小規模タスクの場合は、従来通り設計の妥当性も確認してください。
|
||||
|
||||
- name: qa-review
|
||||
edit: false
|
||||
agent: ../agents/default/qa-reviewer.md
|
||||
report:
|
||||
name: 06-qa-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# QAレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## レビュー観点
|
||||
| 観点 | 結果 | 備考 |
|
||||
|------|------|------|
|
||||
| テストカバレッジ | ✅ | - |
|
||||
| テスト品質 | ✅ | - |
|
||||
| エラーハンドリング | ✅ | - |
|
||||
| ドキュメント | ✅ | - |
|
||||
| 保守性 | ✅ | - |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | カテゴリ | 問題 | 修正案 |
|
||||
|---|---------|------|--------|
|
||||
| 1 | テスト | 問題の説明 | 修正方法 |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
品質保証の観点から変更をレビューしてください。
|
||||
|
||||
**レビュー観点:**
|
||||
- テストカバレッジと品質
|
||||
- テスト戦略(unit/integration/E2E)
|
||||
- エラーハンドリング
|
||||
- ログとモニタリング
|
||||
- 保守性
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正完了
|
||||
next: reviewers
|
||||
- condition: 判断できない、情報不足
|
||||
next: plan
|
||||
instruction_template: |
|
||||
レビュアーのフィードバックに対応してください。
|
||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
|
||||
- name: supervise
|
||||
edit: false
|
||||
agent: ../agents/default/supervisor.md
|
||||
report:
|
||||
- Validation: 07-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: すべて問題なし
|
||||
next: COMPLETE
|
||||
- condition: 要求未達成、テスト失敗、ビルドエラー
|
||||
next: plan
|
||||
instruction_template: |
|
||||
テスト実行、ビルド確認、最終承認を行ってください。
|
||||
|
||||
**ピース全体の確認:**
|
||||
1. 計画({report:00-plan.md})と設計({report:01-architecture.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` | 概要説明 |
|
||||
|
||||
## レビュー結果
|
||||
| レビュー | 結果 |
|
||||
|---------|------|
|
||||
| Architecture Design | ✅ 完了 |
|
||||
| AI Review | ✅ APPROVE |
|
||||
| Architect Review | ✅ APPROVE |
|
||||
| QA | ✅ APPROVE |
|
||||
| Supervisor | ✅ APPROVE |
|
||||
|
||||
## 確認コマンド
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
@ -17,6 +17,30 @@ max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
loop_monitors:
|
||||
- cycle: [ai_review, ai_fix]
|
||||
threshold: 3
|
||||
judge:
|
||||
agent: ../agents/default/supervisor.md
|
||||
instruction_template: |
|
||||
ai_review と ai_fix のループが {cycle_count} 回繰り返されました。
|
||||
|
||||
各サイクルのレポートを確認し、このループが健全(進捗がある)か、
|
||||
非生産的(同じ問題を繰り返している)かを判断してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- AIレビュー結果: {report:04-ai-review.md}
|
||||
|
||||
**判断基準:**
|
||||
- 各サイクルで新しい問題が発見・修正されているか
|
||||
- 同じ指摘が繰り返されていないか
|
||||
- 修正が実際に反映されているか
|
||||
rules:
|
||||
- condition: 健全(進捗あり)
|
||||
next: ai_review
|
||||
- condition: 非生産的(改善なし)
|
||||
next: reviewers
|
||||
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
|
||||
694
resources/global/ja/pieces/expert-cqrs-hybrid-codex.yaml
Normal file
694
resources/global/ja/pieces/expert-cqrs-hybrid-codex.yaml
Normal file
@ -0,0 +1,694 @@
|
||||
# Expert Review Piece
|
||||
# CQRS+ES、フロントエンド、セキュリティ、QAの専門家によるレビューピース
|
||||
#
|
||||
# フロー:
|
||||
# plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE
|
||||
# ↓ ├─ cqrs-es-review ↓
|
||||
# ai_fix ├─ frontend-review fix_supervisor
|
||||
# ├─ security-review
|
||||
# └─ qa-review
|
||||
# any("needs_fix") → fix → reviewers
|
||||
#
|
||||
# ボイラープレートセクション(Piece Context, User Request, Previous Response,
|
||||
# Additional User Inputs, Instructions heading)はbuildInstruction()が自動挿入。
|
||||
# instruction_templateにはムーブメント固有の内容のみ記述。
|
||||
#
|
||||
# テンプレート変数(instruction_template内で使用可能):
|
||||
# {iteration} - ピース全体のターン数(全エージェントで実行されたムーブメントの合計)
|
||||
# {max_iterations} - ピースの最大イテレーション数
|
||||
# {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか)
|
||||
# {previous_response} - 前のムーブメントの出力(pass_previous_response: true の場合のみ)
|
||||
# {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary")
|
||||
#
|
||||
# ムーブメントレベルフィールド:
|
||||
# report: - ムーブメントのレポートファイル(Piece ContextにReport File/Filesとして自動挿入)
|
||||
# 単一: report: 00-plan.md
|
||||
# 複数: report:
|
||||
# - Scope: 01-coder-scope.md
|
||||
# - Decisions: 02-coder-decisions.md
|
||||
|
||||
name: expert-cqrs-hybrid-codex
|
||||
description: CQRS+ES・フロントエンド・セキュリティ・QA専門家レビュー
|
||||
|
||||
max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
movements:
|
||||
# ===========================================
|
||||
# Movement 0: Planning
|
||||
# ===========================================
|
||||
- name: plan
|
||||
edit: false
|
||||
agent: ../agents/default/planner.md
|
||||
report:
|
||||
name: 00-plan.md
|
||||
format: |
|
||||
```markdown
|
||||
# タスク計画
|
||||
|
||||
## 元の要求
|
||||
{ユーザーの要求をそのまま記載}
|
||||
|
||||
## 分析結果
|
||||
|
||||
### 目的
|
||||
{達成すべきこと}
|
||||
|
||||
### スコープ
|
||||
{影響範囲}
|
||||
|
||||
### 実装アプローチ
|
||||
{どう進めるか}
|
||||
|
||||
## 確認事項(あれば)
|
||||
- {不明点や確認が必要な点}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
タスクを分析し、実装方針を立ててください。
|
||||
|
||||
**注意:** Previous Responseがある場合は差し戻しのため、
|
||||
その内容を踏まえて計画を見直してください(replan)。
|
||||
|
||||
**やること:**
|
||||
1. タスクの要件を理解する
|
||||
2. 影響範囲を特定する
|
||||
3. 実装アプローチを決める
|
||||
rules:
|
||||
- condition: タスク分析と計画が完了した
|
||||
next: implement
|
||||
- condition: 要件が不明確で計画を立てられない
|
||||
next: ABORT
|
||||
|
||||
# ===========================================
|
||||
# Movement 1: Implementation
|
||||
# ===========================================
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
planムーブメントで立てた計画に従って実装してください。
|
||||
計画レポート({report:00-plan.md})を参照し、実装を進めてください。
|
||||
Piece 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
|
||||
|
||||
# ===========================================
|
||||
# Movement 2: AI Review
|
||||
# ===========================================
|
||||
- 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
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**これは {movement_iteration} 回目のAI Reviewです。**
|
||||
|
||||
初回は網羅的にレビューし、指摘すべき問題をすべて出し切ってください。
|
||||
2回目以降は、前回REJECTした項目が修正されたかの確認を優先してください。
|
||||
|
||||
AI特有の問題についてコードをレビューしてください:
|
||||
- 仮定の検証
|
||||
- もっともらしいが間違っているパターン
|
||||
- 既存コードベースとの適合性
|
||||
- スコープクリープの検出
|
||||
rules:
|
||||
- condition: AI特有の問題が見つからない
|
||||
next: reviewers
|
||||
- condition: AI特有の問題が検出された
|
||||
next: ai_fix
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**これは {movement_iteration} 回目の AI Review です。**
|
||||
|
||||
2回目以降は、前回の修正が実際には行われていなかったということです。
|
||||
**あなたの「修正済み」という認識が間違っています。**
|
||||
|
||||
**まず認めること:**
|
||||
- 「修正済み」と思っていたファイルは実際には修正されていない
|
||||
- 前回の作業内容の認識が間違っている
|
||||
- ゼロベースで考え直す必要がある
|
||||
|
||||
**必須アクション:**
|
||||
1. 指摘された全ファイルを Read tool で開く(思い込みを捨てて事実確認)
|
||||
2. 問題箇所を grep で検索して実在を確認する
|
||||
3. 確認した問題を Edit tool で修正する
|
||||
4. テストを実行して検証する(`./gradlew :backend:test` 等)
|
||||
5. 「何を確認して、何を修正したか」を具体的に報告する
|
||||
|
||||
**報告フォーマット:**
|
||||
- ❌ 「既に修正されています」
|
||||
- ✅ 「ファイルXのL123を確認した結果、問題Yが存在したため、Zに修正しました」
|
||||
|
||||
**絶対に禁止:**
|
||||
- ファイルを開かずに「修正済み」と報告
|
||||
- 思い込みで判断
|
||||
- AI Reviewer が REJECT した問題の放置
|
||||
|
||||
**修正不要の扱い(必須)**
|
||||
- AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない
|
||||
- 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「修正を進行できない」に対応するタグを出力する
|
||||
- 修正不要の場合は「修正を進行できない」に対応するタグを出力し、理由と確認範囲を明記する
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 確認したファイル
|
||||
- {ファイルパス:行番号}
|
||||
## 実行した検索
|
||||
- {コマンドと要約}
|
||||
## 修正内容
|
||||
- {変更内容}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
rules:
|
||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||
next: ai_review
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: ai_no_fix
|
||||
- condition: 修正を進行できない
|
||||
next: ai_no_fix
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_reviewの指摘が妥当(修正すべき)
|
||||
next: ai_fix
|
||||
- condition: ai_fixの判断が妥当(修正不要)
|
||||
next: reviewers
|
||||
instruction_template: |
|
||||
ai_review(レビュアー)と ai_fix(コーダー)の意見が食い違っています。
|
||||
|
||||
- ai_review は問題を指摘し REJECT しました
|
||||
- ai_fix は確認の上「修正不要」と判断しました
|
||||
|
||||
両者の出力を確認し、どちらの判断が妥当か裁定してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- AIレビュー結果: {report:03-ai-review.md}
|
||||
|
||||
**判断基準:**
|
||||
- ai_review の指摘が具体的で、コード上の実在する問題を指しているか
|
||||
- ai_fix の反論に根拠(ファイル確認結果、テスト結果)があるか
|
||||
- 指摘が非ブロッキング(記録のみ)レベルか、実際に修正が必要か
|
||||
|
||||
# ===========================================
|
||||
# Movement 3: Expert Reviews (Parallel)
|
||||
# ===========================================
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: cqrs-es-review
|
||||
edit: false
|
||||
agent: ../agents/expert-cqrs/cqrs-es-reviewer.md
|
||||
report:
|
||||
name: 04-cqrs-es-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# CQRS+ESレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
| 観点 | 結果 | 備考 |
|
||||
|------|------|------|
|
||||
| Aggregate設計 | ✅ | - |
|
||||
| イベント設計 | ✅ | - |
|
||||
| Command/Query分離 | ✅ | - |
|
||||
| プロジェクション | ✅ | - |
|
||||
| 結果整合性 | ✅ | - |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | スコープ | 場所 | 問題 | 修正案 |
|
||||
|---|---------|------|------|--------|
|
||||
| 1 | スコープ内 | `src/file.ts:42` | 問題の説明 | 修正方法 |
|
||||
|
||||
スコープ: 「スコープ内」(今回修正可能)/ 「スコープ外」(既存問題・非ブロッキング)
|
||||
|
||||
## 既存問題(参考・非ブロッキング)
|
||||
- {既存問題の記録。今回の変更と無関係な問題}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
CQRS(コマンドクエリ責務分離)とEvent Sourcing(イベントソーシング)の観点から
|
||||
変更をレビューしてください。AI特有の問題のレビューは不要です(ai_reviewムーブメントで実施済み)。
|
||||
|
||||
**レビュー観点:**
|
||||
- Aggregate設計の妥当性
|
||||
- イベント設計(粒度、命名、スキーマ)
|
||||
- Command/Queryの分離
|
||||
- プロジェクション設計
|
||||
- 結果整合性の考慮
|
||||
|
||||
**注意**: このプロジェクトがCQRS+ESパターンを使用していない場合は、
|
||||
一般的なドメイン設計の観点からレビューしてください。
|
||||
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
agent: ../agents/expert/frontend-reviewer.md
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# フロントエンドレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
| 観点 | 結果 | 備考 |
|
||||
|------|------|------|
|
||||
| コンポーネント設計 | ✅ | - |
|
||||
| 状態管理 | ✅ | - |
|
||||
| パフォーマンス | ✅ | - |
|
||||
| アクセシビリティ | ✅ | - |
|
||||
| 型安全性 | ✅ | - |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | 場所 | 問題 | 修正案 |
|
||||
|---|------|------|--------|
|
||||
| 1 | `src/file.tsx:42` | 問題の説明 | 修正方法 |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
フロントエンド開発の観点から変更をレビューしてください。
|
||||
|
||||
**レビュー観点:**
|
||||
- コンポーネント設計(責務分離、粒度)
|
||||
- 状態管理(ローカル/グローバルの判断)
|
||||
- パフォーマンス(再レンダリング、メモ化)
|
||||
- アクセシビリティ(キーボード操作、ARIA)
|
||||
- データフェッチパターン
|
||||
- TypeScript型安全性
|
||||
|
||||
**注意**: このプロジェクトがフロントエンドを含まない場合は、
|
||||
問題なしとして次に進んでください。
|
||||
|
||||
- name: security-review
|
||||
edit: false
|
||||
agent: ../agents/expert/security-reviewer.md
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# セキュリティレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## 重大度: None / Low / Medium / High / Critical
|
||||
|
||||
## チェック結果
|
||||
| カテゴリ | 結果 | 備考 |
|
||||
|---------|------|------|
|
||||
| インジェクション | ✅ | - |
|
||||
| 認証・認可 | ✅ | - |
|
||||
| データ保護 | ✅ | - |
|
||||
| 依存関係 | ✅ | - |
|
||||
|
||||
## 脆弱性(REJECTの場合)
|
||||
| # | 重大度 | 種類 | 場所 | 修正案 |
|
||||
|---|--------|------|------|--------|
|
||||
| 1 | High | SQLi | `src/db.ts:42` | パラメータ化クエリを使用 |
|
||||
|
||||
## 警告(ブロッキングではない)
|
||||
- {セキュリティに関する推奨事項}
|
||||
```
|
||||
|
||||
**認知負荷軽減ルール:**
|
||||
- 問題なし → チェック表のみ(10行以内)
|
||||
- 警告 → + 警告1-2行(15行以内)
|
||||
- 脆弱性 → + 表形式(30行以内)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
セキュリティの観点から変更をレビューしてください。以下の脆弱性をチェック:
|
||||
- インジェクション攻撃(SQL, コマンド, XSS)
|
||||
- 認証・認可の不備
|
||||
- データ露出リスク
|
||||
- 暗号化の弱点
|
||||
|
||||
- name: qa-review
|
||||
edit: false
|
||||
agent: ../agents/expert/qa-reviewer.md
|
||||
report:
|
||||
name: 07-qa-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# QAレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
| 観点 | 結果 | 備考 |
|
||||
|------|------|------|
|
||||
| テストカバレッジ | ✅ | - |
|
||||
| テスト品質 | ✅ | - |
|
||||
| エラーハンドリング | ✅ | - |
|
||||
| ドキュメント | ✅ | - |
|
||||
| 保守性 | ✅ | - |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | カテゴリ | 問題 | 修正案 |
|
||||
|---|---------|------|--------|
|
||||
| 1 | テスト | 問題の説明 | 修正方法 |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
品質保証の観点から変更をレビューしてください。
|
||||
|
||||
**レビュー観点:**
|
||||
- テストカバレッジと品質
|
||||
- テスト戦略(単体/統合/E2E)
|
||||
- ドキュメント(コード内・外部)
|
||||
- エラーハンドリング
|
||||
- ログとモニタリング
|
||||
- 保守性
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
instruction_template: |
|
||||
レビュアーからのフィードバックに対応してください。
|
||||
「Original User Request」は参考情報であり、最新の指示ではありません。
|
||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
|
||||
# ===========================================
|
||||
# Movement 4: Supervision
|
||||
# ===========================================
|
||||
- name: supervise
|
||||
edit: false
|
||||
agent: ../agents/expert/supervisor.md
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Previous Reviews Summary
|
||||
このムーブメントに到達したということは、以下のレビューがすべてAPPROVEされています:
|
||||
- AI Review: APPROVED
|
||||
- CQRS+ES Review: APPROVED
|
||||
- Frontend Review: APPROVED
|
||||
- Security Review: APPROVED
|
||||
- QA Review: APPROVED
|
||||
|
||||
テスト実行、ビルド確認、最終承認を行ってください。
|
||||
|
||||
**ピース全体の確認:**
|
||||
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 |
|
||||
| CQRS+ES | ✅ APPROVE |
|
||||
| Frontend | ✅ APPROVE |
|
||||
| Security | ✅ APPROVE |
|
||||
| QA | ✅ APPROVE |
|
||||
| Supervisor | ✅ APPROVE |
|
||||
|
||||
## 確認コマンド
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
rules:
|
||||
- condition: すべての検証が完了し、マージ可能な状態である
|
||||
next: COMPLETE
|
||||
- condition: 問題が検出された
|
||||
next: fix_supervisor
|
||||
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
監督者からの指摘を修正してください。
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: supervise
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
685
resources/global/ja/pieces/expert-hybrid-codex.yaml
Normal file
685
resources/global/ja/pieces/expert-hybrid-codex.yaml
Normal file
@ -0,0 +1,685 @@
|
||||
# Expert Review Piece
|
||||
# アーキテクチャ、フロントエンド、セキュリティ、QAの専門家によるレビューピース
|
||||
#
|
||||
# フロー:
|
||||
# plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE
|
||||
# ↓ ├─ arch-review ↓
|
||||
# ai_fix ├─ frontend-review fix_supervisor
|
||||
# ├─ security-review
|
||||
# └─ qa-review
|
||||
# any("needs_fix") → fix → reviewers
|
||||
#
|
||||
# テンプレート変数:
|
||||
# {iteration} - ピース全体のターン数(全エージェントで実行されたムーブメントの合計)
|
||||
# {max_iterations} - ピースの最大イテレーション数
|
||||
# {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか)
|
||||
# {task} - 元のユーザー要求
|
||||
# {previous_response} - 前のムーブメントの出力
|
||||
# {user_inputs} - ピース中に蓄積されたユーザー入力
|
||||
# {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary")
|
||||
|
||||
name: expert-hybrid-codex
|
||||
description: アーキテクチャ・フロントエンド・セキュリティ・QA専門家レビュー
|
||||
|
||||
max_iterations: 30
|
||||
|
||||
initial_movement: plan
|
||||
|
||||
movements:
|
||||
# ===========================================
|
||||
# Movement 0: Planning
|
||||
# ===========================================
|
||||
- name: plan
|
||||
edit: false
|
||||
agent: ../agents/default/planner.md
|
||||
report:
|
||||
name: 00-plan.md
|
||||
format: |
|
||||
```markdown
|
||||
# タスク計画
|
||||
|
||||
## 元の要求
|
||||
{ユーザーの要求をそのまま記載}
|
||||
|
||||
## 分析結果
|
||||
|
||||
### 目的
|
||||
{達成すべきこと}
|
||||
|
||||
### スコープ
|
||||
{影響範囲}
|
||||
|
||||
### 実装アプローチ
|
||||
{どう進めるか}
|
||||
|
||||
## 確認事項(あれば)
|
||||
- {不明点や確認が必要な点}
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
タスクを分析し、実装方針を立ててください。
|
||||
|
||||
**注意:** Previous Responseがある場合は差し戻しのため、
|
||||
その内容を踏まえて計画を見直してください(replan)。
|
||||
|
||||
**やること:**
|
||||
1. タスクの要件を理解する
|
||||
2. 影響範囲を特定する
|
||||
3. 実装アプローチを決める
|
||||
rules:
|
||||
- condition: タスク分析と計画が完了した
|
||||
next: implement
|
||||
- condition: 要件が不明確で計画を立てられない
|
||||
next: ABORT
|
||||
|
||||
# ===========================================
|
||||
# Movement 1: Implementation
|
||||
# ===========================================
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
planムーブメントで立てた計画に従って実装してください。
|
||||
計画レポート({report:00-plan.md})を参照し、実装を進めてください。
|
||||
Piece 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
|
||||
|
||||
# ===========================================
|
||||
# Movement 2: AI Review
|
||||
# ===========================================
|
||||
- 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
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**これは {movement_iteration} 回目のAI Reviewです。**
|
||||
|
||||
初回は網羅的にレビューし、指摘すべき問題をすべて出し切ってください。
|
||||
2回目以降は、前回REJECTした項目が修正されたかの確認を優先してください。
|
||||
|
||||
AI特有の問題についてコードをレビューしてください:
|
||||
- 仮定の検証
|
||||
- もっともらしいが間違っているパターン
|
||||
- 既存コードベースとの適合性
|
||||
- スコープクリープの検出
|
||||
rules:
|
||||
- condition: AI特有の問題が見つからない
|
||||
next: reviewers
|
||||
- condition: AI特有の問題が検出された
|
||||
next: ai_fix
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
session: refresh
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
**これは {movement_iteration} 回目の AI Review です。**
|
||||
|
||||
2回目以降は、前回の修正が実際には行われていなかったということです。
|
||||
**あなたの「修正済み」という認識が間違っています。**
|
||||
|
||||
**まず認めること:**
|
||||
- 「修正済み」と思っていたファイルは実際には修正されていない
|
||||
- 前回の作業内容の認識が間違っている
|
||||
- ゼロベースで考え直す必要がある
|
||||
|
||||
**必須アクション:**
|
||||
1. 指摘された全ファイルを Read tool で開く(思い込みを捨てて事実確認)
|
||||
2. 問題箇所を grep で検索して実在を確認する
|
||||
3. 確認した問題を Edit tool で修正する
|
||||
4. テストを実行して検証する(`./gradlew :backend:test` 等)
|
||||
5. 「何を確認して、何を修正したか」を具体的に報告する
|
||||
|
||||
**報告フォーマット:**
|
||||
- ❌ 「既に修正されています」
|
||||
- ✅ 「ファイルXのL123を確認した結果、問題Yが存在したため、Zに修正しました」
|
||||
|
||||
**絶対に禁止:**
|
||||
- ファイルを開かずに「修正済み」と報告
|
||||
- 思い込みで判断
|
||||
- AI Reviewer が REJECT した問題の放置
|
||||
|
||||
**修正不要の扱い(必須)**
|
||||
- AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない
|
||||
- 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「修正を進行できない」に対応するタグを出力する
|
||||
- 修正不要の場合は「修正を進行できない」に対応するタグを出力し、理由と確認範囲を明記する
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 確認したファイル
|
||||
- {ファイルパス:行番号}
|
||||
## 実行した検索
|
||||
- {コマンドと要約}
|
||||
## 修正内容
|
||||
- {変更内容}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
rules:
|
||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||
next: ai_review
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: ai_no_fix
|
||||
- condition: 修正を進行できない
|
||||
next: ai_no_fix
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_reviewの指摘が妥当(修正すべき)
|
||||
next: ai_fix
|
||||
- condition: ai_fixの判断が妥当(修正不要)
|
||||
next: reviewers
|
||||
instruction_template: |
|
||||
ai_review(レビュアー)と ai_fix(コーダー)の意見が食い違っています。
|
||||
|
||||
- ai_review は問題を指摘し REJECT しました
|
||||
- ai_fix は確認の上「修正不要」と判断しました
|
||||
|
||||
両者の出力を確認し、どちらの判断が妥当か裁定してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- AIレビュー結果: {report:03-ai-review.md}
|
||||
|
||||
**判断基準:**
|
||||
- ai_review の指摘が具体的で、コード上の実在する問題を指しているか
|
||||
- ai_fix の反論に根拠(ファイル確認結果、テスト結果)があるか
|
||||
- 指摘が非ブロッキング(記録のみ)レベルか、実際に修正が必要か
|
||||
|
||||
# ===========================================
|
||||
# Movement 3: Expert Reviews (Parallel)
|
||||
# ===========================================
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
agent: ../agents/default/architecture-reviewer.md
|
||||
report:
|
||||
name: 04-architect-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# アーキテクチャレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
- [x] 構造・設計
|
||||
- [x] コード品質
|
||||
- [x] 変更スコープ
|
||||
- [x] テストカバレッジ
|
||||
- [x] デッドコード
|
||||
- [x] 呼び出しチェーン検証
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | スコープ | 場所 | 問題 | 修正案 |
|
||||
|---|---------|------|------|--------|
|
||||
| 1 | スコープ内 | `src/file.ts:42` | 問題の説明 | 修正方法 |
|
||||
|
||||
スコープ: 「スコープ内」(今回修正可能)/ 「スコープ外」(既存問題・非ブロッキング)
|
||||
|
||||
## 既存問題(参考・非ブロッキング)
|
||||
- {既存問題の記録。今回の変更と無関係な問題}
|
||||
```
|
||||
|
||||
**認知負荷軽減ルール:**
|
||||
- APPROVE → サマリーのみ(5行以内)
|
||||
- REJECT → 問題点を表形式で(30行以内)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
**アーキテクチャと設計**のレビューに集中してください。AI特有の問題のレビューは不要です(ai_reviewムーブメントで実施済み)。
|
||||
|
||||
**レビュー観点:**
|
||||
- 構造・設計の妥当性
|
||||
- コード品質
|
||||
- 変更スコープの適切性
|
||||
- テストカバレッジ
|
||||
- デッドコード
|
||||
- 呼び出しチェーン検証
|
||||
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
agent: ../agents/expert/frontend-reviewer.md
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# フロントエンドレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
| 観点 | 結果 | 備考 |
|
||||
|------|------|------|
|
||||
| コンポーネント設計 | ✅ | - |
|
||||
| 状態管理 | ✅ | - |
|
||||
| パフォーマンス | ✅ | - |
|
||||
| アクセシビリティ | ✅ | - |
|
||||
| 型安全性 | ✅ | - |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | 場所 | 問題 | 修正案 |
|
||||
|---|------|------|--------|
|
||||
| 1 | `src/file.tsx:42` | 問題の説明 | 修正方法 |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
フロントエンド開発の観点から変更をレビューしてください。
|
||||
|
||||
**レビュー観点:**
|
||||
- コンポーネント設計(責務分離、粒度)
|
||||
- 状態管理(ローカル/グローバルの判断)
|
||||
- パフォーマンス(再レンダリング、メモ化)
|
||||
- アクセシビリティ(キーボード操作、ARIA)
|
||||
- データフェッチパターン
|
||||
- TypeScript型安全性
|
||||
|
||||
**注意**: このプロジェクトがフロントエンドを含まない場合は、
|
||||
問題なしとして次に進んでください。
|
||||
|
||||
- name: security-review
|
||||
edit: false
|
||||
agent: ../agents/expert/security-reviewer.md
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# セキュリティレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## 重大度: None / Low / Medium / High / Critical
|
||||
|
||||
## チェック結果
|
||||
| カテゴリ | 結果 | 備考 |
|
||||
|---------|------|------|
|
||||
| インジェクション | ✅ | - |
|
||||
| 認証・認可 | ✅ | - |
|
||||
| データ保護 | ✅ | - |
|
||||
| 依存関係 | ✅ | - |
|
||||
|
||||
## 脆弱性(REJECTの場合)
|
||||
| # | 重大度 | 種類 | 場所 | 修正案 |
|
||||
|---|--------|------|------|--------|
|
||||
| 1 | High | SQLi | `src/db.ts:42` | パラメータ化クエリを使用 |
|
||||
|
||||
## 警告(ブロッキングではない)
|
||||
- {セキュリティに関する推奨事項}
|
||||
```
|
||||
|
||||
**認知負荷軽減ルール:**
|
||||
- 問題なし → チェック表のみ(10行以内)
|
||||
- 警告 → + 警告1-2行(15行以内)
|
||||
- 脆弱性 → + 表形式(30行以内)
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
セキュリティの観点から変更をレビューしてください。以下の脆弱性をチェック:
|
||||
- インジェクション攻撃(SQL, コマンド, XSS)
|
||||
- 認証・認可の不備
|
||||
- データ露出リスク
|
||||
- 暗号化の弱点
|
||||
|
||||
- name: qa-review
|
||||
edit: false
|
||||
agent: ../agents/expert/qa-reviewer.md
|
||||
report:
|
||||
name: 07-qa-review.md
|
||||
format: |
|
||||
```markdown
|
||||
# QAレビュー
|
||||
|
||||
## 結果: APPROVE / REJECT
|
||||
|
||||
## サマリー
|
||||
{1-2文で結果を要約}
|
||||
|
||||
## 確認した観点
|
||||
| 観点 | 結果 | 備考 |
|
||||
|------|------|------|
|
||||
| テストカバレッジ | ✅ | - |
|
||||
| テスト品質 | ✅ | - |
|
||||
| エラーハンドリング | ✅ | - |
|
||||
| ドキュメント | ✅ | - |
|
||||
| 保守性 | ✅ | - |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | カテゴリ | 問題 | 修正案 |
|
||||
|---|---------|------|--------|
|
||||
| 1 | テスト | 問題の説明 | 修正方法 |
|
||||
```
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction_template: |
|
||||
品質保証の観点から変更をレビューしてください。
|
||||
|
||||
**レビュー観点:**
|
||||
- テストカバレッジと品質
|
||||
- テスト戦略(単体/統合/E2E)
|
||||
- ドキュメント(コード内・外部)
|
||||
- エラーハンドリング
|
||||
- ログとモニタリング
|
||||
- 保守性
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
instruction_template: |
|
||||
レビュアーからのフィードバックに対応してください。
|
||||
「Original User Request」は参考情報であり、最新の指示ではありません。
|
||||
セッションの会話履歴を確認し、レビュアーの指摘事項を修正してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
|
||||
# ===========================================
|
||||
# Movement 4: Supervision
|
||||
# ===========================================
|
||||
- name: supervise
|
||||
edit: false
|
||||
agent: ../agents/expert/supervisor.md
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Previous Reviews Summary
|
||||
このムーブメントに到達したということは、以下のレビューがすべてAPPROVEされています:
|
||||
- AI Review: APPROVED
|
||||
- Architecture Review: APPROVED
|
||||
- Frontend Review: APPROVED
|
||||
- Security Review: APPROVED
|
||||
- QA Review: APPROVED
|
||||
|
||||
テスト実行、ビルド確認、最終承認を行ってください。
|
||||
|
||||
**ピース全体の確認:**
|
||||
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 |
|
||||
| Architecture | ✅ APPROVE |
|
||||
| Frontend | ✅ APPROVE |
|
||||
| Security | ✅ APPROVE |
|
||||
| QA | ✅ APPROVE |
|
||||
| Supervisor | ✅ APPROVE |
|
||||
|
||||
## 確認コマンド
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
```
|
||||
rules:
|
||||
- condition: すべての検証が完了し、マージ可能な状態である
|
||||
next: COMPLETE
|
||||
- condition: 問題が検出された
|
||||
next: fix_supervisor
|
||||
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
監督者からの指摘を修正してください。
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: supervise
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
428
resources/global/ja/pieces/minimal-hybrid-codex.yaml
Normal file
428
resources/global/ja/pieces/minimal-hybrid-codex.yaml
Normal file
@ -0,0 +1,428 @@
|
||||
# Simple TAKT Piece
|
||||
# Implement -> AI Review -> Supervisor Approval
|
||||
# (最もシンプルな構成 - plan, architect review, fix ムーブメントなし)
|
||||
#
|
||||
# Template Variables (auto-injected):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {task} - Original user request (auto-injected)
|
||||
# {previous_response} - Output from the previous movement (auto-injected)
|
||||
# {user_inputs} - Accumulated user inputs during piece (auto-injected)
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
|
||||
name: minimal-hybrid-codex
|
||||
description: Minimal development piece (implement -> parallel review -> fix if needed -> complete)
|
||||
|
||||
max_iterations: 20
|
||||
|
||||
initial_movement: implement
|
||||
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
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: |
|
||||
タスクを実装してください。
|
||||
Piece 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
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
- condition: 判断できない、情報不足
|
||||
instruction_template: |
|
||||
**これは {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
next: reviewers
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: implement
|
||||
- condition: 判断できない、情報不足
|
||||
next: implement
|
||||
instruction_template: |
|
||||
**これは {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: implement
|
||||
instruction_template: |
|
||||
監督者からの指摘を修正してください。
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
43
resources/global/ja/pieces/passthrough-hybrid-codex.yaml
Normal file
43
resources/global/ja/pieces/passthrough-hybrid-codex.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
# Passthrough TAKT Piece
|
||||
# タスクをそのままエージェントに渡す最薄ラッパー。
|
||||
#
|
||||
# フロー:
|
||||
# execute (タスク実行)
|
||||
# ↓
|
||||
# COMPLETE
|
||||
|
||||
name: passthrough-hybrid-codex
|
||||
description: Single-agent thin wrapper. Pass task directly to coder as-is.
|
||||
|
||||
max_iterations: 10
|
||||
|
||||
initial_movement: execute
|
||||
|
||||
movements:
|
||||
- name: execute
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
report:
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: タスク完了
|
||||
next: COMPLETE
|
||||
- condition: 進行できない
|
||||
next: ABORT
|
||||
- condition: ユーザー入力が必要
|
||||
next: execute
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
タスクをこなしてください。
|
||||
42
resources/global/ja/pieces/passthrough.yaml
Normal file
42
resources/global/ja/pieces/passthrough.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
# Passthrough TAKT Piece
|
||||
# タスクをそのままエージェントに渡す最薄ラッパー。
|
||||
#
|
||||
# フロー:
|
||||
# execute (タスク実行)
|
||||
# ↓
|
||||
# COMPLETE
|
||||
|
||||
name: passthrough
|
||||
description: Single-agent thin wrapper. Pass task directly to coder as-is.
|
||||
|
||||
max_iterations: 10
|
||||
|
||||
initial_movement: execute
|
||||
|
||||
movements:
|
||||
- name: execute
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
report:
|
||||
- Summary: summary.md
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: タスク完了
|
||||
next: COMPLETE
|
||||
- condition: 進行できない
|
||||
next: ABORT
|
||||
- condition: ユーザー入力が必要
|
||||
next: execute
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
タスクをこなしてください。
|
||||
428
resources/global/ja/pieces/review-fix-minimal-hybrid-codex.yaml
Normal file
428
resources/global/ja/pieces/review-fix-minimal-hybrid-codex.yaml
Normal file
@ -0,0 +1,428 @@
|
||||
# Review-Fix Minimal TAKT Piece
|
||||
# Review -> Fix (if needed) -> Re-review -> Complete
|
||||
# (レビューから開始、実装ムーブメントなし)
|
||||
#
|
||||
# Template Variables (auto-injected):
|
||||
# {iteration} - Piece-wide turn count (total movements executed across all agents)
|
||||
# {max_iterations} - Maximum iterations allowed for the piece
|
||||
# {movement_iteration} - Per-movement iteration count (how many times THIS movement has been executed)
|
||||
# {task} - Original user request (auto-injected)
|
||||
# {previous_response} - Output from the previous movement (auto-injected)
|
||||
# {user_inputs} - Accumulated user inputs during piece (auto-injected)
|
||||
# {report_dir} - Report directory name (e.g., "20250126-143052-task-summary")
|
||||
|
||||
name: review-fix-minimal-hybrid-codex
|
||||
description: 既存コードのレビューと修正ピース(レビュー開始、実装なし)
|
||||
|
||||
max_iterations: 20
|
||||
|
||||
initial_movement: reviewers
|
||||
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
agent: ../agents/default/coder.md
|
||||
provider: codex
|
||||
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: |
|
||||
タスクを実装してください。
|
||||
Piece 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
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
- condition: 判断できない、情報不足
|
||||
instruction_template: |
|
||||
**これは {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
next: reviewers
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: implement
|
||||
- condition: 判断できない、情報不足
|
||||
next: implement
|
||||
instruction_template: |
|
||||
**これは {movement_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
|
||||
provider: codex
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: implement
|
||||
instruction_template: |
|
||||
監督者からの指摘を修正してください。
|
||||
|
||||
監督者は全体を俯瞰した視点から問題を指摘しています。
|
||||
優先度の高い項目から順に対応してください。
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
## 証拠
|
||||
- {確認したファイル/検索/差分/ログの要点を列挙}
|
||||
199
resources/skill/SKILL.md
Normal file
199
resources/skill/SKILL.md
Normal file
@ -0,0 +1,199 @@
|
||||
---
|
||||
name: takt-engine
|
||||
description: TAKT ピースエンジン。Agent Team を使ったマルチエージェントオーケストレーション。/takt コマンドから使用される。
|
||||
---
|
||||
|
||||
# TAKT Piece Engine
|
||||
|
||||
## あなたの役割: Team Lead
|
||||
|
||||
あなたは **Team Lead(オーケストレーター)** である。
|
||||
ピースYAMLに定義されたワークフロー(状態遷移マシン)に従って Agent Team を率いる。
|
||||
|
||||
### 禁止事項
|
||||
|
||||
- **自分で作業するな** — コーディング、レビュー、設計、テスト等は全てチームメイトに委任する
|
||||
- **タスクを自分で分析して1つの Task にまとめるな** — movement を1つずつ順番に実行せよ
|
||||
- **movement をスキップするな** — 必ず initial_movement から開始し、Rule 評価で決まった次の movement に進む
|
||||
|
||||
### あなたの仕事は4つだけ
|
||||
|
||||
1. ピースYAML を読んでワークフローを理解する
|
||||
2. 各 movement のプロンプトを構築する(references/engine.md 参照)
|
||||
3. **Task tool** でチームメイトを起動して作業を委任する
|
||||
4. チームメイトの出力から Rule 評価を行い、次の movement を決定する
|
||||
|
||||
### ツールの使い分け(重要)
|
||||
|
||||
| やること | 使うツール | 説明 |
|
||||
|---------|-----------|------|
|
||||
| チーム作成 | **Teammate** tool (operation: "spawnTeam") | 最初に1回だけ呼ぶ |
|
||||
| チーム解散 | **Teammate** tool (operation: "cleanup") | 最後に1回だけ呼ぶ |
|
||||
| チームメイト起動 | **Task** tool (team_name 付き) | movement ごとに呼ぶ。**結果は同期的に返る** |
|
||||
|
||||
**Teammate tool でチームメイトを個別に起動することはできない。** チームメイトの起動は必ず Task tool を使う。
|
||||
**Task tool は同期的に結果を返す。** TaskOutput やポーリングは不要。呼べば結果が返ってくる。
|
||||
|
||||
## 手順(この順序で厳密に実行せよ)
|
||||
|
||||
### 手順 1: ピース解決と読み込み
|
||||
|
||||
引数の第1トークンからピースYAMLファイルを特定して Read で読む。
|
||||
|
||||
**第1トークンがない場合(ピース名未指定):**
|
||||
→ ユーザーに「ピース名を指定してください。例: `/takt coding タスク内容`」と伝えて終了する。
|
||||
|
||||
**ピースYAMLの検索順序:**
|
||||
1. `.yaml` / `.yml` で終わる、または `/` を含む → ファイルパスとして直接 Read
|
||||
2. ピース名として検索:
|
||||
- `~/.takt/pieces/{name}.yaml` (ユーザーカスタム、優先)
|
||||
- `~/.claude/skills/takt/pieces/{name}.yaml` (Skill同梱ビルトイン)
|
||||
3. 見つからない場合: 上記2ディレクトリを Glob で列挙し、AskUserQuestion で選択させる
|
||||
|
||||
YAMLから以下を抽出する(→ references/yaml-schema.md 参照):
|
||||
- `name`, `max_iterations`, `initial_movement`, `movements` 配列
|
||||
|
||||
### 手順 2: エージェント .md の事前読み込み
|
||||
|
||||
全 movement(parallel のサブステップ含む)から `agent:` パスを収集する。
|
||||
パスは **ピースYAMLファイルのディレクトリからの相対パス** で解決する。
|
||||
|
||||
例: ピースが `~/.claude/skills/takt/pieces/coding.yaml` にあり、`agent: ../agents/default/coder.md` の場合
|
||||
→ 絶対パスは `~/.claude/skills/takt/agents/default/coder.md`
|
||||
|
||||
重複を除いて Read で全て読み込む。読み込んだ内容はチームメイトへのプロンプトに使う。
|
||||
|
||||
### 手順 3: Agent Team 作成
|
||||
|
||||
**今すぐ** Teammate tool を呼べ:
|
||||
|
||||
```
|
||||
Teammate tool を呼ぶ:
|
||||
operation: "spawnTeam"
|
||||
team_name: "takt"
|
||||
description: "TAKT {piece_name} ワークフロー"
|
||||
```
|
||||
|
||||
### 手順 4: 初期化
|
||||
|
||||
`initial_movement` の名前を確認し、`movements` 配列から該当する movement を取得する。
|
||||
**以下の変数を初期化する:**
|
||||
- `iteration = 1`
|
||||
- `current_movement = initial_movement の movement 定義`
|
||||
- `previous_response = ""`
|
||||
- `permission_mode = コマンドで解析された権限モード("bypassPermissions" または "default")`
|
||||
- `movement_history = []`(遷移履歴。Loop Monitor 用)
|
||||
|
||||
**レポートディレクトリ**: いずれかの movement に `report` フィールドがある場合、`.takt/reports/{YYYYMMDD-HHmmss}-{slug}/` を作成し、パスを `report_dir` 変数に保持する。
|
||||
|
||||
次に **手順 5** に進む。
|
||||
|
||||
### 手順 5: チームメイト起動
|
||||
|
||||
**iteration が max_iterations を超えていたら → 手順 8(ABORT: イテレーション上限)に進む。**
|
||||
|
||||
current_movement のプロンプトを構築する(→ references/engine.md のプロンプト構築を参照)。
|
||||
|
||||
**通常 movement の場合(parallel フィールドなし):**
|
||||
|
||||
Task tool を1つ呼ぶ。**Task tool は同期的に結果を返す。待機やポーリングは不要。**
|
||||
|
||||
```
|
||||
Task tool を呼ぶ:
|
||||
prompt: <構築したプロンプト全文>
|
||||
description: "{movement名} - {piece_name}"
|
||||
subagent_type: "general-purpose"
|
||||
team_name: "takt"
|
||||
name: "{movement の name}"
|
||||
mode: permission_mode
|
||||
```
|
||||
|
||||
Task tool の戻り値がチームメイトの出力。**手順 5a** に進む。
|
||||
|
||||
**parallel movement の場合:**
|
||||
|
||||
**1つのメッセージで**、parallel 配列の各サブステップに対して Task tool を並列に呼ぶ。
|
||||
全ての Task tool が結果を返したら **手順 5a** に進む。
|
||||
|
||||
```
|
||||
// サブステップの数だけ Task tool を同時に呼ぶ(例: 2つの場合)
|
||||
Task tool を呼ぶ(1つ目):
|
||||
prompt: <サブステップ1用プロンプト>
|
||||
description: "{サブステップ1名} - {piece_name}"
|
||||
subagent_type: "general-purpose"
|
||||
team_name: "takt"
|
||||
name: "{サブステップ1の name}"
|
||||
mode: permission_mode
|
||||
|
||||
Task tool を呼ぶ(2つ目):
|
||||
prompt: <サブステップ2用プロンプト>
|
||||
description: "{サブステップ2名} - {piece_name}"
|
||||
subagent_type: "general-purpose"
|
||||
team_name: "takt"
|
||||
name: "{サブステップ2の name}"
|
||||
mode: permission_mode
|
||||
```
|
||||
|
||||
### 手順 5a: レポート抽出と Loop Monitor
|
||||
|
||||
**レポート抽出**(current_movement に `report` フィールドがある場合のみ):
|
||||
チームメイト出力から ```markdown ブロックを抽出し、Write tool で `{report_dir}/{ファイル名}` に保存する。
|
||||
詳細は references/engine.md の「レポートの抽出と保存」を参照。
|
||||
|
||||
**Loop Monitor チェック**(ピースに `loop_monitors` がある場合のみ):
|
||||
`movement_history` に current_movement の名前を追加する。
|
||||
遷移履歴が loop_monitor の `cycle` パターンに `threshold` 回以上マッチした場合、judge チームメイトを起動して遷移先をオーバーライドする。
|
||||
詳細は references/engine.md の「Loop Monitors」を参照。
|
||||
|
||||
### 手順 6: Rule 評価
|
||||
|
||||
Task tool から返ってきたチームメイトの出力から matched_rule を決定する。
|
||||
|
||||
**通常 movement:**
|
||||
1. 出力に `[STEP:N]` タグがあるか探す(複数ある場合は最後のタグを採用)
|
||||
2. タグがあれば → rules[N] を選択(0始まりインデックス)
|
||||
3. タグがなければ → 出力全体を読み、全 condition と比較して最も近いものを選択
|
||||
|
||||
**parallel movement:**
|
||||
1. 各サブステップの Task tool 出力に対して、サブステップの rules で条件マッチを判定
|
||||
2. マッチした condition 文字列を記録
|
||||
3. 親 movement の rules で aggregate 評価:
|
||||
- `all("X")`: 全サブステップが "X" にマッチしたら true
|
||||
- `any("X")`: いずれかのサブステップが "X" にマッチしたら true
|
||||
- `all("X", "Y")`: サブステップ1が "X"、サブステップ2が "Y" にマッチしたら true
|
||||
4. 親 rules を上から順に評価し、最初に true になった rule を選択
|
||||
|
||||
matched_rule が決まったら **手順 7** に進む。
|
||||
どの rule にもマッチしなかったら → **手順 8(ABORT: ルール不一致)** に進む。
|
||||
|
||||
### 手順 7: 次の movement を決定
|
||||
|
||||
matched_rule の `next` を確認する:
|
||||
|
||||
- **`next` が "COMPLETE"** → **手順 8(COMPLETE)** に進む
|
||||
- **`next` が "ABORT"** → **手順 8(ABORT)** に進む
|
||||
- **`next` が movement 名** → 以下を実行して **手順 5 に戻る**:
|
||||
1. `previous_response` = 直前のチームメイト出力
|
||||
2. `current_movement` = `next` で指定された movement を movements 配列から取得
|
||||
3. `iteration` を +1 する
|
||||
4. **手順 5 に戻る**
|
||||
|
||||
### 手順 8: 終了
|
||||
|
||||
1. Teammate tool を呼ぶ:
|
||||
```
|
||||
Teammate tool を呼ぶ:
|
||||
operation: "cleanup"
|
||||
```
|
||||
|
||||
2. ユーザーに結果を報告する:
|
||||
- **COMPLETE**: 最後のチームメイト出力のサマリーを表示
|
||||
- **ABORT**: 失敗理由を表示
|
||||
- **イテレーション上限**: 強制終了を通知
|
||||
|
||||
## 詳細リファレンス
|
||||
|
||||
| ファイル | 内容 |
|
||||
|---------|------|
|
||||
| `references/engine.md` | プロンプト構築、レポート管理、ループ検出の詳細 |
|
||||
| `references/yaml-schema.md` | ピースYAMLの構造定義とフィールド説明 |
|
||||
374
resources/skill/references/engine.md
Normal file
374
resources/skill/references/engine.md
Normal file
@ -0,0 +1,374 @@
|
||||
# TAKT 実行エンジン詳細
|
||||
|
||||
## チームメイトの起動方法
|
||||
|
||||
全ての movement は Task tool でチームメイトを起動して実行する。
|
||||
**あなた(Team Lead)が直接作業することは禁止。**
|
||||
|
||||
### Task tool の呼び出し
|
||||
|
||||
```
|
||||
Task tool:
|
||||
subagent_type: "general-purpose"
|
||||
team_name: "takt"
|
||||
name: "{movement_name}"
|
||||
description: "{movement_name} - {piece_name}"
|
||||
prompt: <プロンプト構築で組み立てた内容>
|
||||
mode: permission_mode
|
||||
```
|
||||
|
||||
### permission_mode
|
||||
|
||||
コマンド引数で解析された `permission_mode` をそのまま Task tool の `mode` に渡す。
|
||||
- `/takt coding yolo タスク` → `permission_mode = "bypassPermissions"`(確認なし)
|
||||
- `/takt coding タスク` → `permission_mode = "default"`(権限確認あり)
|
||||
|
||||
## 通常 Movement の実行
|
||||
|
||||
通常の movement(`parallel` フィールドを持たない)は、Task tool で1つのチームメイトを起動する。
|
||||
|
||||
1. プロンプトを構築する(後述の「プロンプト構築」参照)
|
||||
2. Task tool でチームメイトを起動する
|
||||
3. チームメイトの出力を受け取る
|
||||
4. Rule 評価で次の movement を決定する
|
||||
|
||||
## Parallel Movement の実行
|
||||
|
||||
`parallel` フィールドを持つ movement は、複数のチームメイトを並列起動する。
|
||||
|
||||
### 実行手順
|
||||
|
||||
1. parallel 配列の各サブステップに対して Task tool を準備する
|
||||
2. **全ての Task tool を1つのメッセージで並列に呼び出す**(依存関係がないため)
|
||||
3. 全チームメイトの完了を待つ
|
||||
4. 各サブステップの出力を収集する
|
||||
5. 各サブステップの出力に対して、そのサブステップの `rules` で条件マッチを判定する
|
||||
6. 親 movement の `rules` で aggregate 評価(all()/any())を行う
|
||||
|
||||
### サブステップの条件マッチ判定
|
||||
|
||||
各サブステップの出力テキストに対して、そのサブステップの `rules` の中からマッチする condition を特定する。
|
||||
|
||||
判定方法(通常 movement の Rule 評価と同じ優先順位):
|
||||
1. `[STEP:N]` タグがあればインデックスで照合(最後のタグを採用)
|
||||
2. タグがなければ、出力全体を読んでどの condition に最も近いかを判断する
|
||||
|
||||
マッチした condition 文字列を記録する(次の aggregate 評価で使う)。
|
||||
|
||||
## プロンプト構築
|
||||
|
||||
各チームメイト起動時、以下を結合してプロンプトを組み立てる。
|
||||
|
||||
### 構成要素(上から順に結合)
|
||||
|
||||
```
|
||||
1. エージェントプロンプト(agent: で参照される .md の全内容)
|
||||
2. ---(区切り線)
|
||||
3. 実行コンテキスト情報
|
||||
4. instruction_template の内容(テンプレート変数を展開済み)
|
||||
5. ユーザーのタスク({task} が template に含まれない場合、末尾に自動追加)
|
||||
6. 前の movement の出力(pass_previous_response: true の場合、自動追加)
|
||||
7. レポート出力指示(report フィールドがある場合、自動追加)
|
||||
8. ステータスタグ出力指示(rules がある場合、自動追加)
|
||||
```
|
||||
|
||||
### 実行コンテキスト情報
|
||||
|
||||
```
|
||||
## 実行コンテキスト
|
||||
- ワーキングディレクトリ: {cwd}
|
||||
- ピース: {piece_name}
|
||||
- Movement: {movement_name}
|
||||
- イテレーション: {iteration} / {max_iterations}
|
||||
- Movement イテレーション: {movement_iteration} 回目
|
||||
```
|
||||
|
||||
### テンプレート変数の展開
|
||||
|
||||
`instruction_template` 内の以下のプレースホルダーを置換する:
|
||||
|
||||
| 変数 | 値 |
|
||||
|-----|-----|
|
||||
| `{task}` | ユーザーが入力したタスク内容 |
|
||||
| `{previous_response}` | 前の movement のチームメイト出力 |
|
||||
| `{iteration}` | ピース全体のイテレーション数(1始まり) |
|
||||
| `{max_iterations}` | ピースの max_iterations 値 |
|
||||
| `{movement_iteration}` | この movement が実行された回数(1始まり) |
|
||||
| `{report_dir}` | レポートディレクトリパス |
|
||||
| `{report:ファイル名}` | 指定レポートファイルの内容(Read で取得) |
|
||||
|
||||
### {report:ファイル名} の処理
|
||||
|
||||
`instruction_template` 内に `{report:04-ai-review.md}` のような記法がある場合:
|
||||
1. レポートディレクトリ内に対応するレポートファイルがあれば Read で読む
|
||||
2. 読み込んだ内容をプレースホルダーに展開する
|
||||
3. ファイルが存在しない場合は「(レポート未作成)」に置換する
|
||||
|
||||
### agent フィールドがない場合
|
||||
|
||||
`agent:` が指定されていない movement の場合、エージェントプロンプト部分を省略し、`instruction_template` の内容のみでプロンプトを構成する。
|
||||
|
||||
## レポート出力指示の自動注入
|
||||
|
||||
movement に `report` フィールドがある場合、プロンプト末尾にレポート出力指示を自動追加する。
|
||||
|
||||
### 形式1: name + format
|
||||
|
||||
```yaml
|
||||
report:
|
||||
name: 01-plan.md
|
||||
format: |
|
||||
# タスク計画
|
||||
## 元の要求
|
||||
...
|
||||
```
|
||||
|
||||
→ プロンプトに追加する指示:
|
||||
|
||||
```
|
||||
---
|
||||
## レポート出力(必須)
|
||||
作業完了後、以下のフォーマットに従ってレポートを出力してください。
|
||||
レポートは ```markdown ブロックで囲んで出力してください。
|
||||
|
||||
ファイル名: 01-plan.md
|
||||
フォーマット:
|
||||
# タスク計画
|
||||
## 元の要求
|
||||
...
|
||||
```
|
||||
|
||||
### 形式2: 配列(複数レポート)
|
||||
|
||||
```yaml
|
||||
report:
|
||||
- Summary: summary.md
|
||||
- Scope: 01-scope.md
|
||||
```
|
||||
|
||||
→ プロンプトに追加する指示:
|
||||
|
||||
```
|
||||
---
|
||||
## レポート出力(必須)
|
||||
作業完了後、以下の各レポートを出力してください。
|
||||
各レポートは見出し付きの ```markdown ブロックで囲んで出力してください。
|
||||
|
||||
1. Summary → ファイル名: summary.md
|
||||
2. Scope → ファイル名: 01-scope.md
|
||||
```
|
||||
|
||||
### レポートの抽出と保存
|
||||
|
||||
チームメイトの出力からレポート内容を抽出し、Write tool でレポートディレクトリに保存する。
|
||||
**この作業は Team Lead(あなた)が行う。** チームメイトの出力を受け取った後に実施する。
|
||||
|
||||
**レポートディレクトリ**: `.takt/reports/{timestamp}-{slug}/` に作成する。
|
||||
- `{timestamp}`: `YYYYMMDD-HHmmss` 形式
|
||||
- `{slug}`: タスク内容の先頭30文字をスラグ化
|
||||
|
||||
抽出方法:
|
||||
- 出力内の ```markdown ブロックからレポート内容を取得する
|
||||
- ファイル名の手がかり(見出しやコメント)から対応するレポートを特定する
|
||||
- 特定できない場合は出力全体をレポートとして保存する
|
||||
|
||||
## ステータスタグ出力指示の自動注入
|
||||
|
||||
movement に `rules` がある場合、プロンプト末尾にステータスタグ出力指示を自動追加する。
|
||||
|
||||
### 注入する指示
|
||||
|
||||
```
|
||||
---
|
||||
## ステータス出力(必須)
|
||||
全ての作業とレポート出力が完了した後、最後に以下のいずれかのタグを出力してください。
|
||||
あなたの作業結果に最も合致するものを1つだけ選んでください。
|
||||
|
||||
[STEP:0] = {rules[0].condition}
|
||||
[STEP:1] = {rules[1].condition}
|
||||
[STEP:2] = {rules[2].condition}
|
||||
...
|
||||
```
|
||||
|
||||
### ai() 条件の場合
|
||||
|
||||
condition が `ai("条件テキスト")` 形式の場合でも、同じくタグ出力指示に含める:
|
||||
|
||||
```
|
||||
[STEP:0] = 条件テキスト
|
||||
[STEP:1] = 別の条件テキスト
|
||||
```
|
||||
|
||||
ai() の括弧は除去して condition テキストのみを表示する。
|
||||
|
||||
### サブステップの場合
|
||||
|
||||
parallel のサブステップにも同様にタグ出力指示を注入する。サブステップの rules からタグリストを生成する。
|
||||
|
||||
## Rule 評価
|
||||
|
||||
チームメイトの出力からどの rule にマッチするかを判定する。
|
||||
|
||||
### 通常 Movement の Rule 評価
|
||||
|
||||
判定優先順位(最初にマッチしたものを採用):
|
||||
|
||||
#### 1. タグベース検出(優先)
|
||||
|
||||
チームメイト出力に `[STEP:N]` タグ(N は 0始まりのインデックス)が含まれる場合、そのインデックスに対応する rule を選択する。複数のタグがある場合は **最後のタグ** を採用する。
|
||||
|
||||
例: rules が `["タスク完了", "進行できない"]` で出力に `[STEP:0]` → "タスク完了" を選択
|
||||
|
||||
#### 2. フォールバック(AI 判定)
|
||||
|
||||
タグが出力に含まれない場合、出力テキスト全体を読み、全ての condition と比較して最もマッチするものを選択する。
|
||||
|
||||
### Parallel Movement の Rule 評価(Aggregate)
|
||||
|
||||
親 movement の rules に `all()` / `any()` の aggregate 条件を使用する。
|
||||
|
||||
#### all() の評価
|
||||
|
||||
```yaml
|
||||
- condition: all("approved")
|
||||
next: COMPLETE
|
||||
```
|
||||
|
||||
**引数が1つ**: 全サブステップのマッチ条件が "approved" であれば true。
|
||||
|
||||
```yaml
|
||||
- condition: all("AI特有の問題なし", "すべて問題なし")
|
||||
next: COMPLETE
|
||||
```
|
||||
|
||||
**引数が複数(位置対応)**: サブステップ1が "AI特有の問題なし" にマッチ AND サブステップ2が "すべて問題なし" にマッチ であれば true。
|
||||
|
||||
#### any() の評価
|
||||
|
||||
```yaml
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
```
|
||||
|
||||
いずれかのサブステップのマッチ条件が "needs_fix" であれば true。
|
||||
|
||||
#### Aggregate 評価の順序
|
||||
|
||||
親 rules を上から順に評価し、最初にマッチした rule を採用する。
|
||||
|
||||
### Rule にマッチしない場合
|
||||
|
||||
全ての rule を評価してもマッチしない場合は ABORT する。エラーメッセージとともに、マッチしなかった出力の要約をユーザーに報告する。
|
||||
|
||||
## ループ検出
|
||||
|
||||
### 基本ルール
|
||||
|
||||
- 同じ movement が連続3回以上実行されたら警告を表示する
|
||||
- `max_iterations` に到達したら強制終了(ABORT)する
|
||||
|
||||
### カウンター管理
|
||||
|
||||
以下のカウンターを管理する:
|
||||
|
||||
| カウンター | 説明 | リセットタイミング |
|
||||
|-----------|------|-------------------|
|
||||
| `iteration` | ピース全体の movement 実行回数 | リセットしない |
|
||||
| `movement_iteration[name]` | 各 movement の実行回数 | リセットしない |
|
||||
| `consecutive_count[name]` | 同じ movement の連続実行回数 | 別の movement に遷移したとき |
|
||||
|
||||
## Loop Monitors
|
||||
|
||||
ピースに `loop_monitors` が定義されている場合、特定の movement サイクルを監視する。
|
||||
|
||||
### 動作
|
||||
|
||||
```yaml
|
||||
loop_monitors:
|
||||
- cycle: [ai_review, ai_fix]
|
||||
threshold: 3
|
||||
judge:
|
||||
agent: ../agents/default/supervisor.md
|
||||
instruction_template: |
|
||||
サイクルが {cycle_count} 回繰り返されました...
|
||||
rules:
|
||||
- condition: 健全
|
||||
next: ai_review
|
||||
- condition: 非生産的
|
||||
next: reviewers
|
||||
```
|
||||
|
||||
### 検出ロジック
|
||||
|
||||
1. movement 遷移履歴を記録する(例: `[plan, implement, ai_review, ai_fix, ai_review, ai_fix, ...]`)
|
||||
2. 各 loop_monitor の `cycle` パターンが履歴の末尾に `threshold` 回以上連続で出現するかチェックする
|
||||
3. 閾値に達した場合:
|
||||
a. judge の `agent` を Read で読み込む
|
||||
b. `instruction_template` の `{cycle_count}` を実際のサイクル回数に置換する
|
||||
c. Task tool でチームメイト(judge)を起動する
|
||||
d. judge の出力を judge の `rules` で評価する
|
||||
e. マッチした rule の `next` に遷移する(通常のルール評価をオーバーライドする)
|
||||
|
||||
## レポート管理
|
||||
|
||||
### レポートディレクトリの作成
|
||||
|
||||
ピース実行開始時にレポートディレクトリを作成する:
|
||||
|
||||
```
|
||||
.takt/reports/{YYYYMMDD-HHmmss}-{slug}/
|
||||
```
|
||||
|
||||
このパスを `{report_dir}` 変数として全 movement から参照可能にする。
|
||||
|
||||
### レポートの保存
|
||||
|
||||
チームメイト出力からレポート内容を抽出し、Write tool でレポートディレクトリに保存する。
|
||||
|
||||
抽出手順:
|
||||
1. 出力内の ```markdown ブロックを検索する
|
||||
2. レポートのファイル名やセクション見出しから対応するレポートを特定する
|
||||
3. Write tool で `{report_dir}/{ファイル名}` に保存する
|
||||
|
||||
### レポートの参照
|
||||
|
||||
後続の movement の `instruction_template` 内で `{report:ファイル名}` として参照すると、そのレポートファイルを Read して内容をプレースホルダーに展開する。
|
||||
|
||||
## 状態遷移の全体像
|
||||
|
||||
```
|
||||
[開始]
|
||||
↓
|
||||
ピースYAML読み込み + エージェント .md 読み込み
|
||||
↓
|
||||
Teammate(spawnTeam) でチーム作成
|
||||
↓
|
||||
レポートディレクトリ作成
|
||||
↓
|
||||
initial_movement を取得
|
||||
↓
|
||||
┌─→ Task tool でチームメイト起動
|
||||
│ ├── 通常: 1つの Task tool 呼び出し
|
||||
│ │ prompt = agent.md + context + instruction + task
|
||||
│ │ + previous_response + レポート指示 + タグ指示
|
||||
│ └── parallel: 複数の Task tool を1メッセージで並列呼び出し
|
||||
│ 各サブステップを別々のチームメイトとして起動
|
||||
│ ↓
|
||||
│ チームメイトの出力を受け取る
|
||||
│ ↓
|
||||
│ 出力からレポート抽出 → Write で保存(Team Lead が実施)
|
||||
│ ↓
|
||||
│ Loop Monitor チェック(該当サイクルがあれば judge チームメイト介入)
|
||||
│ ↓
|
||||
│ Rule 評価(Team Lead が実施)
|
||||
│ ├── タグ検出 [STEP:N] → rule 選択
|
||||
│ └── タグなし → AI フォールバック判定
|
||||
│ ├── parallel: サブステップ条件 → aggregate(all/any)
|
||||
│ ↓
|
||||
│ next を決定
|
||||
│ ├── COMPLETE → Teammate(cleanup) → ユーザーに結果報告
|
||||
│ ├── ABORT → Teammate(cleanup) → ユーザーにエラー報告
|
||||
│ └── movement名 → ループ検出チェック → 次の movement
|
||||
│ ↓
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
164
resources/skill/references/yaml-schema.md
Normal file
164
resources/skill/references/yaml-schema.md
Normal file
@ -0,0 +1,164 @@
|
||||
# ピースYAML スキーマリファレンス
|
||||
|
||||
このドキュメントはピースYAMLの構造を定義する。具体的なピース定義は含まない。
|
||||
|
||||
## トップレベルフィールド
|
||||
|
||||
```yaml
|
||||
name: piece-name # ピース名(必須)
|
||||
description: 説明テキスト # ピースの説明(任意)
|
||||
max_iterations: 10 # 最大イテレーション数(必須)
|
||||
initial_movement: plan # 最初に実行する movement 名(必須)
|
||||
movements: [...] # movement 定義の配列(必須)
|
||||
loop_monitors: [...] # ループ監視設定(任意)
|
||||
```
|
||||
|
||||
## Movement 定義
|
||||
|
||||
### 通常 Movement
|
||||
|
||||
```yaml
|
||||
- name: movement-name # movement 名(必須、一意)
|
||||
agent: ../agents/path.md # エージェントプロンプトへの相対パス(任意)
|
||||
agent_name: coder # 表示名(任意)
|
||||
edit: true # ファイル編集可否(必須)
|
||||
permission_mode: edit # 権限モード: edit / readonly / full(任意)
|
||||
session: refresh # セッション管理(任意)
|
||||
pass_previous_response: true # 前の出力を渡すか(デフォルト: true)
|
||||
allowed_tools: [...] # 許可ツール一覧(任意、参考情報)
|
||||
instruction_template: | # ステップ固有の指示テンプレート(任意)
|
||||
指示内容...
|
||||
report: ... # レポート設定(任意)
|
||||
rules: [...] # 遷移ルール(必須)
|
||||
```
|
||||
|
||||
### Parallel Movement
|
||||
|
||||
```yaml
|
||||
- name: reviewers # 親 movement 名(必須)
|
||||
parallel: # 並列サブステップ配列(これがあると parallel movement)
|
||||
- name: sub-step-1 # サブステップ名
|
||||
agent: ../agents/a.md
|
||||
edit: false
|
||||
instruction_template: |
|
||||
...
|
||||
rules: # サブステップの rules(condition のみ、next は無視される)
|
||||
- condition: "approved"
|
||||
- condition: "needs_fix"
|
||||
# report, allowed_tools 等も指定可能
|
||||
|
||||
- name: sub-step-2
|
||||
agent: ../agents/b.md
|
||||
edit: false
|
||||
instruction_template: |
|
||||
...
|
||||
rules:
|
||||
- condition: "passed"
|
||||
- condition: "failed"
|
||||
|
||||
rules: # 親の rules(aggregate 条件で遷移先を決定)
|
||||
- condition: all("approved", "passed")
|
||||
next: complete-step
|
||||
- condition: any("needs_fix", "failed")
|
||||
next: fix-step
|
||||
```
|
||||
|
||||
**重要**: サブステップの `rules` は結果分類のための condition 定義のみ。`next` は無視される(親の rules が遷移先を決定)。
|
||||
|
||||
## Rules 定義
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- condition: 条件テキスト # マッチ条件(必須)
|
||||
next: next-movement # 遷移先 movement 名(必須、サブステップでは任意)
|
||||
requires_user_input: true # ユーザー入力が必要(任意)
|
||||
interactive_only: true # インタラクティブモードのみ(任意)
|
||||
appendix: | # 追加情報(任意)
|
||||
補足テキスト...
|
||||
```
|
||||
|
||||
### Condition 記法
|
||||
|
||||
| 記法 | 説明 | 例 |
|
||||
|-----|------|-----|
|
||||
| 文字列 | AI判定またはタグで照合 | `"タスク完了"` |
|
||||
| `ai("...")` | AI が出力に対して条件を評価 | `ai("コードに問題がある")` |
|
||||
| `all("...")` | 全サブステップがマッチ(parallel 親のみ) | `all("approved")` |
|
||||
| `any("...")` | いずれかがマッチ(parallel 親のみ) | `any("needs_fix")` |
|
||||
| `all("X", "Y")` | 位置対応で全マッチ(parallel 親のみ) | `all("問題なし", "テスト成功")` |
|
||||
|
||||
### 特殊な next 値
|
||||
|
||||
| 値 | 意味 |
|
||||
|---|------|
|
||||
| `COMPLETE` | ピース成功終了 |
|
||||
| `ABORT` | ピース失敗終了 |
|
||||
| movement 名 | 指定された movement に遷移 |
|
||||
|
||||
## Report 定義
|
||||
|
||||
### 形式1: 単一レポート(name + format)
|
||||
|
||||
```yaml
|
||||
report:
|
||||
name: 01-plan.md
|
||||
format: |
|
||||
```markdown
|
||||
# レポートタイトル
|
||||
## セクション
|
||||
{内容}
|
||||
```
|
||||
```
|
||||
|
||||
`format` はエージェントへの出力フォーマット指示。レポート抽出時の参考情報。
|
||||
|
||||
### 形式2: 複数レポート(配列)
|
||||
|
||||
```yaml
|
||||
report:
|
||||
- Summary: summary.md
|
||||
- Scope: 01-scope.md
|
||||
- Decisions: 02-decisions.md
|
||||
```
|
||||
|
||||
各要素のキーがレポート種別名、値がファイル名。
|
||||
|
||||
## テンプレート変数
|
||||
|
||||
`instruction_template` 内で使用可能な変数:
|
||||
|
||||
| 変数 | 説明 |
|
||||
|-----|------|
|
||||
| `{task}` | ユーザーのタスク入力(template に含まれない場合は自動追加) |
|
||||
| `{previous_response}` | 前の movement の出力(pass_previous_response: true 時、自動追加) |
|
||||
| `{iteration}` | ピース全体のイテレーション数 |
|
||||
| `{max_iterations}` | 最大イテレーション数 |
|
||||
| `{movement_iteration}` | この movement の実行回数 |
|
||||
| `{report_dir}` | レポートディレクトリ名 |
|
||||
| `{report:ファイル名}` | 指定レポートファイルの内容を展開 |
|
||||
| `{user_inputs}` | 蓄積されたユーザー入力 |
|
||||
| `{cycle_count}` | loop_monitors 内で使用するサイクル回数 |
|
||||
|
||||
## Loop Monitors(任意)
|
||||
|
||||
```yaml
|
||||
loop_monitors:
|
||||
- cycle: [movement_a, movement_b] # 監視対象の movement サイクル
|
||||
threshold: 3 # 発動閾値(サイクル回数)
|
||||
judge:
|
||||
agent: ../agents/supervisor.md # 判定エージェント
|
||||
instruction_template: | # 判定用指示
|
||||
サイクルが {cycle_count} 回繰り返されました。
|
||||
健全性を判断してください。
|
||||
rules:
|
||||
- condition: 健全(進捗あり)
|
||||
next: movement_a
|
||||
- condition: 非生産的(改善なし)
|
||||
next: alternative_movement
|
||||
```
|
||||
|
||||
特定の movement 間のサイクルが閾値に達した場合、judge エージェントが介入して遷移先を判断する。
|
||||
|
||||
## allowed_tools について
|
||||
|
||||
`allowed_tools` は TAKT 本体のエージェントプロバイダーで使用されるフィールド。Claude Code の Skill として実行する場合、Task tool のエージェントが使用可能なツールは Claude Code の設定に従う。このフィールドは参考情報として扱い、`edit` フィールドの方を権限制御に使用する。
|
||||
37
resources/skill/takt-command.md
Normal file
37
resources/skill/takt-command.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: takt
|
||||
description: TAKT ピースランナー。ピースYAMLワークフローに従ってマルチエージェントを実行する。
|
||||
---
|
||||
|
||||
TAKT ピースランナーを実行する。
|
||||
|
||||
## 引数
|
||||
|
||||
$ARGUMENTS を以下のように解析する:
|
||||
|
||||
```
|
||||
/takt {piece} [permission] {task...}
|
||||
```
|
||||
|
||||
- **第1トークン**: ピース名またはYAMLファイルパス(必須)
|
||||
- **第2トークン**: 権限モード(任意)。以下のキーワードの場合は権限モードとして解釈する:
|
||||
- `yolo` — 全権限付与(mode: "bypassPermissions")
|
||||
- 上記以外 → タスク内容の一部として扱う
|
||||
- **残りのトークン**: タスク内容(省略時は AskUserQuestion でユーザーに入力を求める)
|
||||
- **権限モード省略時のデフォルト**: `"default"`(権限確認あり)
|
||||
|
||||
例:
|
||||
- `/takt coding FizzBuzzを作って` → coding ピース、default 権限
|
||||
- `/takt coding yolo FizzBuzzを作って` → coding ピース、bypassPermissions
|
||||
- `/takt passthrough yolo 全テストを実行` → passthrough ピース、bypassPermissions
|
||||
- `/takt /path/to/custom.yaml 実装して` → カスタムYAML、default 権限
|
||||
|
||||
## 実行手順
|
||||
|
||||
以下のファイルを **Read tool で読み込み**、記載された手順に従って実行する:
|
||||
|
||||
1. `~/.claude/skills/takt/SKILL.md` - エンジン概要とピース解決
|
||||
2. `~/.claude/skills/takt/references/engine.md` - 実行エンジンの詳細ロジック
|
||||
3. `~/.claude/skills/takt/references/yaml-schema.md` - ピースYAML構造リファレンス
|
||||
|
||||
**重要**: これら3ファイルを最初に全て読み込んでから、SKILL.md の「手順」に従って処理を開始する。
|
||||
@ -70,6 +70,7 @@ vi.mock('../infra/github/issue.js', () => ({
|
||||
}
|
||||
return numbers;
|
||||
}),
|
||||
createIssue: vi.fn(),
|
||||
}));
|
||||
|
||||
import { interactiveMode } from '../features/interactive/index.js';
|
||||
@ -77,10 +78,11 @@ import { promptInput, confirm } from '../shared/prompt/index.js';
|
||||
import { summarizeTaskName } from '../infra/task/summarize.js';
|
||||
import { determinePiece } from '../features/tasks/execute/selectAndExecute.js';
|
||||
import { getPieceDescription } from '../infra/config/loaders/pieceResolver.js';
|
||||
import { resolveIssueTask } from '../infra/github/issue.js';
|
||||
import { resolveIssueTask, createIssue } from '../infra/github/issue.js';
|
||||
import { addTask } from '../features/tasks/index.js';
|
||||
|
||||
const mockResolveIssueTask = vi.mocked(resolveIssueTask);
|
||||
const mockCreateIssue = vi.mocked(createIssue);
|
||||
const mockInteractiveMode = vi.mocked(interactiveMode);
|
||||
const mockPromptInput = vi.mocked(promptInput);
|
||||
const mockConfirm = vi.mocked(confirm);
|
||||
@ -97,7 +99,7 @@ function setupFullFlowMocks(overrides?: {
|
||||
|
||||
mockDeterminePiece.mockResolvedValue('default');
|
||||
mockGetPieceDescription.mockReturnValue({ name: 'default', description: '', pieceStructure: '' });
|
||||
mockInteractiveMode.mockResolvedValue({ confirmed: true, task });
|
||||
mockInteractiveMode.mockResolvedValue({ action: 'execute', task });
|
||||
mockSummarizeTaskName.mockResolvedValue(slug);
|
||||
mockConfirm.mockResolvedValue(false);
|
||||
}
|
||||
@ -122,7 +124,7 @@ describe('addTask', () => {
|
||||
it('should cancel when interactive mode is not confirmed', async () => {
|
||||
// Given: user cancels interactive mode
|
||||
mockDeterminePiece.mockResolvedValue('default');
|
||||
mockInteractiveMode.mockResolvedValue({ confirmed: false, task: '' });
|
||||
mockInteractiveMode.mockResolvedValue({ action: 'cancel', task: '' });
|
||||
|
||||
// When
|
||||
await addTask(testDir);
|
||||
@ -341,4 +343,51 @@ describe('addTask', () => {
|
||||
const content = fs.readFileSync(taskFile, 'utf-8');
|
||||
expect(content).toContain('issue: 99');
|
||||
});
|
||||
|
||||
describe('create_issue action', () => {
|
||||
it('should call createIssue when create_issue action is selected', async () => {
|
||||
// Given: interactive mode returns create_issue action
|
||||
const task = 'Create a new feature\nWith detailed description';
|
||||
mockDeterminePiece.mockResolvedValue('default');
|
||||
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task });
|
||||
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||
|
||||
// When
|
||||
await addTask(testDir);
|
||||
|
||||
// Then: createIssue is called via createIssueFromTask
|
||||
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||
title: 'Create a new feature',
|
||||
body: task,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create task file when create_issue action is selected', async () => {
|
||||
// Given: interactive mode returns create_issue action
|
||||
mockDeterminePiece.mockResolvedValue('default');
|
||||
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'Some task' });
|
||||
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||
|
||||
// When
|
||||
await addTask(testDir);
|
||||
|
||||
// Then: no task file created
|
||||
const tasksDir = path.join(testDir, '.takt', 'tasks');
|
||||
const files = fs.existsSync(tasksDir) ? fs.readdirSync(tasksDir) : [];
|
||||
expect(files.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not prompt for worktree settings when create_issue action is selected', async () => {
|
||||
// Given: interactive mode returns create_issue action
|
||||
mockDeterminePiece.mockResolvedValue('default');
|
||||
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'Some task' });
|
||||
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||
|
||||
// When
|
||||
await addTask(testDir);
|
||||
|
||||
// Then: confirm (worktree prompt) is never called
|
||||
expect(mockConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ describe('getBuiltinPiece', () => {
|
||||
});
|
||||
|
||||
it('should return null for non-existent piece names', () => {
|
||||
expect(getBuiltinPiece('passthrough')).toBeNull();
|
||||
expect(getBuiltinPiece('nonexistent-piece')).toBeNull();
|
||||
expect(getBuiltinPiece('unknown')).toBeNull();
|
||||
expect(getBuiltinPiece('')).toBeNull();
|
||||
});
|
||||
|
||||
137
src/__tests__/createIssue.test.ts
Normal file
137
src/__tests__/createIssue.test.ts
Normal file
@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Tests for createIssue function
|
||||
*
|
||||
* createIssue uses `gh issue create` via execFileSync, which is an
|
||||
* integration concern. Tests focus on argument construction and error handling
|
||||
* by mocking child_process.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
execFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { createIssue, checkGhCli } from '../infra/github/issue.js';
|
||||
|
||||
const mockExecFileSync = vi.mocked(execFileSync);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createIssue', () => {
|
||||
it('should return success with URL when gh issue create succeeds', () => {
|
||||
// Given: gh auth and issue creation both succeed
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||
.mockReturnValueOnce('https://github.com/owner/repo/issues/42\n' as unknown as Buffer);
|
||||
|
||||
// When
|
||||
const result = createIssue({ title: 'Test issue', body: 'Test body' });
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.url).toBe('https://github.com/owner/repo/issues/42');
|
||||
});
|
||||
|
||||
it('should pass title and body as arguments', () => {
|
||||
// Given
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||
.mockReturnValueOnce('https://github.com/owner/repo/issues/1\n' as unknown as Buffer);
|
||||
|
||||
// When
|
||||
createIssue({ title: 'My Title', body: 'My Body' });
|
||||
|
||||
// Then: verify the second call (issue create) has correct args
|
||||
const issueCreateCall = mockExecFileSync.mock.calls[1];
|
||||
expect(issueCreateCall?.[0]).toBe('gh');
|
||||
expect(issueCreateCall?.[1]).toEqual([
|
||||
'issue', 'create', '--title', 'My Title', '--body', 'My Body',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should include labels when provided', () => {
|
||||
// Given
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||
.mockReturnValueOnce('https://github.com/owner/repo/issues/1\n' as unknown as Buffer);
|
||||
|
||||
// When
|
||||
createIssue({ title: 'Bug', body: 'Fix it', labels: ['bug', 'priority:high'] });
|
||||
|
||||
// Then
|
||||
const issueCreateCall = mockExecFileSync.mock.calls[1];
|
||||
expect(issueCreateCall?.[1]).toEqual([
|
||||
'issue', 'create', '--title', 'Bug', '--body', 'Fix it',
|
||||
'--label', 'bug,priority:high',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not include --label when labels is empty', () => {
|
||||
// Given
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||
.mockReturnValueOnce('https://github.com/owner/repo/issues/1\n' as unknown as Buffer);
|
||||
|
||||
// When
|
||||
createIssue({ title: 'Title', body: 'Body', labels: [] });
|
||||
|
||||
// Then
|
||||
const issueCreateCall = mockExecFileSync.mock.calls[1];
|
||||
expect(issueCreateCall?.[1]).not.toContain('--label');
|
||||
});
|
||||
|
||||
it('should return error when gh CLI is not authenticated', () => {
|
||||
// Given: auth fails, version succeeds
|
||||
mockExecFileSync
|
||||
.mockImplementationOnce(() => { throw new Error('not authenticated'); })
|
||||
.mockReturnValueOnce(Buffer.from('gh version 2.0.0'));
|
||||
|
||||
// When
|
||||
const result = createIssue({ title: 'Test', body: 'Body' });
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('not authenticated');
|
||||
});
|
||||
|
||||
it('should return error when gh CLI is not installed', () => {
|
||||
// Given: both auth and version fail
|
||||
mockExecFileSync
|
||||
.mockImplementationOnce(() => { throw new Error('command not found'); })
|
||||
.mockImplementationOnce(() => { throw new Error('command not found'); });
|
||||
|
||||
// When
|
||||
const result = createIssue({ title: 'Test', body: 'Body' });
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('not installed');
|
||||
});
|
||||
|
||||
it('should return error when gh issue create fails', () => {
|
||||
// Given: auth succeeds but issue creation fails
|
||||
mockExecFileSync
|
||||
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||
.mockImplementationOnce(() => { throw new Error('repo not found'); });
|
||||
|
||||
// When
|
||||
const result = createIssue({ title: 'Test', body: 'Body' });
|
||||
|
||||
// Then
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('repo not found');
|
||||
});
|
||||
});
|
||||
131
src/__tests__/createIssueFromTask.test.ts
Normal file
131
src/__tests__/createIssueFromTask.test.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Tests for createIssueFromTask function
|
||||
*
|
||||
* Verifies title truncation (100-char boundary), success/failure UI output,
|
||||
* and multi-line task handling (first line → title, full text → body).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('../infra/github/issue.js', () => ({
|
||||
createIssue: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
success: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { createIssue } from '../infra/github/issue.js';
|
||||
import { success, error } from '../shared/ui/index.js';
|
||||
import { createIssueFromTask } from '../features/tasks/index.js';
|
||||
|
||||
const mockCreateIssue = vi.mocked(createIssue);
|
||||
const mockSuccess = vi.mocked(success);
|
||||
const mockError = vi.mocked(error);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createIssueFromTask', () => {
|
||||
describe('title truncation boundary', () => {
|
||||
it('should use title as-is when exactly 99 characters', () => {
|
||||
// Given: 99-character first line
|
||||
const title99 = 'a'.repeat(99);
|
||||
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||
|
||||
// When
|
||||
createIssueFromTask(title99);
|
||||
|
||||
// Then: title passed without truncation
|
||||
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||
title: title99,
|
||||
body: title99,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use title as-is when exactly 100 characters', () => {
|
||||
// Given: 100-character first line
|
||||
const title100 = 'a'.repeat(100);
|
||||
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||
|
||||
// When
|
||||
createIssueFromTask(title100);
|
||||
|
||||
// Then: title passed without truncation
|
||||
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||
title: title100,
|
||||
body: title100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should truncate title to 97 chars + ellipsis when 101 characters', () => {
|
||||
// Given: 101-character first line
|
||||
const title101 = 'a'.repeat(101);
|
||||
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||
|
||||
// When
|
||||
createIssueFromTask(title101);
|
||||
|
||||
// Then: title truncated to 97 chars + "..."
|
||||
const expectedTitle = `${'a'.repeat(97)}...`;
|
||||
expect(expectedTitle).toHaveLength(100);
|
||||
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||
title: expectedTitle,
|
||||
body: title101,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display success message with URL when issue creation succeeds', () => {
|
||||
// Given
|
||||
const url = 'https://github.com/owner/repo/issues/42';
|
||||
mockCreateIssue.mockReturnValue({ success: true, url });
|
||||
|
||||
// When
|
||||
createIssueFromTask('Test task');
|
||||
|
||||
// Then
|
||||
expect(mockSuccess).toHaveBeenCalledWith(`Issue created: ${url}`);
|
||||
expect(mockError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should display error message when issue creation fails', () => {
|
||||
// Given
|
||||
const errorMsg = 'repo not found';
|
||||
mockCreateIssue.mockReturnValue({ success: false, error: errorMsg });
|
||||
|
||||
// When
|
||||
createIssueFromTask('Test task');
|
||||
|
||||
// Then
|
||||
expect(mockError).toHaveBeenCalledWith(`Failed to create issue: ${errorMsg}`);
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use first line as title and full text as body for multi-line task', () => {
|
||||
// Given: multi-line task
|
||||
const task = 'First line title\nSecond line details\nThird line more info';
|
||||
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||
|
||||
// When
|
||||
createIssueFromTask(task);
|
||||
|
||||
// Then: first line → title, full text → body
|
||||
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||
title: 'First line title',
|
||||
body: task,
|
||||
});
|
||||
});
|
||||
});
|
||||
218
src/__tests__/cycle-detector.test.ts
Normal file
218
src/__tests__/cycle-detector.test.ts
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* CycleDetector unit tests
|
||||
*
|
||||
* Tests cycle detection logic for loop_monitors.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CycleDetector } from '../core/piece/engine/cycle-detector.js';
|
||||
import type { LoopMonitorConfig } from '../core/models/index.js';
|
||||
|
||||
function makeMonitor(
|
||||
cycle: string[],
|
||||
threshold: number,
|
||||
rules = [
|
||||
{ condition: 'healthy', next: 'ai_review' },
|
||||
{ condition: 'unproductive', next: 'reviewers' },
|
||||
],
|
||||
): LoopMonitorConfig {
|
||||
return {
|
||||
cycle,
|
||||
threshold,
|
||||
judge: { rules },
|
||||
};
|
||||
}
|
||||
|
||||
describe('CycleDetector', () => {
|
||||
describe('2-step cycle detection', () => {
|
||||
let detector: CycleDetector;
|
||||
const monitor = makeMonitor(['ai_review', 'ai_fix'], 3);
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new CycleDetector([monitor]);
|
||||
});
|
||||
|
||||
it('should not trigger before threshold is reached', () => {
|
||||
// 2 complete cycles (4 movements)
|
||||
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
|
||||
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
|
||||
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
|
||||
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
|
||||
});
|
||||
|
||||
it('should trigger when threshold (3 cycles) is reached', () => {
|
||||
// 3 complete cycles (6 movements)
|
||||
detector.recordAndCheck('ai_review');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
detector.recordAndCheck('ai_review');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
detector.recordAndCheck('ai_review');
|
||||
const result = detector.recordAndCheck('ai_fix');
|
||||
|
||||
expect(result.triggered).toBe(true);
|
||||
expect(result.cycleCount).toBe(3);
|
||||
expect(result.monitor).toBe(monitor);
|
||||
});
|
||||
|
||||
it('should not trigger when cycle is interrupted by another movement', () => {
|
||||
detector.recordAndCheck('ai_review');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
detector.recordAndCheck('ai_review');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
// Interrupt the cycle with a different movement
|
||||
detector.recordAndCheck('plan');
|
||||
detector.recordAndCheck('ai_review');
|
||||
const result = detector.recordAndCheck('ai_fix');
|
||||
|
||||
// Only 1 complete cycle since the interruption
|
||||
expect(result.triggered).toBe(false);
|
||||
});
|
||||
|
||||
it('should not trigger when only the cycle end matches', () => {
|
||||
// History doesn't form a valid cycle pattern
|
||||
detector.recordAndCheck('plan');
|
||||
detector.recordAndCheck('implement');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
|
||||
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
|
||||
});
|
||||
|
||||
it('should reset correctly and not trigger after reset', () => {
|
||||
// Build up 2 cycles
|
||||
detector.recordAndCheck('ai_review');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
detector.recordAndCheck('ai_review');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
|
||||
// Reset
|
||||
detector.reset();
|
||||
|
||||
// Now only 1 cycle after reset
|
||||
detector.recordAndCheck('ai_review');
|
||||
const result = detector.recordAndCheck('ai_fix');
|
||||
expect(result.triggered).toBe(false);
|
||||
expect(result.cycleCount).toBe(0); // Less than threshold
|
||||
});
|
||||
|
||||
it('should trigger exactly at threshold, not before', () => {
|
||||
// Threshold is 3
|
||||
// Cycle 1
|
||||
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
|
||||
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
|
||||
|
||||
// Cycle 2
|
||||
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
|
||||
expect(detector.recordAndCheck('ai_fix').triggered).toBe(false);
|
||||
|
||||
// Cycle 3 (threshold reached)
|
||||
expect(detector.recordAndCheck('ai_review').triggered).toBe(false);
|
||||
expect(detector.recordAndCheck('ai_fix').triggered).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('3-step cycle detection', () => {
|
||||
it('should detect 3-step cycles', () => {
|
||||
const monitor = makeMonitor(['A', 'B', 'C'], 2);
|
||||
const detector = new CycleDetector([monitor]);
|
||||
|
||||
// Cycle 1
|
||||
detector.recordAndCheck('A');
|
||||
detector.recordAndCheck('B');
|
||||
detector.recordAndCheck('C');
|
||||
|
||||
// Cycle 2
|
||||
detector.recordAndCheck('A');
|
||||
detector.recordAndCheck('B');
|
||||
const result = detector.recordAndCheck('C');
|
||||
|
||||
expect(result.triggered).toBe(true);
|
||||
expect(result.cycleCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple monitors', () => {
|
||||
it('should check all monitors and trigger the first matching one', () => {
|
||||
const monitor1 = makeMonitor(['A', 'B'], 3);
|
||||
const monitor2 = makeMonitor(['X', 'Y'], 2);
|
||||
const detector = new CycleDetector([monitor1, monitor2]);
|
||||
|
||||
// 2 cycles of X → Y (threshold for monitor2 is 2)
|
||||
detector.recordAndCheck('X');
|
||||
detector.recordAndCheck('Y');
|
||||
detector.recordAndCheck('X');
|
||||
const result = detector.recordAndCheck('Y');
|
||||
|
||||
expect(result.triggered).toBe(true);
|
||||
expect(result.cycleCount).toBe(2);
|
||||
expect(result.monitor).toBe(monitor2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no monitors', () => {
|
||||
it('should never trigger with empty monitors', () => {
|
||||
const detector = new CycleDetector([]);
|
||||
detector.recordAndCheck('ai_review');
|
||||
detector.recordAndCheck('ai_fix');
|
||||
detector.recordAndCheck('ai_review');
|
||||
const result = detector.recordAndCheck('ai_fix');
|
||||
|
||||
expect(result.triggered).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHistory', () => {
|
||||
it('should return the full movement history', () => {
|
||||
const detector = new CycleDetector([]);
|
||||
detector.recordAndCheck('plan');
|
||||
detector.recordAndCheck('implement');
|
||||
detector.recordAndCheck('ai_review');
|
||||
|
||||
expect(detector.getHistory()).toEqual(['plan', 'implement', 'ai_review']);
|
||||
});
|
||||
|
||||
it('should return empty after reset', () => {
|
||||
const detector = new CycleDetector([]);
|
||||
detector.recordAndCheck('plan');
|
||||
detector.reset();
|
||||
|
||||
expect(detector.getHistory()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('threshold of 1', () => {
|
||||
it('should trigger after first complete cycle', () => {
|
||||
const monitor = makeMonitor(['A', 'B'], 1);
|
||||
const detector = new CycleDetector([monitor]);
|
||||
|
||||
detector.recordAndCheck('A');
|
||||
const result = detector.recordAndCheck('B');
|
||||
|
||||
expect(result.triggered).toBe(true);
|
||||
expect(result.cycleCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beyond threshold', () => {
|
||||
it('should also trigger at threshold + N (consecutive cycles)', () => {
|
||||
const monitor = makeMonitor(['A', 'B'], 2);
|
||||
const detector = new CycleDetector([monitor]);
|
||||
|
||||
// 2 cycles → threshold met
|
||||
detector.recordAndCheck('A');
|
||||
detector.recordAndCheck('B');
|
||||
detector.recordAndCheck('A');
|
||||
const result1 = detector.recordAndCheck('B');
|
||||
expect(result1.triggered).toBe(true);
|
||||
expect(result1.cycleCount).toBe(2);
|
||||
|
||||
// After reset + 3 more cycles → triggers at 2 again
|
||||
detector.reset();
|
||||
detector.recordAndCheck('A');
|
||||
detector.recordAndCheck('B');
|
||||
detector.recordAndCheck('A');
|
||||
const result2 = detector.recordAndCheck('B');
|
||||
expect(result2.triggered).toBe(true);
|
||||
expect(result2.cycleCount).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
315
src/__tests__/engine-loop-monitors.test.ts
Normal file
315
src/__tests__/engine-loop-monitors.test.ts
Normal file
@ -0,0 +1,315 @@
|
||||
/**
|
||||
* PieceEngine integration tests: loop_monitors (cycle detection + judge)
|
||||
*
|
||||
* Covers:
|
||||
* - Loop monitor triggers judge when cycle threshold reached
|
||||
* - Judge decision overrides normal next movement
|
||||
* - Cycle detector resets after judge intervention
|
||||
* - No trigger when threshold not reached
|
||||
* - Validation of loop_monitors config
|
||||
* - movement:cycle_detected event emission
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { existsSync, rmSync } from 'node:fs';
|
||||
import type { PieceConfig, PieceMovement, LoopMonitorConfig } from '../core/models/index.js';
|
||||
|
||||
// --- Mock setup (must be before imports that use these modules) ---
|
||||
|
||||
vi.mock('../agents/runner.js', () => ({
|
||||
runAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../core/piece/evaluation/index.js', () => ({
|
||||
detectMatchedRule: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../core/piece/phase-runner.js', () => ({
|
||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||
}));
|
||||
|
||||
// --- Imports (after mocks) ---
|
||||
|
||||
import { PieceEngine } from '../core/piece/index.js';
|
||||
import { runAgent } from '../agents/runner.js';
|
||||
import {
|
||||
makeResponse,
|
||||
makeMovement,
|
||||
makeRule,
|
||||
mockRunAgentSequence,
|
||||
mockDetectMatchedRuleSequence,
|
||||
createTestTmpDir,
|
||||
applyDefaultMocks,
|
||||
cleanupPieceEngine,
|
||||
} from './engine-test-helpers.js';
|
||||
|
||||
/**
|
||||
* Build a piece config with ai_review ↔ ai_fix loop and loop_monitors.
|
||||
*/
|
||||
function buildConfigWithLoopMonitor(
|
||||
threshold = 3,
|
||||
monitorOverrides: Partial<LoopMonitorConfig> = {},
|
||||
): PieceConfig {
|
||||
return {
|
||||
name: 'test-loop-monitor',
|
||||
description: 'Test piece with loop monitors',
|
||||
maxIterations: 30,
|
||||
initialMovement: 'implement',
|
||||
loopMonitors: [
|
||||
{
|
||||
cycle: ['ai_review', 'ai_fix'],
|
||||
threshold,
|
||||
judge: {
|
||||
rules: [
|
||||
{ condition: 'Healthy', next: 'ai_review' },
|
||||
{ condition: 'Unproductive', next: 'reviewers' },
|
||||
],
|
||||
},
|
||||
...monitorOverrides,
|
||||
},
|
||||
],
|
||||
movements: [
|
||||
makeMovement('implement', {
|
||||
rules: [makeRule('done', 'ai_review')],
|
||||
}),
|
||||
makeMovement('ai_review', {
|
||||
rules: [
|
||||
makeRule('No issues', 'reviewers'),
|
||||
makeRule('Issues found', 'ai_fix'),
|
||||
],
|
||||
}),
|
||||
makeMovement('ai_fix', {
|
||||
rules: [
|
||||
makeRule('Fixed', 'ai_review'),
|
||||
makeRule('No fix needed', 'reviewers'),
|
||||
],
|
||||
}),
|
||||
makeMovement('reviewers', {
|
||||
rules: [makeRule('All approved', 'COMPLETE')],
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe('PieceEngine Integration: Loop Monitors', () => {
|
||||
let tmpDir: string;
|
||||
let engine: PieceEngine | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
applyDefaultMocks();
|
||||
tmpDir = createTestTmpDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (engine) {
|
||||
cleanupPieceEngine(engine);
|
||||
engine = null;
|
||||
}
|
||||
if (existsSync(tmpDir)) {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// 1. Cycle triggers judge → unproductive → skip to reviewers
|
||||
// =====================================================
|
||||
describe('Judge triggered on cycle threshold', () => {
|
||||
it('should run judge and redirect to reviewers when cycle is unproductive', async () => {
|
||||
const config = buildConfigWithLoopMonitor(2);
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
// implement
|
||||
makeResponse({ agent: 'implement', content: 'Implementation done' }),
|
||||
// ai_review → issues found
|
||||
makeResponse({ agent: 'ai_review', content: 'Issues found: X' }),
|
||||
// ai_fix → fixed → ai_review
|
||||
makeResponse({ agent: 'ai_fix', content: 'Fixed X' }),
|
||||
// ai_review → issues found again
|
||||
makeResponse({ agent: 'ai_review', content: 'Issues found: Y' }),
|
||||
// ai_fix → fixed → cycle threshold reached (2 cycles complete)
|
||||
makeResponse({ agent: 'ai_fix', content: 'Fixed Y' }),
|
||||
// Judge runs (synthetic movement)
|
||||
makeResponse({ agent: 'supervisor', content: 'Unproductive loop detected' }),
|
||||
// reviewers (after judge redirects here)
|
||||
makeResponse({ agent: 'reviewers', content: 'All approved' }),
|
||||
]);
|
||||
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' }, // implement → ai_review
|
||||
{ index: 1, method: 'phase1_tag' }, // ai_review → ai_fix (issues found)
|
||||
{ index: 0, method: 'phase1_tag' }, // ai_fix → ai_review (fixed)
|
||||
{ index: 1, method: 'phase1_tag' }, // ai_review → ai_fix (issues found again)
|
||||
{ index: 0, method: 'phase1_tag' }, // ai_fix → ai_review (fixed) — but cycle detected!
|
||||
// Judge rule match: Unproductive (index 1) → reviewers
|
||||
{ index: 1, method: 'ai_judge_fallback' },
|
||||
// reviewers → COMPLETE
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
]);
|
||||
|
||||
const cycleDetectedFn = vi.fn();
|
||||
engine.on('movement:cycle_detected', cycleDetectedFn);
|
||||
|
||||
const state = await engine.run();
|
||||
|
||||
expect(state.status).toBe('completed');
|
||||
expect(cycleDetectedFn).toHaveBeenCalledOnce();
|
||||
expect(cycleDetectedFn.mock.calls[0][1]).toBe(2); // cycleCount
|
||||
// 7 iterations: implement + ai_review + ai_fix + ai_review + ai_fix + judge + reviewers
|
||||
expect(state.iteration).toBe(7);
|
||||
});
|
||||
|
||||
it('should run judge and continue loop when cycle is healthy', async () => {
|
||||
const config = buildConfigWithLoopMonitor(2);
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
// implement
|
||||
makeResponse({ agent: 'implement', content: 'Implementation done' }),
|
||||
// Cycle 1: ai_review → ai_fix
|
||||
makeResponse({ agent: 'ai_review', content: 'Issues found: A' }),
|
||||
makeResponse({ agent: 'ai_fix', content: 'Fixed A' }),
|
||||
// Cycle 2: ai_review → ai_fix (threshold reached)
|
||||
makeResponse({ agent: 'ai_review', content: 'Issues found: B' }),
|
||||
makeResponse({ agent: 'ai_fix', content: 'Fixed B' }),
|
||||
// Judge says healthy → continue to ai_review
|
||||
makeResponse({ agent: 'supervisor', content: 'Loop is healthy, making progress' }),
|
||||
// ai_review → no issues
|
||||
makeResponse({ agent: 'ai_review', content: 'No issues remaining' }),
|
||||
// reviewers → COMPLETE
|
||||
makeResponse({ agent: 'reviewers', content: 'All approved' }),
|
||||
]);
|
||||
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' }, // implement → ai_review
|
||||
{ index: 1, method: 'phase1_tag' }, // ai_review → ai_fix
|
||||
{ index: 0, method: 'phase1_tag' }, // ai_fix → ai_review
|
||||
{ index: 1, method: 'phase1_tag' }, // ai_review → ai_fix
|
||||
{ index: 0, method: 'phase1_tag' }, // ai_fix → ai_review — cycle detected!
|
||||
// Judge: Healthy (index 0) → ai_review
|
||||
{ index: 0, method: 'ai_judge_fallback' },
|
||||
// ai_review → reviewers (no issues)
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
// reviewers → COMPLETE
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
]);
|
||||
|
||||
const state = await engine.run();
|
||||
|
||||
expect(state.status).toBe('completed');
|
||||
// 8 iterations: impl + ai_review*3 + ai_fix*2 + judge + reviewers
|
||||
expect(state.iteration).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// 2. No trigger when threshold not reached
|
||||
// =====================================================
|
||||
describe('No trigger before threshold', () => {
|
||||
it('should not trigger judge when fewer cycles than threshold', async () => {
|
||||
const config = buildConfigWithLoopMonitor(3); // threshold = 3, only do 1 cycle
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'implement', content: 'Implementation done' }),
|
||||
makeResponse({ agent: 'ai_review', content: 'Issues found' }),
|
||||
makeResponse({ agent: 'ai_fix', content: 'Fixed' }),
|
||||
makeResponse({ agent: 'ai_review', content: 'No issues' }),
|
||||
makeResponse({ agent: 'reviewers', content: 'All approved' }),
|
||||
]);
|
||||
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' }, // implement → ai_review
|
||||
{ index: 1, method: 'phase1_tag' }, // ai_review → ai_fix
|
||||
{ index: 0, method: 'phase1_tag' }, // ai_fix → ai_review
|
||||
{ index: 0, method: 'phase1_tag' }, // ai_review → reviewers (no issues)
|
||||
{ index: 0, method: 'phase1_tag' }, // reviewers → COMPLETE
|
||||
]);
|
||||
|
||||
const cycleDetectedFn = vi.fn();
|
||||
engine.on('movement:cycle_detected', cycleDetectedFn);
|
||||
|
||||
const state = await engine.run();
|
||||
|
||||
expect(state.status).toBe('completed');
|
||||
expect(cycleDetectedFn).not.toHaveBeenCalled();
|
||||
// No judge was called, so only 5 iterations
|
||||
expect(state.iteration).toBe(5);
|
||||
expect(vi.mocked(runAgent)).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// 3. Validation errors
|
||||
// =====================================================
|
||||
describe('Config validation', () => {
|
||||
it('should throw when loop_monitor cycle references nonexistent movement', () => {
|
||||
const config = buildConfigWithLoopMonitor(3);
|
||||
config.loopMonitors = [
|
||||
{
|
||||
cycle: ['ai_review', 'nonexistent'],
|
||||
threshold: 3,
|
||||
judge: {
|
||||
rules: [{ condition: 'test', next: 'ai_review' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => {
|
||||
new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
}).toThrow('nonexistent');
|
||||
});
|
||||
|
||||
it('should throw when loop_monitor judge rule references nonexistent movement', () => {
|
||||
const config = buildConfigWithLoopMonitor(3);
|
||||
config.loopMonitors = [
|
||||
{
|
||||
cycle: ['ai_review', 'ai_fix'],
|
||||
threshold: 3,
|
||||
judge: {
|
||||
rules: [{ condition: 'test', next: 'nonexistent_target' }],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => {
|
||||
new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
}).toThrow('nonexistent_target');
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================
|
||||
// 4. No loop monitors configured
|
||||
// =====================================================
|
||||
describe('No loop monitors', () => {
|
||||
it('should work normally without loop_monitors configured', async () => {
|
||||
const config = buildConfigWithLoopMonitor(3);
|
||||
config.loopMonitors = undefined;
|
||||
engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir });
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ agent: 'implement', content: 'Done' }),
|
||||
makeResponse({ agent: 'ai_review', content: 'No issues' }),
|
||||
makeResponse({ agent: 'reviewers', content: 'All approved' }),
|
||||
]);
|
||||
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
]);
|
||||
|
||||
const state = await engine.run();
|
||||
expect(state.status).toBe('completed');
|
||||
expect(state.iteration).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -96,7 +96,8 @@ describe('label integrity', () => {
|
||||
expect(ui).toHaveProperty('summarizeFailed');
|
||||
expect(ui).toHaveProperty('continuePrompt');
|
||||
expect(ui).toHaveProperty('proposed');
|
||||
expect(ui).toHaveProperty('confirm');
|
||||
expect(ui).toHaveProperty('actionPrompt');
|
||||
expect(ui).toHaveProperty('actions');
|
||||
expect(ui).toHaveProperty('cancelled');
|
||||
});
|
||||
|
||||
|
||||
@ -123,11 +123,12 @@ function setupMockProvider(responses: string[]): void {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSelectOption.mockResolvedValue('yes');
|
||||
// selectPostSummaryAction uses selectOption with action values
|
||||
mockSelectOption.mockResolvedValue('execute');
|
||||
});
|
||||
|
||||
describe('interactiveMode', () => {
|
||||
it('should return confirmed=false when user types /cancel', async () => {
|
||||
it('should return action=cancel when user types /cancel', async () => {
|
||||
// Given
|
||||
setupInputSequence(['/cancel']);
|
||||
setupMockProvider([]);
|
||||
@ -136,11 +137,11 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.confirmed).toBe(false);
|
||||
expect(result.action).toBe('cancel');
|
||||
expect(result.task).toBe('');
|
||||
});
|
||||
|
||||
it('should return confirmed=false on EOF (Ctrl+D)', async () => {
|
||||
it('should return action=cancel on EOF (Ctrl+D)', async () => {
|
||||
// Given
|
||||
setupInputSequence([null]);
|
||||
setupMockProvider([]);
|
||||
@ -149,7 +150,7 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.confirmed).toBe(false);
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should call provider with allowed tools for codebase exploration', async () => {
|
||||
@ -172,7 +173,7 @@ describe('interactiveMode', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return confirmed=true with task on /go after conversation', async () => {
|
||||
it('should return action=execute with task on /go after conversation', async () => {
|
||||
// Given
|
||||
setupInputSequence(['add auth feature', '/go']);
|
||||
setupMockProvider(['What kind of authentication?', 'Implement auth feature with chosen method.']);
|
||||
@ -181,7 +182,7 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Implement auth feature with chosen method.');
|
||||
});
|
||||
|
||||
@ -193,8 +194,8 @@ describe('interactiveMode', () => {
|
||||
// When
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then: should not confirm (fell through to /cancel)
|
||||
expect(result.confirmed).toBe(false);
|
||||
// Then: should cancel (fell through to /cancel)
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should skip empty input', async () => {
|
||||
@ -206,7 +207,7 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider.call).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@ -220,7 +221,7 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then: task should be a summary and prompt should include full history
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Summarized task.');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
|
||||
const summaryPrompt = mockProvider.call.mock.calls[2]?.[1] as string;
|
||||
@ -259,7 +260,7 @@ describe('interactiveMode', () => {
|
||||
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a');
|
||||
|
||||
// /go should work because initialInput already started conversation
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Clarify task for "a".');
|
||||
});
|
||||
|
||||
@ -278,12 +279,12 @@ describe('interactiveMode', () => {
|
||||
expect(mockProvider.call.mock.calls[1]?.[1]).toBe('fix the login page');
|
||||
|
||||
// Task still contains all history for downstream use
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Fix login page with clarified scope.');
|
||||
});
|
||||
|
||||
describe('/play command', () => {
|
||||
it('should return confirmed=true with task on /play command', async () => {
|
||||
it('should return action=execute with task on /play command', async () => {
|
||||
// Given
|
||||
setupInputSequence(['/play implement login feature']);
|
||||
setupMockProvider([]);
|
||||
@ -292,7 +293,7 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('implement login feature');
|
||||
});
|
||||
|
||||
@ -304,8 +305,8 @@ describe('interactiveMode', () => {
|
||||
// When
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then: should not confirm (fell through to /cancel)
|
||||
expect(result.confirmed).toBe(false);
|
||||
// Then: should cancel (fell through to /cancel)
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should handle /play with leading/trailing spaces', async () => {
|
||||
@ -317,7 +318,7 @@ describe('interactiveMode', () => {
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('test task');
|
||||
});
|
||||
|
||||
@ -332,8 +333,64 @@ describe('interactiveMode', () => {
|
||||
// Then: provider should NOT have been called (no summary needed)
|
||||
const mockProvider = mockGetProvider.mock.results[0]?.value as { call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider.call).not.toHaveBeenCalled();
|
||||
expect(result.confirmed).toBe(true);
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('quick task');
|
||||
});
|
||||
});
|
||||
|
||||
describe('action selection after /go', () => {
|
||||
it('should return action=create_issue when user selects create issue', async () => {
|
||||
// Given
|
||||
setupInputSequence(['describe task', '/go']);
|
||||
setupMockProvider(['response', 'Summarized task.']);
|
||||
mockSelectOption.mockResolvedValue('create_issue');
|
||||
|
||||
// When
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('create_issue');
|
||||
expect(result.task).toBe('Summarized task.');
|
||||
});
|
||||
|
||||
it('should return action=save_task when user selects save task', async () => {
|
||||
// Given
|
||||
setupInputSequence(['describe task', '/go']);
|
||||
setupMockProvider(['response', 'Summarized task.']);
|
||||
mockSelectOption.mockResolvedValue('save_task');
|
||||
|
||||
// When
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('save_task');
|
||||
expect(result.task).toBe('Summarized task.');
|
||||
});
|
||||
|
||||
it('should continue editing when user selects continue', async () => {
|
||||
// Given: user selects 'continue' first, then cancels
|
||||
setupInputSequence(['describe task', '/go', '/cancel']);
|
||||
setupMockProvider(['response', 'Summarized task.']);
|
||||
mockSelectOption.mockResolvedValueOnce('continue');
|
||||
|
||||
// When
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then: should fall through to /cancel
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should continue editing when user presses ESC (null)', async () => {
|
||||
// Given: selectOption returns null (ESC), then user cancels
|
||||
setupInputSequence(['describe task', '/go', '/cancel']);
|
||||
setupMockProvider(['response', 'Summarized task.']);
|
||||
mockSelectOption.mockResolvedValueOnce(null);
|
||||
|
||||
// When
|
||||
const result = await interactiveMode('/project');
|
||||
|
||||
// Then: should fall through to /cancel
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
181
src/__tests__/listNonInteractive.test.ts
Normal file
181
src/__tests__/listNonInteractive.test.ts
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Tests for listNonInteractive — non-interactive list output and branch actions.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { listTasks } from '../features/tasks/list/index.js';
|
||||
|
||||
describe('listTasks non-interactive text output', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-test-ni-'));
|
||||
execFileSync('git', ['init', '--initial-branch', 'main'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
execFileSync('git', ['commit', '--allow-empty', '-m', 'init'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should output pending tasks in text format', async () => {
|
||||
// Given
|
||||
const tasksDir = path.join(tmpDir, '.takt', 'tasks');
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(tasksDir, 'my-task.md'), 'Fix the login bug');
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When
|
||||
await listTasks(tmpDir, undefined, { enabled: true });
|
||||
|
||||
// Then
|
||||
const calls = logSpy.mock.calls.map((c) => c[0] as string);
|
||||
expect(calls).toContainEqual(expect.stringContaining('[pending] my-task'));
|
||||
expect(calls).toContainEqual(expect.stringContaining('Fix the login bug'));
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should output failed tasks in text format', async () => {
|
||||
// Given
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed', '2025-01-15T12-34-56_failed-task');
|
||||
fs.mkdirSync(failedDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(failedDir, 'failed-task.md'), 'This failed');
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When
|
||||
await listTasks(tmpDir, undefined, { enabled: true });
|
||||
|
||||
// Then
|
||||
const calls = logSpy.mock.calls.map((c) => c[0] as string);
|
||||
expect(calls).toContainEqual(expect.stringContaining('[failed] failed-task'));
|
||||
expect(calls).toContainEqual(expect.stringContaining('This failed'));
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should output both pending and failed tasks in text format', async () => {
|
||||
// Given
|
||||
const tasksDir = path.join(tmpDir, '.takt', 'tasks');
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(tasksDir, 'pending-one.md'), 'Pending task');
|
||||
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed', '2025-01-15T12-34-56_failed-one');
|
||||
fs.mkdirSync(failedDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(failedDir, 'failed-one.md'), 'Failed task');
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When
|
||||
await listTasks(tmpDir, undefined, { enabled: true });
|
||||
|
||||
// Then
|
||||
const calls = logSpy.mock.calls.map((c) => c[0] as string);
|
||||
expect(calls).toContainEqual(expect.stringContaining('[pending] pending-one'));
|
||||
expect(calls).toContainEqual(expect.stringContaining('[failed] failed-one'));
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should show info message when no tasks exist', async () => {
|
||||
// Given: no tasks, no branches
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When
|
||||
await listTasks(tmpDir, undefined, { enabled: true });
|
||||
|
||||
// Then
|
||||
const calls = logSpy.mock.calls.map((c) => c[0] as string);
|
||||
expect(calls.some((c) => c.includes('No tasks to list'))).toBe(true);
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listTasks non-interactive action errors', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-test-ni-err-'));
|
||||
execFileSync('git', ['init', '--initial-branch', 'main'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
execFileSync('git', ['commit', '--allow-empty', '-m', 'init'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
// Create a pending task so the "no tasks" early return is not triggered
|
||||
const tasksDir = path.join(tmpDir, '.takt', 'tasks');
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(tasksDir, 'dummy.md'), 'dummy');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should exit with code 1 when --action specified without --branch', async () => {
|
||||
// Given
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); });
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When / Then
|
||||
await expect(
|
||||
listTasks(tmpDir, undefined, { enabled: true, action: 'diff' }),
|
||||
).rejects.toThrow('process.exit');
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should exit with code 1 for invalid action', async () => {
|
||||
// Given
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); });
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When / Then
|
||||
await expect(
|
||||
listTasks(tmpDir, undefined, { enabled: true, action: 'invalid', branch: 'some-branch' }),
|
||||
).rejects.toThrow('process.exit');
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should exit with code 1 when branch not found', async () => {
|
||||
// Given
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); });
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When / Then
|
||||
await expect(
|
||||
listTasks(tmpDir, undefined, { enabled: true, action: 'diff', branch: 'takt/nonexistent' }),
|
||||
).rejects.toThrow('process.exit');
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should exit with code 1 for delete without --yes', async () => {
|
||||
// Given: create a branch so it's found
|
||||
execFileSync('git', ['checkout', '-b', 'takt/20250115-test-branch'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
execFileSync('git', ['checkout', 'main'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); });
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When / Then
|
||||
await expect(
|
||||
listTasks(tmpDir, undefined, {
|
||||
enabled: true,
|
||||
action: 'delete',
|
||||
branch: 'takt/20250115-test-branch',
|
||||
}),
|
||||
).rejects.toThrow('process.exit');
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@ -2,14 +2,21 @@
|
||||
* Tests for list-tasks command
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
parseTaktBranches,
|
||||
extractTaskSlug,
|
||||
buildListItems,
|
||||
type BranchInfo,
|
||||
} from '../infra/task/branchList.js';
|
||||
import { TaskRunner } from '../infra/task/runner.js';
|
||||
import type { TaskListItem } from '../infra/task/types.js';
|
||||
import { isBranchMerged, showFullDiff, type ListAction } from '../features/tasks/index.js';
|
||||
import { listTasks } from '../features/tasks/list/index.js';
|
||||
|
||||
describe('parseTaktBranches', () => {
|
||||
it('should parse takt/ branches from git branch output', () => {
|
||||
@ -178,3 +185,206 @@ describe('isBranchMerged', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskRunner.listFailedTasks', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should return empty array for empty failed directory', () => {
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listFailedTasks();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse failed task directories correctly', () => {
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed');
|
||||
const taskDir = path.join(failedDir, '2025-01-15T12-34-56_my-task');
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(taskDir, 'my-task.md'), 'Fix the login bug\nMore details here');
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listFailedTasks();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
kind: 'failed',
|
||||
name: 'my-task',
|
||||
createdAt: '2025-01-15T12:34:56',
|
||||
filePath: taskDir,
|
||||
content: 'Fix the login bug',
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip malformed directory names', () => {
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed');
|
||||
// No underscore → malformed, should be skipped
|
||||
fs.mkdirSync(path.join(failedDir, 'malformed-name'), { recursive: true });
|
||||
// Valid one
|
||||
const validDir = path.join(failedDir, '2025-01-15T12-34-56_valid-task');
|
||||
fs.mkdirSync(validDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(validDir, 'valid-task.md'), 'Content');
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listFailedTasks();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.name).toBe('valid-task');
|
||||
});
|
||||
|
||||
it('should extract task content from task file in directory', () => {
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed');
|
||||
const taskDir = path.join(failedDir, '2025-02-01T00-00-00_content-test');
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
// report.md and log.json should be skipped; the actual task file should be read
|
||||
fs.writeFileSync(path.join(taskDir, 'report.md'), 'Report content');
|
||||
fs.writeFileSync(path.join(taskDir, 'log.json'), '{}');
|
||||
fs.writeFileSync(path.join(taskDir, 'content-test.yaml'), 'task: Do something important');
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listFailedTasks();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.content).toBe('task: Do something important');
|
||||
});
|
||||
|
||||
it('should return empty content when no task file exists', () => {
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed');
|
||||
const taskDir = path.join(failedDir, '2025-02-01T00-00-00_no-task-file');
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
// Only report.md and log.json, no actual task file
|
||||
fs.writeFileSync(path.join(taskDir, 'report.md'), 'Report content');
|
||||
fs.writeFileSync(path.join(taskDir, 'log.json'), '{}');
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listFailedTasks();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.content).toBe('');
|
||||
});
|
||||
|
||||
it('should handle task name with underscores', () => {
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed');
|
||||
const taskDir = path.join(failedDir, '2025-01-15T12-34-56_my_task_name');
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listFailedTasks();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.name).toBe('my_task_name');
|
||||
});
|
||||
|
||||
it('should skip non-directory entries', () => {
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed');
|
||||
fs.mkdirSync(failedDir, { recursive: true });
|
||||
// Create a file (not a directory) in the failed dir
|
||||
fs.writeFileSync(path.join(failedDir, '2025-01-15T12-34-56_file-task'), 'content');
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listFailedTasks();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskRunner.listPendingTaskItems', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should return empty array when no pending tasks', () => {
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listPendingTaskItems();
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should convert TaskInfo to TaskListItem with kind=pending', () => {
|
||||
const tasksDir = path.join(tmpDir, '.takt', 'tasks');
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(tasksDir, 'my-task.md'), 'Fix the login bug\nMore details here');
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listPendingTaskItems();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.kind).toBe('pending');
|
||||
expect(result[0]!.name).toBe('my-task');
|
||||
expect(result[0]!.content).toBe('Fix the login bug');
|
||||
});
|
||||
|
||||
it('should truncate content to first line (max 80 chars)', () => {
|
||||
const tasksDir = path.join(tmpDir, '.takt', 'tasks');
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
const longLine = 'A'.repeat(120) + '\nSecond line';
|
||||
fs.writeFileSync(path.join(tasksDir, 'long-task.md'), longLine);
|
||||
|
||||
const runner = new TaskRunner(tmpDir);
|
||||
const result = runner.listPendingTaskItems();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.content).toBe('A'.repeat(80));
|
||||
});
|
||||
});
|
||||
|
||||
describe('listTasks non-interactive JSON output', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-test-json-'));
|
||||
// Initialize as a git repo so detectDefaultBranch works
|
||||
execFileSync('git', ['init', '--initial-branch', 'main'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
execFileSync('git', ['commit', '--allow-empty', '-m', 'init'], { cwd: tmpDir, stdio: 'pipe' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should output JSON as object with branches, pendingTasks, and failedTasks keys', async () => {
|
||||
// Given: a pending task and a failed task
|
||||
const tasksDir = path.join(tmpDir, '.takt', 'tasks');
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(tasksDir, 'my-task.md'), 'Do something');
|
||||
|
||||
const failedDir = path.join(tmpDir, '.takt', 'failed', '2025-01-15T12-34-56_failed-task');
|
||||
fs.mkdirSync(failedDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(failedDir, 'failed-task.md'), 'This failed');
|
||||
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// When: listTasks is called in non-interactive JSON mode
|
||||
await listTasks(tmpDir, undefined, {
|
||||
enabled: true,
|
||||
format: 'json',
|
||||
});
|
||||
|
||||
// Then: output is an object with branches, pendingTasks, failedTasks
|
||||
expect(logSpy).toHaveBeenCalledTimes(1);
|
||||
const output = JSON.parse(logSpy.mock.calls[0]![0] as string);
|
||||
expect(output).toHaveProperty('branches');
|
||||
expect(output).toHaveProperty('pendingTasks');
|
||||
expect(output).toHaveProperty('failedTasks');
|
||||
expect(Array.isArray(output.branches)).toBe(true);
|
||||
expect(Array.isArray(output.pendingTasks)).toBe(true);
|
||||
expect(Array.isArray(output.failedTasks)).toBe(true);
|
||||
expect(output.pendingTasks).toHaveLength(1);
|
||||
expect(output.pendingTasks[0].name).toBe('my-task');
|
||||
expect(output.failedTasks).toHaveLength(1);
|
||||
expect(output.failedTasks[0].name).toBe('failed-task');
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,24 +3,22 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { PieceWithSource } from '../infra/config/index.js';
|
||||
|
||||
const pathsState = vi.hoisted(() => ({
|
||||
globalConfigPath: '',
|
||||
projectConfigPath: '',
|
||||
resourcesDir: '',
|
||||
userCategoriesPath: '',
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/paths.js', async (importOriginal) => {
|
||||
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
return {
|
||||
...original,
|
||||
getGlobalConfigPath: () => pathsState.globalConfigPath,
|
||||
getProjectConfigPath: () => pathsState.projectConfigPath,
|
||||
getLanguage: () => 'en',
|
||||
};
|
||||
});
|
||||
|
||||
@ -32,27 +30,9 @@ vi.mock('../infra/resources/index.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const pieceCategoriesState = vi.hoisted(() => ({
|
||||
categories: undefined as any,
|
||||
showOthersCategory: undefined as boolean | undefined,
|
||||
othersCategoryName: undefined as string | undefined,
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
vi.mock('../infra/config/global/pieceCategories.js', async () => {
|
||||
return {
|
||||
...original,
|
||||
getLanguage: () => 'en',
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../infra/config/global/pieceCategories.js', async (importOriginal) => {
|
||||
const original = await importOriginal() as Record<string, unknown>;
|
||||
return {
|
||||
...original,
|
||||
getPieceCategoriesConfig: () => pieceCategoriesState.categories,
|
||||
getShowOthersCategory: () => pieceCategoriesState.showOthersCategory,
|
||||
getOthersCategoryName: () => pieceCategoriesState.othersCategoryName,
|
||||
ensureUserCategoriesFile: () => pathsState.userCategoriesPath,
|
||||
};
|
||||
});
|
||||
|
||||
@ -87,32 +67,22 @@ function createPieceMap(entries: { name: string; source: 'builtin' | 'user' | 'p
|
||||
describe('piece 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;
|
||||
|
||||
// Reset piece categories state
|
||||
pieceCategoriesState.categories = undefined;
|
||||
pieceCategoriesState.showOthersCategory = undefined;
|
||||
pieceCategoriesState.othersCategoryName = undefined;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should load default categories when no configs define piece_categories', () => {
|
||||
writeYaml(join(resourcesDir, 'default-categories.yaml'), `
|
||||
it('should load categories from user file (auto-copied from default)', () => {
|
||||
const userPath = join(testDir, 'piece-categories.yaml');
|
||||
writeYaml(userPath, `
|
||||
piece_categories:
|
||||
Default:
|
||||
pieces:
|
||||
@ -120,83 +90,51 @@ piece_categories:
|
||||
show_others_category: true
|
||||
others_category_name: "Others"
|
||||
`);
|
||||
pathsState.userCategoriesPath = userPath;
|
||||
|
||||
const config = getPieceCategories(testDir);
|
||||
const config = getPieceCategories();
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Default', pieces: ['simple'], children: [] },
|
||||
]);
|
||||
expect(config!.showOthersCategory).toBe(true);
|
||||
expect(config!.othersCategoryName).toBe('Others');
|
||||
});
|
||||
|
||||
it('should prefer project config over default when piece_categories is defined', () => {
|
||||
writeYaml(join(resourcesDir, 'default-categories.yaml'), `
|
||||
piece_categories:
|
||||
Default:
|
||||
pieces:
|
||||
- simple
|
||||
it('should return null when user file has no piece_categories', () => {
|
||||
const userPath = join(testDir, 'piece-categories.yaml');
|
||||
writeYaml(userPath, `
|
||||
show_others_category: true
|
||||
`);
|
||||
pathsState.userCategoriesPath = userPath;
|
||||
|
||||
writeYaml(projectConfigPath, `
|
||||
const config = getPieceCategories();
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse nested categories from user file', () => {
|
||||
const userPath = join(testDir, 'piece-categories.yaml');
|
||||
writeYaml(userPath, `
|
||||
piece_categories:
|
||||
Project:
|
||||
Parent:
|
||||
pieces:
|
||||
- custom
|
||||
show_others_category: false
|
||||
- parent-piece
|
||||
Child:
|
||||
pieces:
|
||||
- child-piece
|
||||
`);
|
||||
pathsState.userCategoriesPath = userPath;
|
||||
|
||||
const config = getPieceCategories(testDir);
|
||||
const config = getPieceCategories();
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Project', pieces: ['custom'], children: [] },
|
||||
]);
|
||||
expect(config!.showOthersCategory).toBe(false);
|
||||
});
|
||||
|
||||
it('should prefer user config over project config when piece_categories is defined', () => {
|
||||
writeYaml(join(resourcesDir, 'default-categories.yaml'), `
|
||||
piece_categories:
|
||||
Default:
|
||||
pieces:
|
||||
- simple
|
||||
`);
|
||||
|
||||
writeYaml(projectConfigPath, `
|
||||
piece_categories:
|
||||
Project:
|
||||
pieces:
|
||||
- custom
|
||||
`);
|
||||
|
||||
// Simulate user config from separate file
|
||||
pieceCategoriesState.categories = {
|
||||
User: {
|
||||
pieces: ['preferred'],
|
||||
{
|
||||
name: 'Parent',
|
||||
pieces: ['parent-piece'],
|
||||
children: [
|
||||
{ name: 'Child', pieces: ['child-piece'], children: [] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const config = getPieceCategories(testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'User', pieces: ['preferred'], children: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore configs without piece_categories and fall back to default', () => {
|
||||
writeYaml(join(resourcesDir, 'default-categories.yaml'), `
|
||||
piece_categories:
|
||||
Default:
|
||||
pieces:
|
||||
- simple
|
||||
`);
|
||||
|
||||
writeYaml(globalConfigPath, `
|
||||
show_others_category: false
|
||||
`);
|
||||
|
||||
const config = getPieceCategories(testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Default', pieces: ['simple'], children: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
@ -204,10 +142,25 @@ show_others_category: false
|
||||
const config = loadDefaultCategories();
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
it('should load default categories from resources', () => {
|
||||
writeYaml(join(resourcesDir, 'piece-categories.yaml'), `
|
||||
piece_categories:
|
||||
Quick Start:
|
||||
pieces:
|
||||
- default
|
||||
`);
|
||||
|
||||
const config = loadDefaultCategories();
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.pieceCategories).toEqual([
|
||||
{ name: 'Quick Start', pieces: ['default'], children: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCategorizedPieces', () => {
|
||||
it('should warn for missing pieces and generate Others', () => {
|
||||
it('should place all pieces (user and builtin) into a unified category tree', () => {
|
||||
const allPieces = createPieceMap([
|
||||
{ name: 'a', source: 'user' },
|
||||
{ name: 'b', source: 'user' },
|
||||
@ -215,11 +168,7 @@ describe('buildCategorizedPieces', () => {
|
||||
]);
|
||||
const config = {
|
||||
pieceCategories: [
|
||||
{
|
||||
name: 'Cat',
|
||||
pieces: ['a', 'missing', 'c'],
|
||||
children: [],
|
||||
},
|
||||
{ name: 'Cat', pieces: ['a', 'missing', 'c'], children: [] },
|
||||
],
|
||||
showOthersCategory: true,
|
||||
othersCategoryName: 'Others',
|
||||
@ -227,12 +176,9 @@ describe('buildCategorizedPieces', () => {
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
expect(categorized.categories).toEqual([
|
||||
{ name: 'Cat', pieces: ['a'], children: [] },
|
||||
{ name: 'Cat', pieces: ['a', 'c'], children: [] },
|
||||
{ name: 'Others', pieces: ['b'], children: [] },
|
||||
]);
|
||||
expect(categorized.builtinCategories).toEqual([
|
||||
{ name: 'Cat', pieces: ['c'], children: [] },
|
||||
]);
|
||||
expect(categorized.missingPieces).toEqual([
|
||||
{ categoryPath: ['Cat'], pieceName: 'missing' },
|
||||
]);
|
||||
@ -252,7 +198,67 @@ describe('buildCategorizedPieces', () => {
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
expect(categorized.categories).toEqual([]);
|
||||
expect(categorized.builtinCategories).toEqual([]);
|
||||
});
|
||||
|
||||
it('should append Others category for uncategorized pieces', () => {
|
||||
const allPieces = createPieceMap([
|
||||
{ name: 'default', source: 'builtin' },
|
||||
{ name: 'extra', source: 'builtin' },
|
||||
]);
|
||||
const config = {
|
||||
pieceCategories: [
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
],
|
||||
showOthersCategory: true,
|
||||
othersCategoryName: 'Others',
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
expect(categorized.categories).toEqual([
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
{ name: 'Others', pieces: ['extra'], children: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should merge uncategorized pieces into existing Others category', () => {
|
||||
const allPieces = createPieceMap([
|
||||
{ name: 'default', source: 'builtin' },
|
||||
{ name: 'extra', source: 'builtin' },
|
||||
{ name: 'user-piece', source: 'user' },
|
||||
]);
|
||||
const config = {
|
||||
pieceCategories: [
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
{ name: 'Others', pieces: ['extra'], children: [] },
|
||||
],
|
||||
showOthersCategory: true,
|
||||
othersCategoryName: 'Others',
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
expect(categorized.categories).toEqual([
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
{ name: 'Others', pieces: ['extra', 'user-piece'], children: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not append Others when showOthersCategory is false', () => {
|
||||
const allPieces = createPieceMap([
|
||||
{ name: 'default', source: 'builtin' },
|
||||
{ name: 'extra', source: 'builtin' },
|
||||
]);
|
||||
const config = {
|
||||
pieceCategories: [
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
],
|
||||
showOthersCategory: false,
|
||||
othersCategoryName: 'Others',
|
||||
};
|
||||
|
||||
const categorized = buildCategorizedPieces(allPieces, config);
|
||||
expect(categorized.categories).toEqual([
|
||||
{ name: 'Main', pieces: ['default'], children: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find categories containing a piece', () => {
|
||||
@ -280,3 +286,25 @@ describe('buildCategorizedPieces', () => {
|
||||
expect(paths).toEqual(['Parent / Child']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureUserCategoriesFile (integration)', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(tmpdir(), `takt-cat-ensure-${randomUUID()}`);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should copy default categories to user path when missing', async () => {
|
||||
// Use real ensureUserCategoriesFile (not mocked)
|
||||
const { ensureUserCategoriesFile } = await import('../infra/config/global/pieceCategories.js');
|
||||
|
||||
// This test depends on the mock still being active — just verify the mock returns our path
|
||||
const result = ensureUserCategoriesFile('/tmp/default.yaml');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,23 +4,41 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { PieceDirEntry } from '../infra/config/loaders/pieceLoader.js';
|
||||
import type { CategorizedPieces } from '../infra/config/loaders/pieceCategories.js';
|
||||
import type { PieceWithSource } from '../infra/config/loaders/pieceResolver.js';
|
||||
|
||||
const selectOptionMock = vi.fn();
|
||||
const bookmarkState = vi.hoisted(() => ({
|
||||
bookmarks: [] as string[],
|
||||
}));
|
||||
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
selectOption: selectOptionMock,
|
||||
}));
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/global/index.js', () => ({
|
||||
getBookmarkedPieces: () => [],
|
||||
getBookmarkedPieces: () => bookmarkState.bookmarks,
|
||||
addBookmark: vi.fn(),
|
||||
removeBookmark: vi.fn(),
|
||||
toggleBookmark: vi.fn(),
|
||||
}));
|
||||
|
||||
const { selectPieceFromEntries } = await import('../features/pieceSelection/index.js');
|
||||
vi.mock('../infra/config/index.js', async (importOriginal) => {
|
||||
const actual = await importOriginal() as Record<string, unknown>;
|
||||
return actual;
|
||||
});
|
||||
|
||||
const { selectPieceFromEntries, selectPieceFromCategorizedPieces } = await import('../features/pieceSelection/index.js');
|
||||
|
||||
describe('selectPieceFromEntries', () => {
|
||||
beforeEach(() => {
|
||||
selectOptionMock.mockReset();
|
||||
bookmarkState.bookmarks = [];
|
||||
});
|
||||
|
||||
it('should select from custom pieces when source is chosen', async () => {
|
||||
@ -50,3 +68,166 @@ describe('selectPieceFromEntries', () => {
|
||||
expect(selectOptionMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
function createPieceMap(entries: { name: string; source: 'user' | 'builtin' }[]): Map<string, PieceWithSource> {
|
||||
const map = new Map<string, PieceWithSource>();
|
||||
for (const e of entries) {
|
||||
map.set(e.name, {
|
||||
source: e.source,
|
||||
config: {
|
||||
name: e.name,
|
||||
movements: [],
|
||||
initialMovement: 'start',
|
||||
maxIterations: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
describe('selectPieceFromCategorizedPieces', () => {
|
||||
beforeEach(() => {
|
||||
selectOptionMock.mockReset();
|
||||
bookmarkState.bookmarks = [];
|
||||
});
|
||||
|
||||
it('should show categories at top level', async () => {
|
||||
const categorized: CategorizedPieces = {
|
||||
categories: [
|
||||
{ name: 'My Pieces', pieces: ['my-piece'], children: [] },
|
||||
{ name: 'Quick Start', pieces: ['default'], children: [] },
|
||||
],
|
||||
allPieces: createPieceMap([
|
||||
{ name: 'my-piece', source: 'user' },
|
||||
{ name: 'default', source: 'builtin' },
|
||||
]),
|
||||
missingPieces: [],
|
||||
};
|
||||
|
||||
selectOptionMock.mockResolvedValueOnce('__current__');
|
||||
|
||||
await selectPieceFromCategorizedPieces(categorized, 'my-piece');
|
||||
|
||||
const firstCallOptions = selectOptionMock.mock.calls[0]![1] as { label: string; value: string }[];
|
||||
const labels = firstCallOptions.map((o) => o.label);
|
||||
|
||||
expect(labels[0]).toBe('🎼 my-piece (current)');
|
||||
expect(labels.some((l) => l.includes('My Pieces'))).toBe(true);
|
||||
expect(labels.some((l) => l.includes('Quick Start'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should show current piece and bookmarks above categories', async () => {
|
||||
bookmarkState.bookmarks = ['research'];
|
||||
|
||||
const categorized: CategorizedPieces = {
|
||||
categories: [
|
||||
{ name: 'Quick Start', pieces: ['default'], children: [] },
|
||||
],
|
||||
allPieces: createPieceMap([
|
||||
{ name: 'default', source: 'builtin' },
|
||||
{ name: 'research', source: 'builtin' },
|
||||
]),
|
||||
missingPieces: [],
|
||||
};
|
||||
|
||||
selectOptionMock.mockResolvedValueOnce('__current__');
|
||||
|
||||
const selected = await selectPieceFromCategorizedPieces(categorized, 'default');
|
||||
expect(selected).toBe('default');
|
||||
|
||||
const firstCallOptions = selectOptionMock.mock.calls[0]![1] as { label: string; value: string }[];
|
||||
const labels = firstCallOptions.map((o) => o.label);
|
||||
|
||||
// Current piece first, bookmarks second, categories after
|
||||
expect(labels[0]).toBe('🎼 default (current)');
|
||||
expect(labels[1]).toBe('🎼 research [*]');
|
||||
});
|
||||
|
||||
it('should navigate into a category and select a piece', async () => {
|
||||
const categorized: CategorizedPieces = {
|
||||
categories: [
|
||||
{ name: 'Dev', pieces: ['my-piece'], children: [] },
|
||||
],
|
||||
allPieces: createPieceMap([
|
||||
{ name: 'my-piece', source: 'user' },
|
||||
]),
|
||||
missingPieces: [],
|
||||
};
|
||||
|
||||
// Select category, then select piece inside it
|
||||
selectOptionMock
|
||||
.mockResolvedValueOnce('__custom_category__:Dev')
|
||||
.mockResolvedValueOnce('my-piece');
|
||||
|
||||
const selected = await selectPieceFromCategorizedPieces(categorized, '');
|
||||
expect(selected).toBe('my-piece');
|
||||
});
|
||||
|
||||
it('should navigate into subcategories recursively', async () => {
|
||||
const categorized: CategorizedPieces = {
|
||||
categories: [
|
||||
{
|
||||
name: 'Hybrid',
|
||||
pieces: [],
|
||||
children: [
|
||||
{ name: 'Quick Start', pieces: ['hybrid-default'], children: [] },
|
||||
{ name: 'Full Stack', pieces: ['hybrid-expert'], children: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
allPieces: createPieceMap([
|
||||
{ name: 'hybrid-default', source: 'builtin' },
|
||||
{ name: 'hybrid-expert', source: 'builtin' },
|
||||
]),
|
||||
missingPieces: [],
|
||||
};
|
||||
|
||||
// Select Hybrid category → Quick Start subcategory → piece
|
||||
selectOptionMock
|
||||
.mockResolvedValueOnce('__custom_category__:Hybrid')
|
||||
.mockResolvedValueOnce('__category__:Quick Start')
|
||||
.mockResolvedValueOnce('hybrid-default');
|
||||
|
||||
const selected = await selectPieceFromCategorizedPieces(categorized, '');
|
||||
expect(selected).toBe('hybrid-default');
|
||||
expect(selectOptionMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should show subcategories and pieces at the same level within a category', async () => {
|
||||
const categorized: CategorizedPieces = {
|
||||
categories: [
|
||||
{
|
||||
name: 'Dev',
|
||||
pieces: ['base-piece'],
|
||||
children: [
|
||||
{ name: 'Advanced', pieces: ['adv-piece'], children: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
allPieces: createPieceMap([
|
||||
{ name: 'base-piece', source: 'user' },
|
||||
{ name: 'adv-piece', source: 'user' },
|
||||
]),
|
||||
missingPieces: [],
|
||||
};
|
||||
|
||||
// Select Dev category, then directly select the root-level piece
|
||||
selectOptionMock
|
||||
.mockResolvedValueOnce('__custom_category__:Dev')
|
||||
.mockResolvedValueOnce('base-piece');
|
||||
|
||||
const selected = await selectPieceFromCategorizedPieces(categorized, '');
|
||||
expect(selected).toBe('base-piece');
|
||||
|
||||
// Second call should show Advanced subcategory AND base-piece at same level
|
||||
const secondCallOptions = selectOptionMock.mock.calls[1]![1] as { label: string; value: string }[];
|
||||
const labels = secondCallOptions.map((o) => o.label);
|
||||
|
||||
// Should contain the subcategory folder
|
||||
expect(labels.some((l) => l.includes('Advanced'))).toBe(true);
|
||||
// Should contain the piece
|
||||
expect(labels.some((l) => l.includes('base-piece'))).toBe(true);
|
||||
// Should NOT contain the parent category again
|
||||
expect(labels.some((l) => l.includes('Dev'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
186
src/__tests__/saveTaskFile.test.ts
Normal file
186
src/__tests__/saveTaskFile.test.ts
Normal file
@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Tests for saveTaskFile and saveTaskFromInteractive
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
vi.mock('../infra/task/summarize.js', () => ({
|
||||
summarizeTaskName: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
success: vi.fn(),
|
||||
info: vi.fn(),
|
||||
blankLine: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { summarizeTaskName } from '../infra/task/summarize.js';
|
||||
import { success, info } from '../shared/ui/index.js';
|
||||
import { saveTaskFile, saveTaskFromInteractive } from '../features/tasks/add/index.js';
|
||||
|
||||
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
|
||||
const mockSuccess = vi.mocked(success);
|
||||
const mockInfo = vi.mocked(info);
|
||||
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testDir = fs.mkdtempSync(path.join(tmpdir(), 'takt-test-save-'));
|
||||
mockSummarizeTaskName.mockResolvedValue('test-task');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('saveTaskFile', () => {
|
||||
it('should create task file with correct YAML content', async () => {
|
||||
// Given
|
||||
const taskContent = 'Implement feature X\nDetails here';
|
||||
|
||||
// When
|
||||
const filePath = await saveTaskFile(testDir, taskContent);
|
||||
|
||||
// Then
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).toContain('Implement feature X');
|
||||
expect(content).toContain('Details here');
|
||||
});
|
||||
|
||||
it('should create .takt/tasks directory if it does not exist', async () => {
|
||||
// Given
|
||||
const tasksDir = path.join(testDir, '.takt', 'tasks');
|
||||
expect(fs.existsSync(tasksDir)).toBe(false);
|
||||
|
||||
// When
|
||||
await saveTaskFile(testDir, 'Task content');
|
||||
|
||||
// Then
|
||||
expect(fs.existsSync(tasksDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('should include piece in YAML when specified', async () => {
|
||||
// When
|
||||
const filePath = await saveTaskFile(testDir, 'Task', { piece: 'review' });
|
||||
|
||||
// Then
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).toContain('piece: review');
|
||||
});
|
||||
|
||||
it('should include issue number in YAML when specified', async () => {
|
||||
// When
|
||||
const filePath = await saveTaskFile(testDir, 'Task', { issue: 42 });
|
||||
|
||||
// Then
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).toContain('issue: 42');
|
||||
});
|
||||
|
||||
it('should include worktree in YAML when specified', async () => {
|
||||
// When
|
||||
const filePath = await saveTaskFile(testDir, 'Task', { worktree: true });
|
||||
|
||||
// Then
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).toContain('worktree: true');
|
||||
});
|
||||
|
||||
it('should include branch in YAML when specified', async () => {
|
||||
// When
|
||||
const filePath = await saveTaskFile(testDir, 'Task', { branch: 'feat/my-branch' });
|
||||
|
||||
// Then
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).toContain('branch: feat/my-branch');
|
||||
});
|
||||
|
||||
it('should not include optional fields when not specified', async () => {
|
||||
// When
|
||||
const filePath = await saveTaskFile(testDir, 'Simple task');
|
||||
|
||||
// Then
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
expect(content).not.toContain('piece:');
|
||||
expect(content).not.toContain('issue:');
|
||||
expect(content).not.toContain('worktree:');
|
||||
expect(content).not.toContain('branch:');
|
||||
});
|
||||
|
||||
it('should use first line for filename generation', async () => {
|
||||
// When
|
||||
await saveTaskFile(testDir, 'First line\nSecond line');
|
||||
|
||||
// Then
|
||||
expect(mockSummarizeTaskName).toHaveBeenCalledWith('First line', { cwd: testDir });
|
||||
});
|
||||
|
||||
it('should handle duplicate filenames with counter', async () => {
|
||||
// Given: first file already exists
|
||||
await saveTaskFile(testDir, 'Task 1');
|
||||
|
||||
// When: second file with same slug
|
||||
const filePath = await saveTaskFile(testDir, 'Task 2');
|
||||
|
||||
// Then
|
||||
expect(path.basename(filePath)).toBe('test-task-1.yaml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveTaskFromInteractive', () => {
|
||||
it('should save task and display success message', async () => {
|
||||
// When
|
||||
await saveTaskFromInteractive(testDir, 'Task content');
|
||||
|
||||
// Then
|
||||
expect(mockSuccess).toHaveBeenCalledWith('Task created: test-task.yaml');
|
||||
expect(mockInfo).toHaveBeenCalledWith(expect.stringContaining('Path:'));
|
||||
});
|
||||
|
||||
it('should display piece info when specified', async () => {
|
||||
// When
|
||||
await saveTaskFromInteractive(testDir, 'Task content', 'review');
|
||||
|
||||
// Then
|
||||
expect(mockInfo).toHaveBeenCalledWith(' Piece: review');
|
||||
});
|
||||
|
||||
it('should include piece in saved YAML', async () => {
|
||||
// When
|
||||
await saveTaskFromInteractive(testDir, 'Task content', 'custom');
|
||||
|
||||
// Then
|
||||
const tasksDir = path.join(testDir, '.takt', 'tasks');
|
||||
const files = fs.readdirSync(tasksDir);
|
||||
expect(files.length).toBe(1);
|
||||
const content = fs.readFileSync(path.join(tasksDir, files[0]!), 'utf-8');
|
||||
expect(content).toContain('piece: custom');
|
||||
});
|
||||
|
||||
it('should not display piece info when not specified', async () => {
|
||||
// When
|
||||
await saveTaskFromInteractive(testDir, 'Task content');
|
||||
|
||||
// Then
|
||||
const pieceInfoCalls = mockInfo.mock.calls.filter(
|
||||
(call) => typeof call[0] === 'string' && call[0].includes('Piece:')
|
||||
);
|
||||
expect(pieceInfoCalls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
180
src/__tests__/taskDeleteActions.test.ts
Normal file
180
src/__tests__/taskDeleteActions.test.ts
Normal file
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Tests for taskDeleteActions — pending/failed task deletion
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
confirm: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { confirm } from '../shared/prompt/index.js';
|
||||
import { success, error as logError } from '../shared/ui/index.js';
|
||||
import { deletePendingTask, deleteFailedTask } from '../features/tasks/list/taskDeleteActions.js';
|
||||
import type { TaskListItem } from '../infra/task/types.js';
|
||||
|
||||
const mockConfirm = vi.mocked(confirm);
|
||||
const mockSuccess = vi.mocked(success);
|
||||
const mockLogError = vi.mocked(logError);
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'takt-test-delete-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('deletePendingTask', () => {
|
||||
it('should delete pending task file when confirmed', async () => {
|
||||
// Given
|
||||
const filePath = path.join(tmpDir, 'my-task.md');
|
||||
fs.writeFileSync(filePath, 'task content');
|
||||
const task: TaskListItem = {
|
||||
kind: 'pending',
|
||||
name: 'my-task',
|
||||
createdAt: '2025-01-15',
|
||||
filePath,
|
||||
content: 'task content',
|
||||
};
|
||||
mockConfirm.mockResolvedValue(true);
|
||||
|
||||
// When
|
||||
const result = await deletePendingTask(task);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(true);
|
||||
expect(fs.existsSync(filePath)).toBe(false);
|
||||
expect(mockSuccess).toHaveBeenCalledWith('Deleted pending task: my-task');
|
||||
});
|
||||
|
||||
it('should not delete when user declines confirmation', async () => {
|
||||
// Given
|
||||
const filePath = path.join(tmpDir, 'my-task.md');
|
||||
fs.writeFileSync(filePath, 'task content');
|
||||
const task: TaskListItem = {
|
||||
kind: 'pending',
|
||||
name: 'my-task',
|
||||
createdAt: '2025-01-15',
|
||||
filePath,
|
||||
content: 'task content',
|
||||
};
|
||||
mockConfirm.mockResolvedValue(false);
|
||||
|
||||
// When
|
||||
const result = await deletePendingTask(task);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false and show error when file does not exist', async () => {
|
||||
// Given
|
||||
const filePath = path.join(tmpDir, 'non-existent.md');
|
||||
const task: TaskListItem = {
|
||||
kind: 'pending',
|
||||
name: 'non-existent',
|
||||
createdAt: '2025-01-15',
|
||||
filePath,
|
||||
content: '',
|
||||
};
|
||||
mockConfirm.mockResolvedValue(true);
|
||||
|
||||
// When
|
||||
const result = await deletePendingTask(task);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
expect(mockLogError).toHaveBeenCalled();
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFailedTask', () => {
|
||||
it('should delete failed task directory when confirmed', async () => {
|
||||
// Given
|
||||
const dirPath = path.join(tmpDir, '2025-01-15T12-34-56_my-task');
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
fs.writeFileSync(path.join(dirPath, 'my-task.md'), 'content');
|
||||
const task: TaskListItem = {
|
||||
kind: 'failed',
|
||||
name: 'my-task',
|
||||
createdAt: '2025-01-15T12:34:56',
|
||||
filePath: dirPath,
|
||||
content: 'content',
|
||||
};
|
||||
mockConfirm.mockResolvedValue(true);
|
||||
|
||||
// When
|
||||
const result = await deleteFailedTask(task);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(true);
|
||||
expect(fs.existsSync(dirPath)).toBe(false);
|
||||
expect(mockSuccess).toHaveBeenCalledWith('Deleted failed task: my-task');
|
||||
});
|
||||
|
||||
it('should not delete when user declines confirmation', async () => {
|
||||
// Given
|
||||
const dirPath = path.join(tmpDir, '2025-01-15T12-34-56_my-task');
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
const task: TaskListItem = {
|
||||
kind: 'failed',
|
||||
name: 'my-task',
|
||||
createdAt: '2025-01-15T12:34:56',
|
||||
filePath: dirPath,
|
||||
content: '',
|
||||
};
|
||||
mockConfirm.mockResolvedValue(false);
|
||||
|
||||
// When
|
||||
const result = await deleteFailedTask(task);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
expect(fs.existsSync(dirPath)).toBe(true);
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false and show error when directory does not exist', async () => {
|
||||
// Given
|
||||
const dirPath = path.join(tmpDir, 'non-existent-dir');
|
||||
const task: TaskListItem = {
|
||||
kind: 'failed',
|
||||
name: 'non-existent',
|
||||
createdAt: '2025-01-15T12:34:56',
|
||||
filePath: dirPath,
|
||||
content: '',
|
||||
};
|
||||
mockConfirm.mockResolvedValue(true);
|
||||
|
||||
// When
|
||||
const result = await deleteFailedTask(task);
|
||||
|
||||
// Then
|
||||
expect(result).toBe(false);
|
||||
expect(mockLogError).toHaveBeenCalled();
|
||||
expect(mockSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -7,7 +7,7 @@
|
||||
import { clearAgentSessions, getCurrentPiece } from '../../infra/config/index.js';
|
||||
import { success } from '../../shared/ui/index.js';
|
||||
import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
|
||||
import { switchPiece, switchConfig, ejectBuiltin } from '../../features/config/index.js';
|
||||
import { switchPiece, switchConfig, ejectBuiltin, resetCategoriesToDefault, deploySkill } from '../../features/config/index.js';
|
||||
import { previewPrompts } from '../../features/prompt/index.js';
|
||||
import { program, resolvedCwd } from './program.js';
|
||||
import { resolveAgentOverrides } from './helpers.js';
|
||||
@ -75,10 +75,11 @@ program
|
||||
|
||||
program
|
||||
.command('eject')
|
||||
.description('Copy builtin piece/agents to ~/.takt/ for customization')
|
||||
.description('Copy builtin piece/agents for customization (default: project .takt/)')
|
||||
.argument('[name]', 'Specific builtin to eject')
|
||||
.action(async (name?: string) => {
|
||||
await ejectBuiltin(name);
|
||||
.option('--global', 'Eject to ~/.takt/ instead of project .takt/')
|
||||
.action(async (name: string | undefined, opts: { global?: boolean }) => {
|
||||
await ejectBuiltin(name, { global: opts.global, projectDir: resolvedCwd });
|
||||
});
|
||||
|
||||
program
|
||||
@ -89,6 +90,17 @@ program
|
||||
await switchConfig(resolvedCwd, key);
|
||||
});
|
||||
|
||||
const reset = program
|
||||
.command('reset')
|
||||
.description('Reset settings to defaults');
|
||||
|
||||
reset
|
||||
.command('categories')
|
||||
.description('Reset piece categories to builtin defaults')
|
||||
.action(async () => {
|
||||
await resetCategoriesToDefault();
|
||||
});
|
||||
|
||||
program
|
||||
.command('prompt')
|
||||
.description('Preview assembled prompts for each movement and phase')
|
||||
@ -96,3 +108,10 @@ program
|
||||
.action(async (piece?: string) => {
|
||||
await previewPrompts(resolvedCwd, piece);
|
||||
});
|
||||
|
||||
program
|
||||
.command('export-cc')
|
||||
.description('Export takt pieces/agents as Claude Code Skill (~/.claude/)')
|
||||
.action(async () => {
|
||||
await deploySkill();
|
||||
});
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import { info, error } from '../../shared/ui/index.js';
|
||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||
import { resolveIssueTask } from '../../infra/github/index.js';
|
||||
import { selectAndExecuteTask, determinePiece, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||
import { executePipeline } from '../../features/pipeline/index.js';
|
||||
import { interactiveMode } from '../../features/interactive/index.js';
|
||||
import { getPieceDescription } from '../../infra/config/index.js';
|
||||
@ -99,14 +99,25 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
const pieceContext = getPieceDescription(pieceId, resolvedCwd);
|
||||
const result = await interactiveMode(resolvedCwd, task, pieceContext);
|
||||
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
switch (result.action) {
|
||||
case 'execute':
|
||||
selectOptions.interactiveUserInput = true;
|
||||
selectOptions.piece = pieceId;
|
||||
selectOptions.interactiveMetadata = { confirmed: true, task: result.task };
|
||||
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
|
||||
break;
|
||||
|
||||
selectOptions.interactiveUserInput = true;
|
||||
selectOptions.piece = pieceId;
|
||||
selectOptions.interactiveMetadata = { confirmed: result.confirmed, task: result.task };
|
||||
await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides);
|
||||
case 'create_issue':
|
||||
createIssueFromTask(result.task);
|
||||
break;
|
||||
|
||||
case 'save_task':
|
||||
await saveTaskFromInteractive(resolvedCwd, result.task, pieceId);
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
|
||||
@ -11,6 +11,9 @@ export type {
|
||||
PieceRule,
|
||||
PieceMovement,
|
||||
LoopDetectionConfig,
|
||||
LoopMonitorConfig,
|
||||
LoopMonitorJudge,
|
||||
LoopMonitorRule,
|
||||
PieceConfig,
|
||||
PieceState,
|
||||
CustomAgentConfig,
|
||||
|
||||
@ -91,6 +91,36 @@ export interface LoopDetectionConfig {
|
||||
action?: 'abort' | 'warn' | 'ignore';
|
||||
}
|
||||
|
||||
/** Rule for loop monitor judge decision */
|
||||
export interface LoopMonitorRule {
|
||||
/** Human-readable condition text */
|
||||
condition: string;
|
||||
/** Next movement name to transition to */
|
||||
next: string;
|
||||
}
|
||||
|
||||
/** Judge configuration for loop monitor */
|
||||
export interface LoopMonitorJudge {
|
||||
/** Agent path, inline prompt, or undefined (uses default) */
|
||||
agent?: string;
|
||||
/** Resolved absolute path to agent prompt file (set by loader) */
|
||||
agentPath?: string;
|
||||
/** Custom instruction template for the judge (uses default if omitted) */
|
||||
instructionTemplate?: string;
|
||||
/** Rules for the judge's decision */
|
||||
rules: LoopMonitorRule[];
|
||||
}
|
||||
|
||||
/** Loop monitor configuration for detecting cyclic patterns between movements */
|
||||
export interface LoopMonitorConfig {
|
||||
/** Ordered list of movement names forming the cycle to detect */
|
||||
cycle: string[];
|
||||
/** Number of complete cycles before triggering the judge (default: 3) */
|
||||
threshold: number;
|
||||
/** Judge configuration for deciding what to do when threshold is reached */
|
||||
judge: LoopMonitorJudge;
|
||||
}
|
||||
|
||||
/** Piece configuration */
|
||||
export interface PieceConfig {
|
||||
name: string;
|
||||
@ -100,6 +130,8 @@ export interface PieceConfig {
|
||||
maxIterations: number;
|
||||
/** Loop detection settings */
|
||||
loopDetection?: LoopDetectionConfig;
|
||||
/** Loop monitors for detecting cyclic patterns between movements */
|
||||
loopMonitors?: LoopMonitorConfig[];
|
||||
/**
|
||||
* Agent to use for answering AskUserQuestion prompts automatically.
|
||||
* When specified, questions from Claude Code are routed to this agent
|
||||
|
||||
@ -158,6 +158,34 @@ export const PieceMovementRawSchema = z.object({
|
||||
parallel: z.array(ParallelSubMovementRawSchema).optional(),
|
||||
});
|
||||
|
||||
/** Loop monitor rule schema */
|
||||
export const LoopMonitorRuleSchema = z.object({
|
||||
/** Human-readable condition text */
|
||||
condition: z.string().min(1),
|
||||
/** Next movement name to transition to */
|
||||
next: z.string().min(1),
|
||||
});
|
||||
|
||||
/** Loop monitor judge schema */
|
||||
export const LoopMonitorJudgeSchema = z.object({
|
||||
/** Agent path, inline prompt, or omitted (uses default) */
|
||||
agent: z.string().optional(),
|
||||
/** Custom instruction template for the judge */
|
||||
instruction_template: z.string().optional(),
|
||||
/** Rules for the judge's decision */
|
||||
rules: z.array(LoopMonitorRuleSchema).min(1),
|
||||
});
|
||||
|
||||
/** Loop monitor configuration schema */
|
||||
export const LoopMonitorSchema = z.object({
|
||||
/** Ordered list of movement names forming the cycle to detect */
|
||||
cycle: z.array(z.string().min(1)).min(2),
|
||||
/** Number of complete cycles before triggering the judge (default: 3) */
|
||||
threshold: z.number().int().positive().optional().default(3),
|
||||
/** Judge configuration */
|
||||
judge: LoopMonitorJudgeSchema,
|
||||
});
|
||||
|
||||
/** Piece configuration schema - raw YAML format */
|
||||
export const PieceConfigRawSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
@ -165,6 +193,7 @@ export const PieceConfigRawSchema = z.object({
|
||||
movements: z.array(PieceMovementRawSchema).min(1),
|
||||
initial_movement: z.string().optional(),
|
||||
max_iterations: z.number().int().positive().optional().default(10),
|
||||
loop_monitors: z.array(LoopMonitorSchema).optional(),
|
||||
answer_agent: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@ -30,6 +30,9 @@ export type {
|
||||
ReportObjectConfig,
|
||||
PieceMovement,
|
||||
LoopDetectionConfig,
|
||||
LoopMonitorConfig,
|
||||
LoopMonitorJudge,
|
||||
LoopMonitorRule,
|
||||
PieceConfig,
|
||||
PieceState,
|
||||
} from './piece-types.js';
|
||||
|
||||
@ -14,11 +14,13 @@ import type {
|
||||
PieceState,
|
||||
PieceMovement,
|
||||
AgentResponse,
|
||||
LoopMonitorConfig,
|
||||
} from '../../models/types.js';
|
||||
import { COMPLETE_MOVEMENT, ABORT_MOVEMENT, ERROR_MESSAGES } from '../constants.js';
|
||||
import type { PieceEngineOptions } from '../types.js';
|
||||
import { determineNextMovementByRules } from './transitions.js';
|
||||
import { LoopDetector } from './loop-detector.js';
|
||||
import { CycleDetector } from './cycle-detector.js';
|
||||
import { handleBlocked } from './blocked-handler.js';
|
||||
import {
|
||||
createInitialState,
|
||||
@ -51,6 +53,7 @@ export class PieceEngine extends EventEmitter {
|
||||
private task: string;
|
||||
private options: PieceEngineOptions;
|
||||
private loopDetector: LoopDetector;
|
||||
private cycleDetector: CycleDetector;
|
||||
private reportDir: string;
|
||||
private abortRequested = false;
|
||||
|
||||
@ -72,6 +75,7 @@ export class PieceEngine extends EventEmitter {
|
||||
this.task = task;
|
||||
this.options = options;
|
||||
this.loopDetector = new LoopDetector(config.loopDetection);
|
||||
this.cycleDetector = new CycleDetector(config.loopMonitors ?? []);
|
||||
this.reportDir = `.takt/reports/${generateReportDir(task)}`;
|
||||
this.ensureReportDirExists();
|
||||
this.validateConfig();
|
||||
@ -183,6 +187,26 @@ export class PieceEngine extends EventEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate loop_monitors
|
||||
if (this.config.loopMonitors) {
|
||||
for (const monitor of this.config.loopMonitors) {
|
||||
for (const cycleName of monitor.cycle) {
|
||||
if (!movementNames.has(cycleName)) {
|
||||
throw new Error(
|
||||
`Invalid loop_monitor: cycle references unknown movement "${cycleName}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const rule of monitor.judge.rules) {
|
||||
if (!movementNames.has(rule.next)) {
|
||||
throw new Error(
|
||||
`Invalid loop_monitor judge rule: target movement "${rule.next}" does not exist`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get current piece state */
|
||||
@ -300,6 +324,120 @@ export class PieceEngine extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the default instruction template for a loop monitor judge.
|
||||
* Used when the monitor config does not specify a custom instruction_template.
|
||||
*/
|
||||
private buildDefaultJudgeInstructionTemplate(
|
||||
monitor: LoopMonitorConfig,
|
||||
cycleCount: number,
|
||||
language: string,
|
||||
): string {
|
||||
const cycleNames = monitor.cycle.join(' → ');
|
||||
const rulesDesc = monitor.judge.rules.map((r) => `- ${r.condition} → ${r.next}`).join('\n');
|
||||
|
||||
if (language === 'ja') {
|
||||
return [
|
||||
`ムーブメントのサイクル [${cycleNames}] が ${cycleCount} 回繰り返されました。`,
|
||||
'',
|
||||
'このループが健全(進捗がある)か、非生産的(同じ問題を繰り返している)かを判断してください。',
|
||||
'',
|
||||
'**判断の選択肢:**',
|
||||
rulesDesc,
|
||||
'',
|
||||
'**判断基準:**',
|
||||
'- 各サイクルで新しい問題が発見・修正されているか',
|
||||
'- 同じ指摘が繰り返されていないか',
|
||||
'- 全体的な進捗があるか',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return [
|
||||
`The movement cycle [${cycleNames}] has repeated ${cycleCount} times.`,
|
||||
'',
|
||||
'Determine whether this loop is healthy (making progress) or unproductive (repeating the same issues).',
|
||||
'',
|
||||
'**Decision options:**',
|
||||
rulesDesc,
|
||||
'',
|
||||
'**Judgment criteria:**',
|
||||
'- Are new issues being found/fixed in each cycle?',
|
||||
'- Are the same findings being repeated?',
|
||||
'- Is there overall progress?',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a loop monitor judge as a synthetic movement.
|
||||
* Returns the next movement name determined by the judge.
|
||||
*/
|
||||
private async runLoopMonitorJudge(
|
||||
monitor: LoopMonitorConfig,
|
||||
cycleCount: number,
|
||||
): Promise<string> {
|
||||
const language = this.options.language ?? 'en';
|
||||
const instructionTemplate = monitor.judge.instructionTemplate
|
||||
?? this.buildDefaultJudgeInstructionTemplate(monitor, cycleCount, language);
|
||||
|
||||
// Replace {cycle_count} in custom templates
|
||||
const processedTemplate = instructionTemplate.replace(/\{cycle_count\}/g, String(cycleCount));
|
||||
|
||||
// Build a synthetic PieceMovement for the judge
|
||||
const judgeMovement: PieceMovement = {
|
||||
name: `_loop_judge_${monitor.cycle.join('_')}`,
|
||||
agent: monitor.judge.agent,
|
||||
agentPath: monitor.judge.agentPath,
|
||||
agentDisplayName: 'loop-judge',
|
||||
edit: false,
|
||||
instructionTemplate: processedTemplate,
|
||||
rules: monitor.judge.rules.map((r) => ({
|
||||
condition: r.condition,
|
||||
next: r.next,
|
||||
})),
|
||||
passPreviousResponse: true,
|
||||
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||
};
|
||||
|
||||
log.info('Running loop monitor judge', {
|
||||
cycle: monitor.cycle,
|
||||
cycleCount,
|
||||
threshold: monitor.threshold,
|
||||
});
|
||||
|
||||
this.state.iteration++;
|
||||
const movementIteration = incrementMovementIteration(this.state, judgeMovement.name);
|
||||
const prebuiltInstruction = this.movementExecutor.buildInstruction(
|
||||
judgeMovement, movementIteration, this.state, this.task, this.config.maxIterations,
|
||||
);
|
||||
|
||||
this.emit('movement:start', judgeMovement, this.state.iteration, prebuiltInstruction);
|
||||
|
||||
const { response, instruction } = await this.movementExecutor.runNormalMovement(
|
||||
judgeMovement,
|
||||
this.state,
|
||||
this.task,
|
||||
this.config.maxIterations,
|
||||
this.updateAgentSession.bind(this),
|
||||
prebuiltInstruction,
|
||||
);
|
||||
this.emitCollectedReports();
|
||||
this.emit('movement:complete', judgeMovement, response, instruction);
|
||||
|
||||
// Resolve next movement from the judge's rules
|
||||
const nextMovement = this.resolveNextMovement(judgeMovement, response);
|
||||
|
||||
log.info('Loop monitor judge decision', {
|
||||
cycle: monitor.cycle,
|
||||
nextMovement,
|
||||
matchedRuleIndex: response.matchedRuleIndex,
|
||||
});
|
||||
|
||||
// Reset cycle detector to prevent re-triggering immediately
|
||||
this.cycleDetector.reset();
|
||||
|
||||
return nextMovement;
|
||||
}
|
||||
|
||||
/** Run the piece to completion */
|
||||
async run(): Promise<PieceState> {
|
||||
while (this.state.status === 'running') {
|
||||
@ -378,7 +516,7 @@ export class PieceEngine extends EventEmitter {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextMovement = this.resolveNextMovement(movement, response);
|
||||
let nextMovement = this.resolveNextMovement(movement, response);
|
||||
log.debug('Movement transition', {
|
||||
from: movement.name,
|
||||
status: response.status,
|
||||
@ -411,6 +549,23 @@ export class PieceEngine extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Check loop monitors (cycle detection) after movement completion
|
||||
const cycleCheck = this.cycleDetector.recordAndCheck(movement.name);
|
||||
if (cycleCheck.triggered && cycleCheck.monitor) {
|
||||
log.info('Loop monitor cycle threshold reached', {
|
||||
cycle: cycleCheck.monitor.cycle,
|
||||
cycleCount: cycleCheck.cycleCount,
|
||||
threshold: cycleCheck.monitor.threshold,
|
||||
});
|
||||
this.emit('movement:cycle_detected', cycleCheck.monitor, cycleCheck.cycleCount);
|
||||
|
||||
// Run the judge to decide what to do
|
||||
nextMovement = await this.runLoopMonitorJudge(
|
||||
cycleCheck.monitor,
|
||||
cycleCheck.cycleCount,
|
||||
);
|
||||
}
|
||||
|
||||
if (nextMovement === COMPLETE_MOVEMENT) {
|
||||
this.state.status = 'completed';
|
||||
this.emit('piece:complete', this.state);
|
||||
|
||||
131
src/core/piece/engine/cycle-detector.ts
Normal file
131
src/core/piece/engine/cycle-detector.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Cycle detection for loop monitors.
|
||||
*
|
||||
* Tracks movement execution history and detects when a specific cycle
|
||||
* of movements has been repeated a configured number of times (threshold).
|
||||
*
|
||||
* Example:
|
||||
* cycle: [ai_review, ai_fix], threshold: 3
|
||||
* History: ai_review → ai_fix → ai_review → ai_fix → ai_review → ai_fix
|
||||
* ↑
|
||||
* 3 cycles → trigger
|
||||
*/
|
||||
|
||||
import type { LoopMonitorConfig } from '../../models/types.js';
|
||||
|
||||
/** Result of checking a single loop monitor */
|
||||
export interface CycleCheckResult {
|
||||
/** Whether the threshold has been reached */
|
||||
triggered: boolean;
|
||||
/** Current number of completed cycles */
|
||||
cycleCount: number;
|
||||
/** The loop monitor config that was triggered (if triggered) */
|
||||
monitor?: LoopMonitorConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks movement execution history and detects cyclic patterns
|
||||
* as defined by loop_monitors configuration.
|
||||
*/
|
||||
export class CycleDetector {
|
||||
/** Movement execution history (names in order) */
|
||||
private history: string[] = [];
|
||||
private monitors: LoopMonitorConfig[];
|
||||
|
||||
constructor(monitors: LoopMonitorConfig[] = []) {
|
||||
this.monitors = monitors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a movement completion and check if any cycle threshold is reached.
|
||||
*
|
||||
* The detection logic works as follows:
|
||||
* 1. The movement name is appended to the history
|
||||
* 2. For each monitor, we check if the cycle pattern has been completed
|
||||
* by looking at the tail of the history
|
||||
* 3. A cycle is "completed" when the last N entries in history match
|
||||
* the cycle pattern repeated `threshold` times
|
||||
*
|
||||
* @param movementName The name of the movement that just completed
|
||||
* @returns CycleCheckResult indicating if any monitor was triggered
|
||||
*/
|
||||
recordAndCheck(movementName: string): CycleCheckResult {
|
||||
this.history.push(movementName);
|
||||
|
||||
for (const monitor of this.monitors) {
|
||||
const result = this.checkMonitor(monitor);
|
||||
if (result.triggered) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { triggered: false, cycleCount: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a single monitor against the current history.
|
||||
*
|
||||
* A cycle is detected when the last element of the history matches the
|
||||
* last element of the cycle, and looking backwards we can find exactly
|
||||
* `threshold` complete cycles.
|
||||
*/
|
||||
private checkMonitor(monitor: LoopMonitorConfig): CycleCheckResult {
|
||||
const { cycle, threshold } = monitor;
|
||||
const cycleLen = cycle.length;
|
||||
|
||||
// The cycle's last step must match the most recent movement
|
||||
const lastStep = cycle[cycleLen - 1];
|
||||
if (this.history[this.history.length - 1] !== lastStep) {
|
||||
return { triggered: false, cycleCount: 0 };
|
||||
}
|
||||
|
||||
// Need at least threshold * cycleLen entries to check
|
||||
const requiredLen = threshold * cycleLen;
|
||||
if (this.history.length < requiredLen) {
|
||||
return { triggered: false, cycleCount: 0 };
|
||||
}
|
||||
|
||||
// Count complete cycles from the end of history backwards
|
||||
let cycleCount = 0;
|
||||
let pos = this.history.length;
|
||||
|
||||
while (pos >= cycleLen) {
|
||||
// Check if the last cycleLen entries match the cycle pattern
|
||||
let matches = true;
|
||||
for (let i = 0; i < cycleLen; i++) {
|
||||
if (this.history[pos - cycleLen + i] !== cycle[i]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
cycleCount++;
|
||||
pos -= cycleLen;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cycleCount >= threshold) {
|
||||
return { triggered: true, cycleCount, monitor };
|
||||
}
|
||||
|
||||
return { triggered: false, cycleCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the history after a judge intervention.
|
||||
* This prevents the same cycle from immediately triggering again.
|
||||
*/
|
||||
reset(): void {
|
||||
this.history = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current movement history (for debugging/testing).
|
||||
*/
|
||||
getHistory(): readonly string[] {
|
||||
return this.history;
|
||||
}
|
||||
}
|
||||
@ -9,3 +9,5 @@ export { MovementExecutor } from './MovementExecutor.js';
|
||||
export type { MovementExecutorDeps } from './MovementExecutor.js';
|
||||
export { ParallelRunner } from './ParallelRunner.js';
|
||||
export { OptionsBuilder } from './OptionsBuilder.js';
|
||||
export { CycleDetector } from './cycle-detector.js';
|
||||
export type { CycleCheckResult } from './cycle-detector.js';
|
||||
|
||||
@ -35,6 +35,9 @@ export { determineNextMovementByRules, extractBlockedPrompt } from './engine/tra
|
||||
// Loop detection (engine/)
|
||||
export { LoopDetector } from './engine/loop-detector.js';
|
||||
|
||||
// Cycle detection (engine/)
|
||||
export { CycleDetector, type CycleCheckResult } from './engine/cycle-detector.js';
|
||||
|
||||
// State management (engine/)
|
||||
export {
|
||||
createInitialState,
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { PieceMovement, AgentResponse, PieceState, Language } from '../models/types.js';
|
||||
import type { PieceMovement, AgentResponse, PieceState, Language, LoopMonitorConfig } from '../models/types.js';
|
||||
|
||||
export type ProviderType = 'claude' | 'codex' | 'mock';
|
||||
|
||||
@ -119,6 +119,7 @@ export interface PieceEvents {
|
||||
'piece:abort': (state: PieceState, reason: string) => void;
|
||||
'iteration:limit': (iteration: number, maxIterations: number) => void;
|
||||
'movement:loop_detected': (step: PieceMovement, consecutiveCount: number) => void;
|
||||
'movement:cycle_detected': (monitor: LoopMonitorConfig, cycleCount: number) => void;
|
||||
}
|
||||
|
||||
/** User input request for blocked state */
|
||||
|
||||
175
src/features/config/deploySkill.ts
Normal file
175
src/features/config/deploySkill.ts
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* takt export-cc — Deploy takt pieces and agents as Claude Code Skill.
|
||||
*
|
||||
* Copies the following to ~/.claude/:
|
||||
* commands/takt.md — /takt command entry point
|
||||
* skills/takt/SKILL.md — Engine overview
|
||||
* skills/takt/references/ — Engine logic + YAML schema
|
||||
* skills/takt/pieces/ — Builtin piece YAML files
|
||||
* skills/takt/agents/ — Builtin agent .md files
|
||||
*
|
||||
* Piece YAML agent paths (../agents/...) work as-is because
|
||||
* the directory structure is mirrored.
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join, dirname, relative } from 'node:path';
|
||||
|
||||
import {
|
||||
getBuiltinPiecesDir,
|
||||
getBuiltinAgentsDir,
|
||||
getLanguage,
|
||||
} from '../../infra/config/index.js';
|
||||
import { getResourcesDir } from '../../infra/resources/index.js';
|
||||
import { confirm } from '../../shared/prompt/index.js';
|
||||
import { header, success, info, warn, blankLine } from '../../shared/ui/index.js';
|
||||
|
||||
/** Files to skip during directory copy */
|
||||
const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']);
|
||||
|
||||
/** Target paths under ~/.claude/ */
|
||||
function getSkillDir(): string {
|
||||
return join(homedir(), '.claude', 'skills', 'takt');
|
||||
}
|
||||
|
||||
function getCommandDir(): string {
|
||||
return join(homedir(), '.claude', 'commands');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy takt skill to Claude Code (~/.claude/).
|
||||
*/
|
||||
export async function deploySkill(): Promise<void> {
|
||||
header('takt export-cc — Deploy to Claude Code');
|
||||
|
||||
const lang = getLanguage();
|
||||
const skillResourcesDir = join(getResourcesDir(), 'skill');
|
||||
const builtinPiecesDir = getBuiltinPiecesDir(lang);
|
||||
const builtinAgentsDir = getBuiltinAgentsDir(lang);
|
||||
const skillDir = getSkillDir();
|
||||
const commandDir = getCommandDir();
|
||||
|
||||
// Verify source directories exist
|
||||
if (!existsSync(skillResourcesDir)) {
|
||||
warn('Skill resources not found. Ensure takt is installed correctly.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if skill already exists and ask for confirmation
|
||||
const skillExists = existsSync(join(skillDir, 'SKILL.md'));
|
||||
if (skillExists) {
|
||||
info('Claude Code Skill が既にインストールされています。');
|
||||
const overwrite = await confirm('上書きしますか?', false);
|
||||
if (!overwrite) {
|
||||
info('キャンセルしました。');
|
||||
return;
|
||||
}
|
||||
blankLine();
|
||||
}
|
||||
|
||||
const copiedFiles: string[] = [];
|
||||
|
||||
// 1. Deploy command file: ~/.claude/commands/takt.md
|
||||
const commandSrc = join(skillResourcesDir, 'takt-command.md');
|
||||
const commandDest = join(commandDir, 'takt.md');
|
||||
copyFile(commandSrc, commandDest, copiedFiles);
|
||||
|
||||
// 2. Deploy SKILL.md
|
||||
const skillSrc = join(skillResourcesDir, 'SKILL.md');
|
||||
const skillDest = join(skillDir, 'SKILL.md');
|
||||
copyFile(skillSrc, skillDest, copiedFiles);
|
||||
|
||||
// 3. Deploy references/ (engine.md, yaml-schema.md)
|
||||
const refsSrcDir = join(skillResourcesDir, 'references');
|
||||
const refsDestDir = join(skillDir, 'references');
|
||||
cleanDir(refsDestDir);
|
||||
copyDirRecursive(refsSrcDir, refsDestDir, copiedFiles);
|
||||
|
||||
// 4. Deploy builtin piece YAMLs → skills/takt/pieces/
|
||||
const piecesDestDir = join(skillDir, 'pieces');
|
||||
cleanDir(piecesDestDir);
|
||||
copyDirRecursive(builtinPiecesDir, piecesDestDir, copiedFiles);
|
||||
|
||||
// 5. Deploy builtin agent .md files → skills/takt/agents/
|
||||
const agentsDestDir = join(skillDir, 'agents');
|
||||
cleanDir(agentsDestDir);
|
||||
copyDirRecursive(builtinAgentsDir, agentsDestDir, copiedFiles);
|
||||
|
||||
// Report results
|
||||
blankLine();
|
||||
if (copiedFiles.length > 0) {
|
||||
success(`${copiedFiles.length} ファイルをデプロイしました。`);
|
||||
blankLine();
|
||||
|
||||
// Show summary by category
|
||||
const skillBase = join(homedir(), '.claude');
|
||||
const commandFiles = copiedFiles.filter((f) => f.startsWith(commandDir));
|
||||
const skillFiles = copiedFiles.filter(
|
||||
(f) => f.startsWith(skillDir) && !f.includes('/pieces/') && !f.includes('/agents/'),
|
||||
);
|
||||
const pieceFiles = copiedFiles.filter((f) => f.includes('/pieces/'));
|
||||
const agentFiles = copiedFiles.filter((f) => f.includes('/agents/'));
|
||||
|
||||
if (commandFiles.length > 0) {
|
||||
info(` コマンド: ${commandFiles.length} ファイル`);
|
||||
for (const f of commandFiles) {
|
||||
info(` ${relative(skillBase, f)}`);
|
||||
}
|
||||
}
|
||||
if (skillFiles.length > 0) {
|
||||
info(` スキル: ${skillFiles.length} ファイル`);
|
||||
for (const f of skillFiles) {
|
||||
info(` ${relative(skillBase, f)}`);
|
||||
}
|
||||
}
|
||||
if (pieceFiles.length > 0) {
|
||||
info(` ピース: ${pieceFiles.length} ファイル`);
|
||||
}
|
||||
if (agentFiles.length > 0) {
|
||||
info(` エージェント: ${agentFiles.length} ファイル`);
|
||||
}
|
||||
|
||||
blankLine();
|
||||
info('使い方: /takt <piece-name> <task>');
|
||||
info('例: /takt passthrough "Hello World テスト"');
|
||||
} else {
|
||||
info('デプロイするファイルがありませんでした。');
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove a directory and all its contents so stale files don't persist across deploys. */
|
||||
function cleanDir(dir: string): void {
|
||||
if (existsSync(dir)) {
|
||||
rmSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy a single file, creating parent directories as needed. */
|
||||
function copyFile(src: string, dest: string, copiedFiles: string[]): void {
|
||||
if (!existsSync(src)) return;
|
||||
mkdirSync(dirname(dest), { recursive: true });
|
||||
writeFileSync(dest, readFileSync(src));
|
||||
copiedFiles.push(dest);
|
||||
}
|
||||
|
||||
/** Recursively copy directory contents, always overwriting. */
|
||||
function copyDirRecursive(srcDir: string, destDir: string, copiedFiles: string[]): void {
|
||||
if (!existsSync(srcDir)) return;
|
||||
mkdirSync(destDir, { recursive: true });
|
||||
|
||||
for (const entry of readdirSync(srcDir)) {
|
||||
if (SKIP_FILES.has(entry)) continue;
|
||||
|
||||
const srcPath = join(srcDir, entry);
|
||||
const destPath = join(destDir, entry);
|
||||
const stat = statSync(srcPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
copyDirRecursive(srcPath, destPath, copiedFiles);
|
||||
} else {
|
||||
writeFileSync(destPath, readFileSync(srcPath));
|
||||
copiedFiles.push(destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
/**
|
||||
* /eject command implementation
|
||||
*
|
||||
* Copies a builtin piece (and its agents) to ~/.takt/ for user customization.
|
||||
* Once ejected, the user copy takes priority over the builtin version.
|
||||
* Copies a builtin piece (and its agents) for user customization.
|
||||
* Directory structure is mirrored so relative agent paths work as-is.
|
||||
*
|
||||
* Default target: project-local (.takt/)
|
||||
* With --global: user global (~/.takt/)
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
@ -10,18 +13,25 @@ import { join, dirname } from 'node:path';
|
||||
import {
|
||||
getGlobalPiecesDir,
|
||||
getGlobalAgentsDir,
|
||||
getProjectPiecesDir,
|
||||
getProjectAgentsDir,
|
||||
getBuiltinPiecesDir,
|
||||
getBuiltinAgentsDir,
|
||||
getLanguage,
|
||||
} from '../../infra/config/index.js';
|
||||
import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js';
|
||||
|
||||
export interface EjectOptions {
|
||||
global?: boolean;
|
||||
projectDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eject a builtin piece to user space for customization.
|
||||
* Copies the piece YAML and related agent .md files to ~/.takt/.
|
||||
* Agent paths in the ejected piece are rewritten from ../agents/ to ~/.takt/agents/.
|
||||
* Eject a builtin piece to project or global space for customization.
|
||||
* Copies the piece YAML and related agent .md files, preserving
|
||||
* the directory structure so relative paths continue to work.
|
||||
*/
|
||||
export async function ejectBuiltin(name?: string): Promise<void> {
|
||||
export async function ejectBuiltin(name?: string, options: EjectOptions = {}): Promise<void> {
|
||||
header('Eject Builtin');
|
||||
|
||||
const lang = getLanguage();
|
||||
@ -29,7 +39,7 @@ export async function ejectBuiltin(name?: string): Promise<void> {
|
||||
|
||||
if (!name) {
|
||||
// List available builtins
|
||||
listAvailableBuiltins(builtinPiecesDir);
|
||||
listAvailableBuiltins(builtinPiecesDir, options.global);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -40,24 +50,24 @@ export async function ejectBuiltin(name?: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const userPiecesDir = getGlobalPiecesDir();
|
||||
const userAgentsDir = getGlobalAgentsDir();
|
||||
const projectDir = options.projectDir || process.cwd();
|
||||
const targetPiecesDir = options.global ? getGlobalPiecesDir() : getProjectPiecesDir(projectDir);
|
||||
const targetAgentsDir = options.global ? getGlobalAgentsDir() : getProjectAgentsDir(projectDir);
|
||||
const builtinAgentsDir = getBuiltinAgentsDir(lang);
|
||||
const targetLabel = options.global ? 'global (~/.takt/)' : 'project (.takt/)';
|
||||
|
||||
// Copy piece YAML (rewrite agent paths)
|
||||
const pieceDest = join(userPiecesDir, `${name}.yaml`);
|
||||
info(`Ejecting to ${targetLabel}`);
|
||||
blankLine();
|
||||
|
||||
// Copy piece YAML as-is (no path rewriting — directory structure mirrors builtin)
|
||||
const pieceDest = join(targetPiecesDir, `${name}.yaml`);
|
||||
if (existsSync(pieceDest)) {
|
||||
warn(`User piece already exists: ${pieceDest}`);
|
||||
warn('Skipping piece copy (user version takes priority).');
|
||||
} else {
|
||||
mkdirSync(dirname(pieceDest), { recursive: true });
|
||||
const content = readFileSync(builtinPath, 'utf-8');
|
||||
// Rewrite relative agent paths to ~/.takt/agents/
|
||||
const rewritten = content.replace(
|
||||
/agent:\s*\.\.\/agents\//g,
|
||||
'agent: ~/.takt/agents/',
|
||||
);
|
||||
writeFileSync(pieceDest, rewritten, 'utf-8');
|
||||
writeFileSync(pieceDest, content, 'utf-8');
|
||||
success(`Ejected piece: ${pieceDest}`);
|
||||
}
|
||||
|
||||
@ -67,7 +77,7 @@ export async function ejectBuiltin(name?: string): Promise<void> {
|
||||
|
||||
for (const relPath of agentPaths) {
|
||||
const srcPath = join(builtinAgentsDir, relPath);
|
||||
const destPath = join(userAgentsDir, relPath);
|
||||
const destPath = join(targetAgentsDir, relPath);
|
||||
|
||||
if (!existsSync(srcPath)) continue;
|
||||
|
||||
@ -88,7 +98,7 @@ export async function ejectBuiltin(name?: string): Promise<void> {
|
||||
}
|
||||
|
||||
/** List available builtin pieces for ejection */
|
||||
function listAvailableBuiltins(builtinPiecesDir: string): void {
|
||||
function listAvailableBuiltins(builtinPiecesDir: string, isGlobal?: boolean): void {
|
||||
if (!existsSync(builtinPiecesDir)) {
|
||||
warn('No builtin pieces found.');
|
||||
return;
|
||||
@ -106,7 +116,11 @@ function listAvailableBuiltins(builtinPiecesDir: string): void {
|
||||
}
|
||||
|
||||
blankLine();
|
||||
info('Usage: takt eject {name}');
|
||||
const globalFlag = isGlobal ? ' --global' : '';
|
||||
info(`Usage: takt eject {name}${globalFlag}`);
|
||||
if (!isGlobal) {
|
||||
info(' Add --global to eject to ~/.takt/ instead of .takt/');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,3 +5,5 @@
|
||||
export { switchPiece } from './switchPiece.js';
|
||||
export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './switchConfig.js';
|
||||
export { ejectBuiltin } from './ejectBuiltin.js';
|
||||
export { resetCategoriesToDefault } from './resetCategories.js';
|
||||
export { deploySkill } from './deploySkill.js';
|
||||
|
||||
18
src/features/config/resetCategories.ts
Normal file
18
src/features/config/resetCategories.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Reset piece categories to builtin defaults.
|
||||
*/
|
||||
|
||||
import { getDefaultCategoriesPath } from '../../infra/config/loaders/pieceCategories.js';
|
||||
import { resetPieceCategories, getPieceCategoriesPath } from '../../infra/config/global/pieceCategories.js';
|
||||
import { header, success, info } from '../../shared/ui/index.js';
|
||||
|
||||
export async function resetCategoriesToDefault(): Promise<void> {
|
||||
header('Reset Categories');
|
||||
|
||||
const defaultPath = getDefaultCategoriesPath();
|
||||
resetPieceCategories(defaultPath);
|
||||
|
||||
const userPath = getPieceCategoriesPath();
|
||||
success('Categories reset to builtin defaults.');
|
||||
info(` ${userPath}`);
|
||||
}
|
||||
@ -28,7 +28,7 @@ export async function switchPiece(cwd: string, pieceName?: string): Promise<bool
|
||||
const current = getCurrentPiece(cwd);
|
||||
info(`Current piece: ${current}`);
|
||||
|
||||
const categoryConfig = getPieceCategories(cwd);
|
||||
const categoryConfig = getPieceCategories();
|
||||
let selected: string | null;
|
||||
if (categoryConfig) {
|
||||
const allPieces = loadAllPiecesWithSources(cwd);
|
||||
|
||||
@ -2,4 +2,9 @@
|
||||
* Interactive mode commands.
|
||||
*/
|
||||
|
||||
export { interactiveMode, type PieceContext, type InteractiveModeResult } from './interactive.js';
|
||||
export {
|
||||
interactiveMode,
|
||||
type PieceContext,
|
||||
type InteractiveModeResult,
|
||||
type InteractiveModeAction,
|
||||
} from './interactive.js';
|
||||
|
||||
@ -38,7 +38,13 @@ interface InteractiveUIText {
|
||||
summarizeFailed: string;
|
||||
continuePrompt: string;
|
||||
proposed: string;
|
||||
confirm: string;
|
||||
actionPrompt: string;
|
||||
actions: {
|
||||
execute: string;
|
||||
createIssue: string;
|
||||
saveTask: string;
|
||||
continue: string;
|
||||
};
|
||||
cancelled: string;
|
||||
playNoTask: string;
|
||||
}
|
||||
@ -149,15 +155,23 @@ function buildSummaryPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
async function confirmTask(task: string, message: string, confirmLabel: string, yesLabel: string, noLabel: string): Promise<boolean> {
|
||||
type PostSummaryAction = InteractiveModeAction | 'continue';
|
||||
|
||||
async function selectPostSummaryAction(
|
||||
task: string,
|
||||
proposedLabel: string,
|
||||
ui: InteractiveUIText,
|
||||
): Promise<PostSummaryAction | null> {
|
||||
blankLine();
|
||||
info(message);
|
||||
info(proposedLabel);
|
||||
console.log(task);
|
||||
const decision = await selectOption(confirmLabel, [
|
||||
{ label: yesLabel, value: 'yes' },
|
||||
{ label: noLabel, value: 'no' },
|
||||
|
||||
return selectOption<PostSummaryAction>(ui.actionPrompt, [
|
||||
{ label: ui.actions.execute, value: 'execute' },
|
||||
{ label: ui.actions.createIssue, value: 'create_issue' },
|
||||
{ label: ui.actions.saveTask, value: 'save_task' },
|
||||
{ label: ui.actions.continue, value: 'continue' },
|
||||
]);
|
||||
return decision === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -221,10 +235,12 @@ async function callAI(
|
||||
return { content: response.content, sessionId: response.sessionId, success };
|
||||
}
|
||||
|
||||
export type InteractiveModeAction = 'execute' | 'save_task' | 'create_issue' | 'cancel';
|
||||
|
||||
export interface InteractiveModeResult {
|
||||
/** Whether the user confirmed with /go */
|
||||
confirmed: boolean;
|
||||
/** The assembled task text (only meaningful when confirmed=true) */
|
||||
/** The action selected by the user */
|
||||
action: InteractiveModeAction;
|
||||
/** The assembled task text (only meaningful when action is not 'cancel') */
|
||||
task: string;
|
||||
}
|
||||
|
||||
@ -338,7 +354,7 @@ export async function interactiveMode(
|
||||
if (!result.success) {
|
||||
error(result.content);
|
||||
blankLine();
|
||||
return { confirmed: false, task: '' };
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
history.push({ role: 'assistant', content: result.content });
|
||||
blankLine();
|
||||
@ -354,7 +370,7 @@ export async function interactiveMode(
|
||||
if (input === null) {
|
||||
blankLine();
|
||||
info('Cancelled');
|
||||
return { confirmed: false, task: '' };
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
@ -372,7 +388,7 @@ export async function interactiveMode(
|
||||
continue;
|
||||
}
|
||||
log.info('Play command', { task });
|
||||
return { confirmed: true, task };
|
||||
return { action: 'execute', task };
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('/go')) {
|
||||
@ -400,27 +416,21 @@ export async function interactiveMode(
|
||||
if (!summaryResult.success) {
|
||||
error(summaryResult.content);
|
||||
blankLine();
|
||||
return { confirmed: false, task: '' };
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
const task = summaryResult.content.trim();
|
||||
const confirmed = await confirmTask(
|
||||
task,
|
||||
prompts.ui.proposed,
|
||||
prompts.ui.confirm,
|
||||
lang === 'ja' ? 'はい' : 'Yes',
|
||||
lang === 'ja' ? 'いいえ' : 'No',
|
||||
);
|
||||
if (!confirmed) {
|
||||
const selectedAction = await selectPostSummaryAction(task, prompts.ui.proposed, prompts.ui);
|
||||
if (selectedAction === 'continue' || selectedAction === null) {
|
||||
info(prompts.ui.continuePrompt);
|
||||
continue;
|
||||
}
|
||||
log.info('Interactive mode confirmed', { messageCount: history.length });
|
||||
return { confirmed: true, task };
|
||||
log.info('Interactive mode action selected', { action: selectedAction, messageCount: history.length });
|
||||
return { action: selectedAction, task };
|
||||
}
|
||||
|
||||
if (trimmed === '/cancel') {
|
||||
info(prompts.ui.cancelled);
|
||||
return { confirmed: false, task: '' };
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
// Regular input — send to AI
|
||||
@ -436,7 +446,7 @@ export async function interactiveMode(
|
||||
error(result.content);
|
||||
blankLine();
|
||||
history.pop();
|
||||
return { confirmed: false, task: '' };
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
history.push({ role: 'assistant', content: result.content });
|
||||
blankLine();
|
||||
|
||||
@ -16,8 +16,6 @@ import {
|
||||
type PieceCategoryNode,
|
||||
type CategorizedPieces,
|
||||
type MissingPiece,
|
||||
type PieceSource,
|
||||
type PieceWithSource,
|
||||
} from '../../infra/config/index.js';
|
||||
|
||||
/** Top-level selection item: either a piece or a category containing pieces */
|
||||
@ -280,63 +278,21 @@ async function selectPieceFromCategoryTree(
|
||||
}
|
||||
}
|
||||
|
||||
function countPiecesIncludingCategories(
|
||||
categories: PieceCategoryNode[],
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
sourceFilter: PieceSource,
|
||||
): number {
|
||||
const categorizedPieces = new Set<string>();
|
||||
const visit = (nodes: PieceCategoryNode[]): void => {
|
||||
for (const node of nodes) {
|
||||
for (const w of node.pieces) {
|
||||
categorizedPieces.add(w);
|
||||
}
|
||||
if (node.children.length > 0) {
|
||||
visit(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(categories);
|
||||
|
||||
let count = 0;
|
||||
for (const [, { source }] of allPieces) {
|
||||
if (source === sourceFilter) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const CURRENT_PIECE_VALUE = '__current__';
|
||||
const CUSTOM_UNCATEGORIZED_VALUE = '__custom_uncategorized__';
|
||||
const BUILTIN_SOURCE_VALUE = '__builtin__';
|
||||
const CUSTOM_CATEGORY_PREFIX = '__custom_category__:';
|
||||
|
||||
type TopLevelSelection =
|
||||
| { type: 'current' }
|
||||
| { type: 'piece'; name: string }
|
||||
| { type: 'custom_category'; node: PieceCategoryNode }
|
||||
| { type: 'custom_uncategorized' }
|
||||
| { type: 'builtin' };
|
||||
| { type: 'category'; node: PieceCategoryNode };
|
||||
|
||||
async function selectTopLevelPieceOption(
|
||||
categorized: CategorizedPieces,
|
||||
currentPiece: string,
|
||||
): Promise<TopLevelSelection | null> {
|
||||
const uncategorizedCustom = getRootLevelPieces(
|
||||
categorized.categories,
|
||||
categorized.allPieces,
|
||||
'user'
|
||||
);
|
||||
const builtinCount = countPiecesIncludingCategories(
|
||||
categorized.builtinCategories,
|
||||
categorized.allPieces,
|
||||
'builtin'
|
||||
);
|
||||
|
||||
const buildOptions = (): SelectOptionItem<string>[] => {
|
||||
const options: SelectOptionItem<string>[] = [];
|
||||
const bookmarkedPieces = getBookmarkedPieces(); // Get fresh bookmarks on every build
|
||||
const bookmarkedPieces = getBookmarkedPieces();
|
||||
|
||||
// 1. Current piece
|
||||
if (currentPiece) {
|
||||
@ -348,14 +304,14 @@ async function selectTopLevelPieceOption(
|
||||
|
||||
// 2. Bookmarked pieces (individual items)
|
||||
for (const pieceName of bookmarkedPieces) {
|
||||
if (pieceName === currentPiece) continue; // Skip if already shown as current
|
||||
if (pieceName === currentPiece) continue;
|
||||
options.push({
|
||||
label: `🎼 ${pieceName} [*]`,
|
||||
value: pieceName,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. User-defined categories
|
||||
// 3. Categories
|
||||
for (const category of categorized.categories) {
|
||||
options.push({
|
||||
label: `📁 ${category.name}/`,
|
||||
@ -363,22 +319,6 @@ async function selectTopLevelPieceOption(
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Builtin pieces
|
||||
if (builtinCount > 0) {
|
||||
options.push({
|
||||
label: `📂 Builtin/ (${builtinCount})`,
|
||||
value: BUILTIN_SOURCE_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Uncategorized custom pieces
|
||||
if (uncategorizedCustom.length > 0) {
|
||||
options.push({
|
||||
label: `📂 Custom/ (${uncategorizedCustom.length})`,
|
||||
value: CUSTOM_UNCATEGORIZED_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
@ -386,12 +326,8 @@ async function selectTopLevelPieceOption(
|
||||
|
||||
const result = await selectOption<string>('Select piece:', buildOptions(), {
|
||||
onKeyPress: (key: string, value: string): SelectOptionItem<string>[] | null => {
|
||||
// Don't handle bookmark keys for special values
|
||||
if (value === CURRENT_PIECE_VALUE ||
|
||||
value === CUSTOM_UNCATEGORIZED_VALUE ||
|
||||
value === BUILTIN_SOURCE_VALUE ||
|
||||
value.startsWith(CUSTOM_CATEGORY_PREFIX)) {
|
||||
return null; // Delegate to default handler
|
||||
if (value === CURRENT_PIECE_VALUE || value.startsWith(CUSTOM_CATEGORY_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === 'b') {
|
||||
@ -404,7 +340,7 @@ async function selectTopLevelPieceOption(
|
||||
return buildOptions();
|
||||
}
|
||||
|
||||
return null; // Delegate to default handler
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
@ -414,52 +350,16 @@ async function selectTopLevelPieceOption(
|
||||
return { type: 'current' };
|
||||
}
|
||||
|
||||
if (result === CUSTOM_UNCATEGORIZED_VALUE) {
|
||||
return { type: 'custom_uncategorized' };
|
||||
}
|
||||
|
||||
if (result === BUILTIN_SOURCE_VALUE) {
|
||||
return { type: 'builtin' };
|
||||
}
|
||||
|
||||
if (result.startsWith(CUSTOM_CATEGORY_PREFIX)) {
|
||||
const categoryName = result.slice(CUSTOM_CATEGORY_PREFIX.length);
|
||||
const node = categorized.categories.find(c => c.name === categoryName);
|
||||
if (!node) return null;
|
||||
return { type: 'custom_category', node };
|
||||
return { type: 'category', node };
|
||||
}
|
||||
|
||||
// Direct piece selection (bookmarked or other)
|
||||
return { type: 'piece', name: result };
|
||||
}
|
||||
|
||||
function getRootLevelPieces(
|
||||
categories: PieceCategoryNode[],
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
sourceFilter: PieceSource,
|
||||
): string[] {
|
||||
const categorizedPieces = new Set<string>();
|
||||
const visit = (nodes: PieceCategoryNode[]): void => {
|
||||
for (const node of nodes) {
|
||||
for (const w of node.pieces) {
|
||||
categorizedPieces.add(w);
|
||||
}
|
||||
if (node.children.length > 0) {
|
||||
visit(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(categories);
|
||||
|
||||
const rootPieces: string[] = [];
|
||||
for (const [name, { source }] of allPieces) {
|
||||
if (source === sourceFilter && !categorizedPieces.has(name)) {
|
||||
rootPieces.push(name);
|
||||
}
|
||||
}
|
||||
return rootPieces.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select piece from categorized pieces (hierarchical UI).
|
||||
*/
|
||||
@ -469,91 +369,20 @@ export async function selectPieceFromCategorizedPieces(
|
||||
): Promise<string | null> {
|
||||
while (true) {
|
||||
const selection = await selectTopLevelPieceOption(categorized, currentPiece);
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
if (!selection) return null;
|
||||
|
||||
// 1. Current piece selected
|
||||
if (selection.type === 'current') {
|
||||
return currentPiece;
|
||||
}
|
||||
if (selection.type === 'current') return currentPiece;
|
||||
|
||||
// 2. Direct piece selected (e.g., bookmarked piece)
|
||||
if (selection.type === 'piece') {
|
||||
return selection.name;
|
||||
}
|
||||
if (selection.type === 'piece') return selection.name;
|
||||
|
||||
// 3. User-defined category selected
|
||||
if (selection.type === 'custom_category') {
|
||||
if (selection.type === 'category') {
|
||||
const piece = await selectPieceFromCategoryTree(
|
||||
[selection.node],
|
||||
selection.node.children,
|
||||
currentPiece,
|
||||
true,
|
||||
selection.node.pieces
|
||||
selection.node.pieces,
|
||||
);
|
||||
if (piece) {
|
||||
return piece;
|
||||
}
|
||||
// null → go back to top-level selection
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Builtin pieces selected
|
||||
if (selection.type === 'builtin') {
|
||||
const rootPieces = getRootLevelPieces(
|
||||
categorized.builtinCategories,
|
||||
categorized.allPieces,
|
||||
'builtin'
|
||||
);
|
||||
|
||||
const piece = await selectPieceFromCategoryTree(
|
||||
categorized.builtinCategories,
|
||||
currentPiece,
|
||||
true,
|
||||
rootPieces
|
||||
);
|
||||
if (piece) {
|
||||
return piece;
|
||||
}
|
||||
// null → go back to top-level selection
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Custom uncategorized pieces selected
|
||||
if (selection.type === 'custom_uncategorized') {
|
||||
const uncategorizedCustom = getRootLevelPieces(
|
||||
categorized.categories,
|
||||
categorized.allPieces,
|
||||
'user'
|
||||
);
|
||||
|
||||
const baseOptions: SelectionOption[] = uncategorizedCustom.map((name) => ({
|
||||
label: name === currentPiece ? `🎼 ${name} (current)` : `🎼 ${name}`,
|
||||
value: name,
|
||||
}));
|
||||
|
||||
const buildFlatOptions = (): SelectionOption[] =>
|
||||
applyBookmarks(baseOptions, getBookmarkedPieces());
|
||||
|
||||
const piece = await selectOption<string>('Select piece:', buildFlatOptions(), {
|
||||
cancelLabel: '← Go back',
|
||||
onKeyPress: (key: string, value: string): SelectOptionItem<string>[] | null => {
|
||||
if (key === 'b') {
|
||||
addBookmark(value);
|
||||
return buildFlatOptions();
|
||||
}
|
||||
if (key === 'r') {
|
||||
removeBookmark(value);
|
||||
return buildFlatOptions();
|
||||
}
|
||||
return null; // Delegate to default handler
|
||||
},
|
||||
});
|
||||
|
||||
if (piece) {
|
||||
return piece;
|
||||
}
|
||||
// null → go back to top-level selection
|
||||
if (piece) return piece;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,12 +9,12 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { promptInput, confirm } from '../../../shared/prompt/index.js';
|
||||
import { success, info } from '../../../shared/ui/index.js';
|
||||
import { success, info, error } from '../../../shared/ui/index.js';
|
||||
import { summarizeTaskName, type TaskFileData } from '../../../infra/task/index.js';
|
||||
import { getPieceDescription } from '../../../infra/config/index.js';
|
||||
import { determinePiece } from '../execute/selectAndExecute.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/index.js';
|
||||
import { isIssueReference, resolveIssueTask, parseIssueNumbers, createIssue } from '../../../infra/github/index.js';
|
||||
import { interactiveMode } from '../../interactive/index.js';
|
||||
|
||||
const log = createLogger('add-task');
|
||||
@ -34,6 +34,74 @@ async function generateFilename(tasksDir: string, taskContent: string, cwd: stri
|
||||
return filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a task file to .takt/tasks/ with YAML format.
|
||||
*
|
||||
* Common logic extracted from addTask(). Used by both addTask()
|
||||
* and saveTaskFromInteractive().
|
||||
*/
|
||||
export async function saveTaskFile(
|
||||
cwd: string,
|
||||
taskContent: string,
|
||||
options?: { piece?: string; issue?: number; worktree?: boolean | string; branch?: string },
|
||||
): Promise<string> {
|
||||
const tasksDir = path.join(cwd, '.takt', 'tasks');
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
|
||||
const firstLine = taskContent.split('\n')[0] || taskContent;
|
||||
const filename = await generateFilename(tasksDir, firstLine, cwd);
|
||||
|
||||
const taskData: TaskFileData = {
|
||||
task: taskContent,
|
||||
...(options?.worktree !== undefined && { worktree: options.worktree }),
|
||||
...(options?.branch && { branch: options.branch }),
|
||||
...(options?.piece && { piece: options.piece }),
|
||||
...(options?.issue !== undefined && { issue: options.issue }),
|
||||
};
|
||||
|
||||
const filePath = path.join(tasksDir, filename);
|
||||
const yamlContent = stringifyYaml(taskData);
|
||||
fs.writeFileSync(filePath, yamlContent, 'utf-8');
|
||||
|
||||
log.info('Task created', { filePath, taskData });
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GitHub Issue from a task description.
|
||||
*
|
||||
* Extracts the first line as the issue title (truncated to 100 chars),
|
||||
* uses the full task as the body, and displays success/error messages.
|
||||
*/
|
||||
export function createIssueFromTask(task: string): void {
|
||||
info('Creating GitHub Issue...');
|
||||
const firstLine = task.split('\n')[0] || task;
|
||||
const title = firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
|
||||
const issueResult = createIssue({ title, body: task });
|
||||
if (issueResult.success) {
|
||||
success(`Issue created: ${issueResult.url}`);
|
||||
} else {
|
||||
error(`Failed to create issue: ${issueResult.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a task from interactive mode result.
|
||||
* Does not prompt for worktree/branch settings.
|
||||
*/
|
||||
export async function saveTaskFromInteractive(
|
||||
cwd: string,
|
||||
task: string,
|
||||
piece?: string,
|
||||
): Promise<void> {
|
||||
const filePath = await saveTaskFile(cwd, task, { piece });
|
||||
const filename = path.basename(filePath);
|
||||
success(`Task created: ${filename}`);
|
||||
info(` Path: ${filePath}`);
|
||||
if (piece) info(` Piece: ${piece}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* add command handler
|
||||
*
|
||||
@ -82,7 +150,13 @@ export async function addTask(cwd: string, task?: string): Promise<void> {
|
||||
|
||||
// Interactive mode: AI conversation to refine task
|
||||
const result = await interactiveMode(cwd, undefined, pieceContext);
|
||||
if (!result.confirmed) {
|
||||
|
||||
if (result.action === 'create_issue') {
|
||||
createIssueFromTask(result.task);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.action !== 'execute' && result.action !== 'save_task') {
|
||||
info('Cancelled.');
|
||||
return;
|
||||
}
|
||||
@ -91,11 +165,7 @@ export async function addTask(cwd: string, task?: string): Promise<void> {
|
||||
taskContent = result.task;
|
||||
}
|
||||
|
||||
// 3. 要約からファイル名生成
|
||||
const firstLine = taskContent.split('\n')[0] || taskContent;
|
||||
const filename = await generateFilename(tasksDir, firstLine, cwd);
|
||||
|
||||
// 4. ワークツリー/ブランチ設定
|
||||
// 3. ワークツリー/ブランチ設定
|
||||
let worktree: boolean | string | undefined;
|
||||
let branch: string | undefined;
|
||||
|
||||
@ -110,27 +180,15 @@ export async function addTask(cwd: string, task?: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. YAMLファイル作成
|
||||
const taskData: TaskFileData = { task: taskContent };
|
||||
if (worktree !== undefined) {
|
||||
taskData.worktree = worktree;
|
||||
}
|
||||
if (branch) {
|
||||
taskData.branch = branch;
|
||||
}
|
||||
if (piece) {
|
||||
taskData.piece = piece;
|
||||
}
|
||||
if (issueNumber !== undefined) {
|
||||
taskData.issue = issueNumber;
|
||||
}
|
||||
|
||||
const filePath = path.join(tasksDir, filename);
|
||||
const yamlContent = stringifyYaml(taskData);
|
||||
fs.writeFileSync(filePath, yamlContent, 'utf-8');
|
||||
|
||||
log.info('Task created', { filePath, taskData });
|
||||
// 4. YAMLファイル作成
|
||||
const filePath = await saveTaskFile(cwd, taskContent, {
|
||||
piece,
|
||||
issue: issueNumber,
|
||||
worktree,
|
||||
branch,
|
||||
});
|
||||
|
||||
const filename = path.basename(filePath);
|
||||
success(`Task created: ${filename}`);
|
||||
info(` Path: ${filePath}`);
|
||||
if (worktree) {
|
||||
|
||||
@ -58,7 +58,7 @@ async function selectPieceWithDirectoryCategories(cwd: string): Promise<string |
|
||||
* Select a piece interactively with 2-stage category support.
|
||||
*/
|
||||
async function selectPiece(cwd: string): Promise<string | null> {
|
||||
const categoryConfig = getPieceCategories(cwd);
|
||||
const categoryConfig = getPieceCategories();
|
||||
if (categoryConfig) {
|
||||
const current = getCurrentPiece(cwd);
|
||||
const allPieces = loadAllPiecesWithSources(cwd);
|
||||
|
||||
@ -14,7 +14,7 @@ export {
|
||||
type SelectAndExecuteOptions,
|
||||
type WorktreeConfirmationResult,
|
||||
} from './execute/selectAndExecute.js';
|
||||
export { addTask } from './add/index.js';
|
||||
export { addTask, saveTaskFile, saveTaskFromInteractive, createIssueFromTask } from './add/index.js';
|
||||
export { watchTasks } from './watch/index.js';
|
||||
export {
|
||||
listTasks,
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
/**
|
||||
* List tasks command — main entry point.
|
||||
*
|
||||
* Interactive UI for reviewing branch-based task results.
|
||||
* Interactive UI for reviewing branch-based task results,
|
||||
* pending tasks (.takt/tasks/), and failed tasks (.takt/failed/).
|
||||
* Individual actions (merge, delete, instruct, diff) are in taskActions.ts.
|
||||
* Task delete actions are in taskDeleteActions.ts.
|
||||
* Non-interactive mode is in listNonInteractive.ts.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import {
|
||||
detectDefaultBranch,
|
||||
listTaktBranches,
|
||||
buildListItems,
|
||||
detectDefaultBranch,
|
||||
TaskRunner,
|
||||
} from '../../../infra/task/index.js';
|
||||
import type { TaskListItem } from '../../../infra/task/index.js';
|
||||
import { selectOption, confirm } from '../../../shared/prompt/index.js';
|
||||
import { info } from '../../../shared/ui/index.js';
|
||||
import { createLogger } from '../../../shared/utils/index.js';
|
||||
import { info, header, blankLine } from '../../../shared/ui/index.js';
|
||||
import type { TaskExecutionOptions } from '../execute/types.js';
|
||||
import type { BranchListItem } from '../../../infra/task/index.js';
|
||||
import {
|
||||
type ListAction,
|
||||
showFullDiff,
|
||||
@ -25,6 +27,10 @@ import {
|
||||
deleteBranch,
|
||||
instructBranch,
|
||||
} from './taskActions.js';
|
||||
import { deletePendingTask, deleteFailedTask } from './taskDeleteActions.js';
|
||||
import { listTasksNonInteractive, type ListNonInteractiveOptions } from './listNonInteractive.js';
|
||||
|
||||
export type { ListNonInteractiveOptions } from './listNonInteractive.js';
|
||||
|
||||
export {
|
||||
type ListAction,
|
||||
@ -36,100 +42,25 @@ export {
|
||||
instructBranch,
|
||||
} from './taskActions.js';
|
||||
|
||||
const log = createLogger('list-tasks');
|
||||
/** Task action type for the task action selection menu */
|
||||
type TaskAction = 'delete';
|
||||
|
||||
export interface ListNonInteractiveOptions {
|
||||
enabled: boolean;
|
||||
action?: string;
|
||||
branch?: string;
|
||||
format?: string;
|
||||
yes?: boolean;
|
||||
}
|
||||
|
||||
function isValidAction(action: string): action is ListAction {
|
||||
return action === 'diff' || action === 'try' || action === 'merge' || action === 'delete';
|
||||
}
|
||||
|
||||
function printNonInteractiveList(items: BranchListItem[], format?: string): void {
|
||||
const outputFormat = format ?? 'text';
|
||||
if (outputFormat === 'json') {
|
||||
console.log(JSON.stringify(items, null, 2));
|
||||
return;
|
||||
/**
|
||||
* Show task details and prompt for an action.
|
||||
* Returns the selected action, or null if cancelled.
|
||||
*/
|
||||
async function showTaskAndPromptAction(task: TaskListItem): Promise<TaskAction | null> {
|
||||
header(`[${task.kind}] ${task.name}`);
|
||||
info(` Created: ${task.createdAt}`);
|
||||
if (task.content) {
|
||||
info(` ${task.content}`);
|
||||
}
|
||||
blankLine();
|
||||
|
||||
for (const item of items) {
|
||||
const worktreeLabel = item.info.worktreePath ? ' (worktree)' : '';
|
||||
const instruction = item.originalInstruction ? ` - ${item.originalInstruction}` : '';
|
||||
console.log(`${item.info.branch}${worktreeLabel} (${item.filesChanged} files)${instruction}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showDiffStat(projectDir: string, defaultBranch: string, branch: string): void {
|
||||
try {
|
||||
const stat = execFileSync(
|
||||
'git', ['diff', '--stat', `${defaultBranch}...${branch}`],
|
||||
{ cwd: projectDir, encoding: 'utf-8', stdio: 'pipe' },
|
||||
);
|
||||
console.log(stat);
|
||||
} catch {
|
||||
info('Could not generate diff stat');
|
||||
}
|
||||
}
|
||||
|
||||
async function listTasksNonInteractive(
|
||||
cwd: string,
|
||||
_options: TaskExecutionOptions | undefined,
|
||||
nonInteractive: ListNonInteractiveOptions,
|
||||
): Promise<void> {
|
||||
const defaultBranch = detectDefaultBranch(cwd);
|
||||
const branches = listTaktBranches(cwd);
|
||||
|
||||
if (branches.length === 0) {
|
||||
info('No tasks to list.');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = buildListItems(cwd, branches, defaultBranch);
|
||||
|
||||
if (!nonInteractive.action) {
|
||||
printNonInteractiveList(items, nonInteractive.format);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nonInteractive.branch) {
|
||||
info('Missing --branch for non-interactive action.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isValidAction(nonInteractive.action)) {
|
||||
info('Invalid --action. Use one of: diff, try, merge, delete.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const item = items.find((entry) => entry.info.branch === nonInteractive.branch);
|
||||
if (!item) {
|
||||
info(`Branch not found: ${nonInteractive.branch}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
switch (nonInteractive.action) {
|
||||
case 'diff':
|
||||
showDiffStat(cwd, defaultBranch, item.info.branch);
|
||||
return;
|
||||
case 'try':
|
||||
tryMergeBranch(cwd, item);
|
||||
return;
|
||||
case 'merge':
|
||||
mergeBranch(cwd, item);
|
||||
return;
|
||||
case 'delete':
|
||||
if (!nonInteractive.yes) {
|
||||
info('Delete requires --yes in non-interactive mode.');
|
||||
process.exit(1);
|
||||
}
|
||||
deleteBranch(cwd, item);
|
||||
return;
|
||||
}
|
||||
return await selectOption<TaskAction>(
|
||||
`Action for ${task.name}:`,
|
||||
[{ label: 'Delete', value: 'delete', description: 'Remove this task permanently' }],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,39 +71,52 @@ export async function listTasks(
|
||||
options?: TaskExecutionOptions,
|
||||
nonInteractive?: ListNonInteractiveOptions,
|
||||
): Promise<void> {
|
||||
log.info('Starting list-tasks');
|
||||
|
||||
if (nonInteractive?.enabled) {
|
||||
await listTasksNonInteractive(cwd, options, nonInteractive);
|
||||
await listTasksNonInteractive(cwd, nonInteractive);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultBranch = detectDefaultBranch(cwd);
|
||||
let branches = listTaktBranches(cwd);
|
||||
|
||||
if (branches.length === 0) {
|
||||
info('No tasks to list.');
|
||||
return;
|
||||
}
|
||||
const runner = new TaskRunner(cwd);
|
||||
|
||||
// Interactive loop
|
||||
while (branches.length > 0) {
|
||||
while (true) {
|
||||
const branches = listTaktBranches(cwd);
|
||||
const items = buildListItems(cwd, branches, defaultBranch);
|
||||
const pendingTasks = runner.listPendingTaskItems();
|
||||
const failedTasks = runner.listFailedTasks();
|
||||
|
||||
const menuOptions = items.map((item, idx) => {
|
||||
const filesSummary = `${item.filesChanged} file${item.filesChanged !== 1 ? 's' : ''} changed`;
|
||||
const description = item.originalInstruction
|
||||
? `${filesSummary} | ${item.originalInstruction}`
|
||||
: filesSummary;
|
||||
return {
|
||||
label: item.info.branch,
|
||||
value: String(idx),
|
||||
description,
|
||||
};
|
||||
});
|
||||
if (items.length === 0 && pendingTasks.length === 0 && failedTasks.length === 0) {
|
||||
info('No tasks to list.');
|
||||
return;
|
||||
}
|
||||
|
||||
const menuOptions = [
|
||||
...items.map((item, idx) => {
|
||||
const filesSummary = `${item.filesChanged} file${item.filesChanged !== 1 ? 's' : ''} changed`;
|
||||
const description = item.originalInstruction
|
||||
? `${filesSummary} | ${item.originalInstruction}`
|
||||
: filesSummary;
|
||||
return {
|
||||
label: item.info.branch,
|
||||
value: `branch:${idx}`,
|
||||
description,
|
||||
};
|
||||
}),
|
||||
...pendingTasks.map((task, idx) => ({
|
||||
label: `[pending] ${task.name}`,
|
||||
value: `pending:${idx}`,
|
||||
description: task.content,
|
||||
})),
|
||||
...failedTasks.map((task, idx) => ({
|
||||
label: `[failed] ${task.name}`,
|
||||
value: `failed:${idx}`,
|
||||
description: task.content,
|
||||
})),
|
||||
];
|
||||
|
||||
const selected = await selectOption<string>(
|
||||
'List Tasks (Branches)',
|
||||
'List Tasks',
|
||||
menuOptions,
|
||||
);
|
||||
|
||||
@ -180,47 +124,63 @@ export async function listTasks(
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIdx = parseInt(selected, 10);
|
||||
const item = items[selectedIdx];
|
||||
if (!item) continue;
|
||||
const colonIdx = selected.indexOf(':');
|
||||
if (colonIdx === -1) continue;
|
||||
const type = selected.slice(0, colonIdx);
|
||||
const idx = parseInt(selected.slice(colonIdx + 1), 10);
|
||||
if (Number.isNaN(idx)) continue;
|
||||
|
||||
// Action loop: re-show menu after viewing diff
|
||||
let action: ListAction | null;
|
||||
do {
|
||||
action = await showDiffAndPromptAction(cwd, defaultBranch, item);
|
||||
if (type === 'branch') {
|
||||
const item = items[idx];
|
||||
if (!item) continue;
|
||||
|
||||
if (action === 'diff') {
|
||||
showFullDiff(cwd, defaultBranch, item.info.branch);
|
||||
}
|
||||
} while (action === 'diff');
|
||||
// Action loop: re-show menu after viewing diff
|
||||
let action: ListAction | null;
|
||||
do {
|
||||
action = await showDiffAndPromptAction(cwd, defaultBranch, item);
|
||||
|
||||
if (action === null) continue;
|
||||
|
||||
switch (action) {
|
||||
case 'instruct':
|
||||
await instructBranch(cwd, item, options);
|
||||
break;
|
||||
case 'try':
|
||||
tryMergeBranch(cwd, item);
|
||||
break;
|
||||
case 'merge':
|
||||
mergeBranch(cwd, item);
|
||||
break;
|
||||
case 'delete': {
|
||||
const confirmed = await confirm(
|
||||
`Delete ${item.info.branch}? This will discard all changes.`,
|
||||
false,
|
||||
);
|
||||
if (confirmed) {
|
||||
deleteBranch(cwd, item);
|
||||
if (action === 'diff') {
|
||||
showFullDiff(cwd, defaultBranch, item.info.branch);
|
||||
}
|
||||
break;
|
||||
} while (action === 'diff');
|
||||
|
||||
if (action === null) continue;
|
||||
|
||||
switch (action) {
|
||||
case 'instruct':
|
||||
await instructBranch(cwd, item, options);
|
||||
break;
|
||||
case 'try':
|
||||
tryMergeBranch(cwd, item);
|
||||
break;
|
||||
case 'merge':
|
||||
mergeBranch(cwd, item);
|
||||
break;
|
||||
case 'delete': {
|
||||
const confirmed = await confirm(
|
||||
`Delete ${item.info.branch}? This will discard all changes.`,
|
||||
false,
|
||||
);
|
||||
if (confirmed) {
|
||||
deleteBranch(cwd, item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (type === 'pending') {
|
||||
const task = pendingTasks[idx];
|
||||
if (!task) continue;
|
||||
const taskAction = await showTaskAndPromptAction(task);
|
||||
if (taskAction === 'delete') {
|
||||
await deletePendingTask(task);
|
||||
}
|
||||
} else if (type === 'failed') {
|
||||
const task = failedTasks[idx];
|
||||
if (!task) continue;
|
||||
const taskAction = await showTaskAndPromptAction(task);
|
||||
if (taskAction === 'delete') {
|
||||
await deleteFailedTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh branch list after action
|
||||
branches = listTaktBranches(cwd);
|
||||
}
|
||||
|
||||
info('All tasks listed.');
|
||||
}
|
||||
|
||||
140
src/features/tasks/list/listNonInteractive.ts
Normal file
140
src/features/tasks/list/listNonInteractive.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Non-interactive list mode.
|
||||
*
|
||||
* Handles --non-interactive output (text/JSON) and
|
||||
* non-interactive branch actions (--action, --branch).
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import type { TaskListItem, BranchListItem } from '../../../infra/task/index.js';
|
||||
import {
|
||||
detectDefaultBranch,
|
||||
listTaktBranches,
|
||||
buildListItems,
|
||||
TaskRunner,
|
||||
} from '../../../infra/task/index.js';
|
||||
import { info } from '../../../shared/ui/index.js';
|
||||
import {
|
||||
type ListAction,
|
||||
tryMergeBranch,
|
||||
mergeBranch,
|
||||
deleteBranch,
|
||||
} from './taskActions.js';
|
||||
|
||||
export interface ListNonInteractiveOptions {
|
||||
enabled: boolean;
|
||||
action?: string;
|
||||
branch?: string;
|
||||
format?: string;
|
||||
yes?: boolean;
|
||||
}
|
||||
|
||||
function isValidAction(action: string): action is ListAction {
|
||||
return action === 'diff' || action === 'try' || action === 'merge' || action === 'delete';
|
||||
}
|
||||
|
||||
function printNonInteractiveList(
|
||||
items: BranchListItem[],
|
||||
pendingTasks: TaskListItem[],
|
||||
failedTasks: TaskListItem[],
|
||||
format?: string,
|
||||
): void {
|
||||
const outputFormat = format ?? 'text';
|
||||
if (outputFormat === 'json') {
|
||||
// stdout に直接出力(JSON パース用途のため UI ヘルパーを経由しない)
|
||||
console.log(JSON.stringify({
|
||||
branches: items,
|
||||
pendingTasks,
|
||||
failedTasks,
|
||||
}, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const worktreeLabel = item.info.worktreePath ? ' (worktree)' : '';
|
||||
const instruction = item.originalInstruction ? ` - ${item.originalInstruction}` : '';
|
||||
info(`${item.info.branch}${worktreeLabel} (${item.filesChanged} files)${instruction}`);
|
||||
}
|
||||
|
||||
for (const task of pendingTasks) {
|
||||
info(`[pending] ${task.name} - ${task.content}`);
|
||||
}
|
||||
|
||||
for (const task of failedTasks) {
|
||||
info(`[failed] ${task.name} - ${task.content}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showDiffStat(projectDir: string, defaultBranch: string, branch: string): void {
|
||||
try {
|
||||
const stat = execFileSync(
|
||||
'git', ['diff', '--stat', `${defaultBranch}...${branch}`],
|
||||
{ cwd: projectDir, encoding: 'utf-8', stdio: 'pipe' },
|
||||
);
|
||||
info(stat);
|
||||
} catch {
|
||||
info('Could not generate diff stat');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run list-tasks in non-interactive mode.
|
||||
*/
|
||||
export async function listTasksNonInteractive(
|
||||
cwd: string,
|
||||
nonInteractive: ListNonInteractiveOptions,
|
||||
): Promise<void> {
|
||||
const defaultBranch = detectDefaultBranch(cwd);
|
||||
const branches = listTaktBranches(cwd);
|
||||
const runner = new TaskRunner(cwd);
|
||||
const pendingTasks = runner.listPendingTaskItems();
|
||||
const failedTasks = runner.listFailedTasks();
|
||||
|
||||
const items = buildListItems(cwd, branches, defaultBranch);
|
||||
|
||||
if (items.length === 0 && pendingTasks.length === 0 && failedTasks.length === 0) {
|
||||
info('No tasks to list.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nonInteractive.action) {
|
||||
printNonInteractiveList(items, pendingTasks, failedTasks, nonInteractive.format);
|
||||
return;
|
||||
}
|
||||
|
||||
// Branch-targeted action (--branch)
|
||||
if (!nonInteractive.branch) {
|
||||
info('Missing --branch for non-interactive action.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isValidAction(nonInteractive.action)) {
|
||||
info('Invalid --action. Use one of: diff, try, merge, delete.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const item = items.find((entry) => entry.info.branch === nonInteractive.branch);
|
||||
if (!item) {
|
||||
info(`Branch not found: ${nonInteractive.branch}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
switch (nonInteractive.action) {
|
||||
case 'diff':
|
||||
showDiffStat(cwd, defaultBranch, item.info.branch);
|
||||
return;
|
||||
case 'try':
|
||||
tryMergeBranch(cwd, item);
|
||||
return;
|
||||
case 'merge':
|
||||
mergeBranch(cwd, item);
|
||||
return;
|
||||
case 'delete':
|
||||
if (!nonInteractive.yes) {
|
||||
info('Delete requires --yes in non-interactive mode.');
|
||||
process.exit(1);
|
||||
}
|
||||
deleteBranch(cwd, item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -8,17 +8,16 @@
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { rmSync, existsSync, unlinkSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
createTempCloneForBranch,
|
||||
removeClone,
|
||||
removeCloneMeta,
|
||||
cleanupOrphanedClone,
|
||||
} from '../../../infra/task/index.js';
|
||||
import {
|
||||
detectDefaultBranch,
|
||||
type BranchListItem,
|
||||
autoCommitAndPush,
|
||||
type BranchListItem,
|
||||
} from '../../../infra/task/index.js';
|
||||
import { selectOption, promptInput } from '../../../shared/prompt/index.js';
|
||||
import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js';
|
||||
@ -86,7 +85,7 @@ export async function showDiffAndPromptAction(
|
||||
): Promise<ListAction | null> {
|
||||
header(item.info.branch);
|
||||
if (item.originalInstruction) {
|
||||
console.log(chalk.dim(` ${item.originalInstruction}`));
|
||||
info(chalk.dim(` ${item.originalInstruction}`));
|
||||
}
|
||||
blankLine();
|
||||
|
||||
@ -95,7 +94,7 @@ export async function showDiffAndPromptAction(
|
||||
'git', ['diff', '--stat', `${defaultBranch}...${item.info.branch}`],
|
||||
{ cwd, encoding: 'utf-8', stdio: 'pipe' },
|
||||
);
|
||||
console.log(stat);
|
||||
info(stat);
|
||||
} catch {
|
||||
warn('Could not generate diff stat');
|
||||
}
|
||||
@ -362,3 +361,4 @@ export async function instructBranch(
|
||||
removeCloneMeta(projectDir, branch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
src/features/tasks/list/taskDeleteActions.ts
Normal file
54
src/features/tasks/list/taskDeleteActions.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Delete actions for pending and failed tasks.
|
||||
*
|
||||
* Provides interactive deletion (with confirm prompt)
|
||||
* for pending task files and failed task directories.
|
||||
*/
|
||||
|
||||
import { rmSync, unlinkSync } from 'node:fs';
|
||||
import type { TaskListItem } from '../../../infra/task/index.js';
|
||||
import { confirm } from '../../../shared/prompt/index.js';
|
||||
import { success, error as logError } from '../../../shared/ui/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
|
||||
const log = createLogger('list-tasks');
|
||||
|
||||
/**
|
||||
* Delete a pending task file.
|
||||
* Prompts user for confirmation first.
|
||||
*/
|
||||
export async function deletePendingTask(task: TaskListItem): Promise<boolean> {
|
||||
const confirmed = await confirm(`Delete pending task "${task.name}"?`, false);
|
||||
if (!confirmed) return false;
|
||||
try {
|
||||
unlinkSync(task.filePath);
|
||||
} catch (err) {
|
||||
const msg = getErrorMessage(err);
|
||||
logError(`Failed to delete pending task "${task.name}": ${msg}`);
|
||||
log.error('Failed to delete pending task', { name: task.name, filePath: task.filePath, error: msg });
|
||||
return false;
|
||||
}
|
||||
success(`Deleted pending task: ${task.name}`);
|
||||
log.info('Deleted pending task', { name: task.name, filePath: task.filePath });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a failed task directory.
|
||||
* Prompts user for confirmation first.
|
||||
*/
|
||||
export async function deleteFailedTask(task: TaskListItem): Promise<boolean> {
|
||||
const confirmed = await confirm(`Delete failed task "${task.name}" and its logs?`, false);
|
||||
if (!confirmed) return false;
|
||||
try {
|
||||
rmSync(task.filePath, { recursive: true });
|
||||
} catch (err) {
|
||||
const msg = getErrorMessage(err);
|
||||
logError(`Failed to delete failed task "${task.name}": ${msg}`);
|
||||
log.error('Failed to delete failed task', { name: task.name, filePath: task.filePath, error: msg });
|
||||
return false;
|
||||
}
|
||||
success(`Deleted failed task: ${task.name}`);
|
||||
log.info('Deleted failed task', { name: task.name, filePath: task.filePath });
|
||||
return true;
|
||||
}
|
||||
@ -26,12 +26,9 @@ export {
|
||||
} from './bookmarks.js';
|
||||
|
||||
export {
|
||||
getPieceCategoriesConfig,
|
||||
setPieceCategoriesConfig,
|
||||
getShowOthersCategory,
|
||||
setShowOthersCategory,
|
||||
getOthersCategoryName,
|
||||
setOthersCategoryName,
|
||||
getPieceCategoriesPath,
|
||||
ensureUserCategoriesFile,
|
||||
resetPieceCategories,
|
||||
} from './pieceCategories.js';
|
||||
|
||||
export {
|
||||
|
||||
@ -1,27 +1,22 @@
|
||||
/**
|
||||
* Piece categories management (separate from config.yaml)
|
||||
* Piece categories file management.
|
||||
*
|
||||
* Categories are stored in a configurable location (default: ~/.takt/preferences/piece-categories.yaml)
|
||||
* The categories file (~/.takt/preferences/piece-categories.yaml) uses the same
|
||||
* format as the builtin piece-categories.yaml (piece_categories key).
|
||||
* If the file doesn't exist, it's auto-copied from builtin defaults.
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||
import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { getGlobalConfigDir } from '../paths.js';
|
||||
import { loadGlobalConfig } from './globalConfig.js';
|
||||
import type { PieceCategoryConfigNode } from '../../../core/models/index.js';
|
||||
|
||||
interface PieceCategoriesFile {
|
||||
categories?: PieceCategoryConfigNode;
|
||||
show_others_category?: boolean;
|
||||
others_category_name?: string;
|
||||
}
|
||||
|
||||
function getDefaultPieceCategoriesPath(): string {
|
||||
return join(getGlobalConfigDir(), 'preferences', 'piece-categories.yaml');
|
||||
}
|
||||
|
||||
function getPieceCategoriesPath(): string {
|
||||
/** Get the path to the user's piece categories file. */
|
||||
export function getPieceCategoriesPath(): string {
|
||||
try {
|
||||
const config = loadGlobalConfig();
|
||||
if (config.pieceCategoriesFile) {
|
||||
@ -33,70 +28,40 @@ function getPieceCategoriesPath(): string {
|
||||
return getDefaultPieceCategoriesPath();
|
||||
}
|
||||
|
||||
function loadPieceCategoriesFile(): PieceCategoriesFile {
|
||||
const categoriesPath = getPieceCategoriesPath();
|
||||
if (!existsSync(categoriesPath)) {
|
||||
return {};
|
||||
/**
|
||||
* Ensure user categories file exists by copying from builtin defaults.
|
||||
* Returns the path to the user categories file.
|
||||
*/
|
||||
export function ensureUserCategoriesFile(defaultCategoriesPath: string): string {
|
||||
const userPath = getPieceCategoriesPath();
|
||||
if (existsSync(userPath)) {
|
||||
return userPath;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(categoriesPath, 'utf-8');
|
||||
const parsed = parseYaml(content);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return parsed as PieceCategoriesFile;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
if (!existsSync(defaultCategoriesPath)) {
|
||||
throw new Error(`Default categories file not found: ${defaultCategoriesPath}`);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function savePieceCategoriesFile(data: PieceCategoriesFile): void {
|
||||
const categoriesPath = getPieceCategoriesPath();
|
||||
const dir = dirname(categoriesPath);
|
||||
const dir = dirname(userPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const content = stringifyYaml(data, { indent: 2 });
|
||||
writeFileSync(categoriesPath, content, 'utf-8');
|
||||
copyFileSync(defaultCategoriesPath, userPath);
|
||||
return userPath;
|
||||
}
|
||||
|
||||
/** Get piece categories configuration */
|
||||
export function getPieceCategoriesConfig(): PieceCategoryConfigNode | undefined {
|
||||
const data = loadPieceCategoriesFile();
|
||||
return data.categories;
|
||||
}
|
||||
/**
|
||||
* Reset user categories file by overwriting with builtin defaults.
|
||||
*/
|
||||
export function resetPieceCategories(defaultCategoriesPath: string): void {
|
||||
if (!existsSync(defaultCategoriesPath)) {
|
||||
throw new Error(`Default categories file not found: ${defaultCategoriesPath}`);
|
||||
}
|
||||
|
||||
/** Set piece categories configuration */
|
||||
export function setPieceCategoriesConfig(categories: PieceCategoryConfigNode): void {
|
||||
const data = loadPieceCategoriesFile();
|
||||
data.categories = categories;
|
||||
savePieceCategoriesFile(data);
|
||||
}
|
||||
|
||||
/** Get show others category flag */
|
||||
export function getShowOthersCategory(): boolean | undefined {
|
||||
const data = loadPieceCategoriesFile();
|
||||
return data.show_others_category;
|
||||
}
|
||||
|
||||
/** Set show others category flag */
|
||||
export function setShowOthersCategory(show: boolean): void {
|
||||
const data = loadPieceCategoriesFile();
|
||||
data.show_others_category = show;
|
||||
savePieceCategoriesFile(data);
|
||||
}
|
||||
|
||||
/** Get others category name */
|
||||
export function getOthersCategoryName(): string | undefined {
|
||||
const data = loadPieceCategoriesFile();
|
||||
return data.others_category_name;
|
||||
}
|
||||
|
||||
/** Set others category name */
|
||||
export function setOthersCategoryName(name: string): void {
|
||||
const data = loadPieceCategoriesFile();
|
||||
data.others_category_name = name;
|
||||
savePieceCategoriesFile(data);
|
||||
const userPath = getPieceCategoriesPath();
|
||||
const dir = dirname(userPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
copyFileSync(defaultCategoriesPath, userPath);
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ export {
|
||||
|
||||
export {
|
||||
loadDefaultCategories,
|
||||
getDefaultCategoriesPath,
|
||||
getPieceCategories,
|
||||
buildCategorizedPieces,
|
||||
findPieceCategories,
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
/**
|
||||
* Piece category configuration loader and helpers.
|
||||
*
|
||||
* Categories are loaded from a single source: the user's piece-categories.yaml file.
|
||||
* If the file doesn't exist, it's auto-copied from builtin defaults.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import { z } from 'zod/v4';
|
||||
import { getProjectConfigPath } from '../paths.js';
|
||||
import { getLanguage, getBuiltinPiecesEnabled, getDisabledBuiltins } from '../global/globalConfig.js';
|
||||
import {
|
||||
getPieceCategoriesConfig,
|
||||
getShowOthersCategory,
|
||||
getOthersCategoryName,
|
||||
} from '../global/pieceCategories.js';
|
||||
import { ensureUserCategoriesFile } from '../global/pieceCategories.js';
|
||||
import { getLanguageResourcesDir } from '../../resources/index.js';
|
||||
import { listBuiltinPieceNames } from './pieceResolver.js';
|
||||
import type { PieceSource, PieceWithSource } from './pieceResolver.js';
|
||||
import type { PieceWithSource } from './pieceResolver.js';
|
||||
|
||||
const CategoryConfigSchema = z.object({
|
||||
piece_categories: z.record(z.string(), z.unknown()).optional(),
|
||||
@ -37,7 +35,6 @@ export interface CategoryConfig {
|
||||
|
||||
export interface CategorizedPieces {
|
||||
categories: PieceCategoryNode[];
|
||||
builtinCategories: PieceCategoryNode[];
|
||||
allPieces: Map<string, PieceWithSource>;
|
||||
missingPieces: MissingPiece[];
|
||||
}
|
||||
@ -152,33 +149,25 @@ function loadCategoryConfigFromPath(path: string, sourceLabel: string): Category
|
||||
*/
|
||||
export function loadDefaultCategories(): CategoryConfig | null {
|
||||
const lang = getLanguage();
|
||||
const filePath = join(getLanguageResourcesDir(lang), 'default-categories.yaml');
|
||||
const filePath = join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
||||
return loadCategoryConfigFromPath(filePath, filePath);
|
||||
}
|
||||
|
||||
/** Get the path to the builtin default categories file. */
|
||||
export function getDefaultCategoriesPath(): string {
|
||||
const lang = getLanguage();
|
||||
return join(getLanguageResourcesDir(lang), 'piece-categories.yaml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective piece categories configuration.
|
||||
* Priority: user config -> project config -> default categories.
|
||||
* Reads from user file (~/.takt/preferences/piece-categories.yaml).
|
||||
* Auto-copies from builtin defaults if user file doesn't exist.
|
||||
*/
|
||||
export function getPieceCategories(cwd: string): CategoryConfig | null {
|
||||
// Check user config from separate file (~/.takt/piece-categories.yaml)
|
||||
const userCategoriesNode = getPieceCategoriesConfig();
|
||||
if (userCategoriesNode) {
|
||||
const showOthersCategory = getShowOthersCategory() ?? true;
|
||||
const othersCategoryName = getOthersCategoryName() ?? 'Others';
|
||||
return {
|
||||
pieceCategories: parseCategoryTree(userCategoriesNode, 'user config'),
|
||||
showOthersCategory,
|
||||
othersCategoryName,
|
||||
};
|
||||
}
|
||||
|
||||
const projectConfig = loadCategoryConfigFromPath(getProjectConfigPath(cwd), 'project config');
|
||||
if (projectConfig) {
|
||||
return projectConfig;
|
||||
}
|
||||
|
||||
return loadDefaultCategories();
|
||||
export function getPieceCategories(): CategoryConfig | null {
|
||||
const defaultPath = getDefaultCategoriesPath();
|
||||
const userPath = ensureUserCategoriesFile(defaultPath);
|
||||
return loadCategoryConfigFromPath(userPath, userPath);
|
||||
}
|
||||
|
||||
function collectMissingPieces(
|
||||
@ -206,10 +195,9 @@ function collectMissingPieces(
|
||||
return missing;
|
||||
}
|
||||
|
||||
function buildCategoryTreeForSource(
|
||||
function buildCategoryTree(
|
||||
categories: PieceCategoryNode[],
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
sourceFilter: (source: PieceSource) => boolean,
|
||||
categorized: Set<string>,
|
||||
): PieceCategoryNode[] {
|
||||
const result: PieceCategoryNode[] = [];
|
||||
@ -217,14 +205,12 @@ function buildCategoryTreeForSource(
|
||||
for (const node of categories) {
|
||||
const pieces: string[] = [];
|
||||
for (const pieceName of node.pieces) {
|
||||
const entry = allPieces.get(pieceName);
|
||||
if (!entry) continue;
|
||||
if (!sourceFilter(entry.source)) continue;
|
||||
if (!allPieces.has(pieceName)) continue;
|
||||
pieces.push(pieceName);
|
||||
categorized.add(pieceName);
|
||||
}
|
||||
|
||||
const children = buildCategoryTreeForSource(node.children, allPieces, sourceFilter, categorized);
|
||||
const children = buildCategoryTree(node.children, allPieces, categorized);
|
||||
if (pieces.length > 0 || children.length > 0) {
|
||||
result.push({ name: node.name, pieces, children });
|
||||
}
|
||||
@ -237,16 +223,10 @@ function appendOthersCategory(
|
||||
categories: PieceCategoryNode[],
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
categorized: Set<string>,
|
||||
sourceFilter: (source: PieceSource) => boolean,
|
||||
othersCategoryName: string,
|
||||
): PieceCategoryNode[] {
|
||||
if (categories.some((node) => node.name === othersCategoryName)) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
const uncategorized: string[] = [];
|
||||
for (const [pieceName, entry] of allPieces.entries()) {
|
||||
if (!sourceFilter(entry.source)) continue;
|
||||
for (const [pieceName] of allPieces.entries()) {
|
||||
if (categorized.has(pieceName)) continue;
|
||||
uncategorized.push(pieceName);
|
||||
}
|
||||
@ -255,11 +235,23 @@ function appendOthersCategory(
|
||||
return categories;
|
||||
}
|
||||
|
||||
// If a category with the same name already exists, merge uncategorized pieces into it
|
||||
const existingIndex = categories.findIndex((node) => node.name === othersCategoryName);
|
||||
if (existingIndex >= 0) {
|
||||
const existing = categories[existingIndex]!;
|
||||
return categories.map((node, i) =>
|
||||
i === existingIndex
|
||||
? { ...node, pieces: [...existing.pieces, ...uncategorized] }
|
||||
: node,
|
||||
);
|
||||
}
|
||||
|
||||
return [...categories, { name: othersCategoryName, pieces: uncategorized, children: [] }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build categorized pieces map from configuration.
|
||||
* All pieces (user and builtin) are placed in a single category tree.
|
||||
*/
|
||||
export function buildCategorizedPieces(
|
||||
allPieces: Map<string, PieceWithSource>,
|
||||
@ -282,48 +274,19 @@ export function buildCategorizedPieces(
|
||||
ignoreMissing,
|
||||
);
|
||||
|
||||
const isBuiltin = (source: PieceSource): boolean => source === 'builtin';
|
||||
const isCustom = (source: PieceSource): boolean => source !== 'builtin';
|
||||
|
||||
const categorizedCustom = new Set<string>();
|
||||
const categories = buildCategoryTreeForSource(
|
||||
const categorized = new Set<string>();
|
||||
const categories = buildCategoryTree(
|
||||
config.pieceCategories,
|
||||
allPieces,
|
||||
isCustom,
|
||||
categorizedCustom,
|
||||
);
|
||||
|
||||
const categorizedBuiltin = new Set<string>();
|
||||
const builtinCategories = buildCategoryTreeForSource(
|
||||
config.pieceCategories,
|
||||
allPieces,
|
||||
isBuiltin,
|
||||
categorizedBuiltin,
|
||||
categorized,
|
||||
);
|
||||
|
||||
const finalCategories = config.showOthersCategory
|
||||
? appendOthersCategory(
|
||||
categories,
|
||||
allPieces,
|
||||
categorizedCustom,
|
||||
isCustom,
|
||||
config.othersCategoryName,
|
||||
)
|
||||
? appendOthersCategory(categories, allPieces, categorized, config.othersCategoryName)
|
||||
: categories;
|
||||
|
||||
const finalBuiltinCategories = config.showOthersCategory
|
||||
? appendOthersCategory(
|
||||
builtinCategories,
|
||||
allPieces,
|
||||
categorizedBuiltin,
|
||||
isBuiltin,
|
||||
config.othersCategoryName,
|
||||
)
|
||||
: builtinCategories;
|
||||
|
||||
return {
|
||||
categories: finalCategories,
|
||||
builtinCategories: finalBuiltinCategories,
|
||||
allPieces,
|
||||
missingPieces,
|
||||
};
|
||||
|
||||
@ -10,7 +10,7 @@ import { join, dirname, basename } from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
import type { z } from 'zod';
|
||||
import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js';
|
||||
import type { PieceConfig, PieceMovement, PieceRule, ReportConfig, ReportObjectConfig } from '../../../core/models/index.js';
|
||||
import type { PieceConfig, PieceMovement, PieceRule, ReportConfig, ReportObjectConfig, LoopMonitorConfig, LoopMonitorJudge } from '../../../core/models/index.js';
|
||||
|
||||
/** Parsed movement type from Zod schema (replaces `any`) */
|
||||
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
||||
@ -210,6 +210,47 @@ function normalizeStepFromRaw(step: RawStep, pieceDir: string): PieceMovement {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a raw loop monitor judge from YAML into internal format.
|
||||
* Resolves agent paths and instruction_template content paths.
|
||||
*/
|
||||
function normalizeLoopMonitorJudge(
|
||||
raw: { agent?: string; instruction_template?: string; rules: Array<{ condition: string; next: string }> },
|
||||
pieceDir: string,
|
||||
): LoopMonitorJudge {
|
||||
const agentSpec = raw.agent || undefined;
|
||||
|
||||
let agentPath: string | undefined;
|
||||
if (agentSpec) {
|
||||
const resolved = resolveAgentPathForPiece(agentSpec, pieceDir);
|
||||
if (existsSync(resolved)) {
|
||||
agentPath = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
agent: agentSpec,
|
||||
agentPath,
|
||||
instructionTemplate: resolveContentPath(raw.instruction_template, pieceDir),
|
||||
rules: raw.rules.map((r) => ({ condition: r.condition, next: r.next })),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize raw loop monitors from YAML into internal format.
|
||||
*/
|
||||
function normalizeLoopMonitors(
|
||||
raw: Array<{ cycle: string[]; threshold: number; judge: { agent?: string; instruction_template?: string; rules: Array<{ condition: string; next: string }> } }> | undefined,
|
||||
pieceDir: string,
|
||||
): LoopMonitorConfig[] | undefined {
|
||||
if (!raw || raw.length === 0) return undefined;
|
||||
return raw.map((monitor) => ({
|
||||
cycle: monitor.cycle,
|
||||
threshold: monitor.threshold,
|
||||
judge: normalizeLoopMonitorJudge(monitor.judge, pieceDir),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw YAML piece config to internal format.
|
||||
* Agent paths are resolved relative to the piece directory.
|
||||
@ -229,6 +270,7 @@ export function normalizePieceConfig(raw: unknown, pieceDir: string): PieceConfi
|
||||
movements,
|
||||
initialMovement,
|
||||
maxIterations: parsed.max_iterations,
|
||||
loopMonitors: normalizeLoopMonitors(parsed.loop_monitors, pieceDir),
|
||||
answerAgent: parsed.answer_agent,
|
||||
};
|
||||
}
|
||||
|
||||
@ -51,6 +51,16 @@ export function getProjectConfigDir(projectDir: string): string {
|
||||
return join(resolve(projectDir), '.takt');
|
||||
}
|
||||
|
||||
/** Get project pieces directory (.takt/pieces in project) */
|
||||
export function getProjectPiecesDir(projectDir: string): string {
|
||||
return join(getProjectConfigDir(projectDir), 'pieces');
|
||||
}
|
||||
|
||||
/** Get project agents directory (.takt/agents in project) */
|
||||
export function getProjectAgentsDir(projectDir: string): string {
|
||||
return join(getProjectConfigDir(projectDir), 'agents');
|
||||
}
|
||||
|
||||
/** Get project config file path */
|
||||
export function getProjectConfigPath(projectDir: string): string {
|
||||
return join(getProjectConfigDir(projectDir), 'config.yaml');
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* GitHub integration - barrel exports
|
||||
*/
|
||||
|
||||
export type { GitHubIssue, GhCliStatus, CreatePrOptions, CreatePrResult } from './types.js';
|
||||
export type { GitHubIssue, GhCliStatus, CreatePrOptions, CreatePrResult, CreateIssueOptions, CreateIssueResult } from './types.js';
|
||||
|
||||
export {
|
||||
checkGhCli,
|
||||
@ -11,6 +11,7 @@ export {
|
||||
parseIssueNumbers,
|
||||
isIssueReference,
|
||||
resolveIssueTask,
|
||||
createIssue,
|
||||
} from './issue.js';
|
||||
|
||||
export { pushBranch, createPullRequest, buildPrBody } from './pr.js';
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createLogger } from '../../shared/utils/index.js';
|
||||
import type { GitHubIssue, GhCliStatus } from './types.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import type { GitHubIssue, GhCliStatus, CreateIssueOptions, CreateIssueResult } from './types.js';
|
||||
|
||||
export type { GitHubIssue, GhCliStatus };
|
||||
export type { GitHubIssue, GhCliStatus, CreateIssueOptions, CreateIssueResult };
|
||||
|
||||
const log = createLogger('github');
|
||||
|
||||
@ -172,3 +172,36 @@ export function resolveIssueTask(task: string): string {
|
||||
const issues = issueNumbers.map((n) => fetchIssue(n));
|
||||
return issues.map(formatIssueAsTask).join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GitHub Issue via `gh issue create`.
|
||||
*/
|
||||
export function createIssue(options: CreateIssueOptions): CreateIssueResult {
|
||||
const ghStatus = checkGhCli();
|
||||
if (!ghStatus.available) {
|
||||
return { success: false, error: ghStatus.error };
|
||||
}
|
||||
|
||||
const args = ['issue', 'create', '--title', options.title, '--body', options.body];
|
||||
if (options.labels && options.labels.length > 0) {
|
||||
args.push('--label', options.labels.join(','));
|
||||
}
|
||||
|
||||
log.info('Creating issue', { title: options.title });
|
||||
|
||||
try {
|
||||
const output = execFileSync('gh', args, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const url = output.trim();
|
||||
log.info('Issue created', { url });
|
||||
|
||||
return { success: true, url };
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
log.error('Issue creation failed', { error: errorMessage });
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,3 +35,20 @@ export interface CreatePrResult {
|
||||
/** Error message on failure */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface CreateIssueOptions {
|
||||
/** Issue title */
|
||||
title: string;
|
||||
/** Issue body (markdown) */
|
||||
body: string;
|
||||
/** Labels to apply */
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export interface CreateIssueResult {
|
||||
success: boolean;
|
||||
/** Issue URL on success */
|
||||
url?: string;
|
||||
/** Error message on failure */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ export type {
|
||||
BranchInfo,
|
||||
BranchListItem,
|
||||
SummarizeOptions,
|
||||
TaskListItem,
|
||||
} from './types.js';
|
||||
|
||||
// Classes
|
||||
|
||||
@ -16,9 +16,12 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { parseTaskFiles, parseTaskFile, type ParsedTask } from './parser.js';
|
||||
import type { TaskInfo, TaskResult } from './types.js';
|
||||
import type { TaskInfo, TaskResult, TaskListItem } from './types.js';
|
||||
import { createLogger } from '../../shared/utils/index.js';
|
||||
|
||||
export type { TaskInfo, TaskResult };
|
||||
export type { TaskInfo, TaskResult, TaskListItem };
|
||||
|
||||
const log = createLogger('task-runner');
|
||||
|
||||
/**
|
||||
* タスク実行管理クラス
|
||||
@ -129,6 +132,78 @@ export class TaskRunner {
|
||||
return this.moveTask(result, this.failedDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* pendingタスクを TaskListItem 形式で取得
|
||||
*/
|
||||
listPendingTaskItems(): TaskListItem[] {
|
||||
return this.listTasks().map((task) => ({
|
||||
kind: 'pending' as const,
|
||||
name: task.name,
|
||||
createdAt: task.createdAt,
|
||||
filePath: task.filePath,
|
||||
content: task.content.trim().split('\n')[0]?.slice(0, 80) ?? '',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* failedタスクの一覧を取得
|
||||
* .takt/failed/ 内のサブディレクトリを走査し、TaskListItem を返す
|
||||
*/
|
||||
listFailedTasks(): TaskListItem[] {
|
||||
this.ensureDirs();
|
||||
|
||||
const entries = fs.readdirSync(this.failedDir);
|
||||
|
||||
return entries
|
||||
.filter((entry) => {
|
||||
const entryPath = path.join(this.failedDir, entry);
|
||||
return fs.statSync(entryPath).isDirectory() && entry.includes('_');
|
||||
})
|
||||
.map((entry) => {
|
||||
const entryPath = path.join(this.failedDir, entry);
|
||||
const underscoreIdx = entry.indexOf('_');
|
||||
const timestampRaw = entry.slice(0, underscoreIdx);
|
||||
const name = entry.slice(underscoreIdx + 1);
|
||||
const createdAt = timestampRaw.replace(
|
||||
/^(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})$/,
|
||||
'$1:$2:$3',
|
||||
);
|
||||
const content = this.readFailedTaskContent(entryPath);
|
||||
return { kind: 'failed' as const, name, createdAt, filePath: entryPath, content };
|
||||
})
|
||||
.filter((item) => item.name !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* failedタスクディレクトリ内のタスクファイルから先頭1行を読み取る
|
||||
*/
|
||||
private readFailedTaskContent(dirPath: string): string {
|
||||
const taskExtensions = ['.md', '.yaml', '.yml'];
|
||||
let files: string[];
|
||||
try {
|
||||
files = fs.readdirSync(dirPath);
|
||||
} catch (err) {
|
||||
log.error('Failed to read failed task directory', { dirPath, error: String(err) });
|
||||
return '';
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const ext = path.extname(file);
|
||||
if (file === 'report.md' || file === 'log.json') continue;
|
||||
if (!taskExtensions.includes(ext)) continue;
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(dirPath, file), 'utf-8');
|
||||
return raw.trim().split('\n')[0]?.slice(0, 80) ?? '';
|
||||
} catch (err) {
|
||||
log.error('Failed to read failed task file', { file, dirPath, error: String(err) });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* タスクファイルを指定ディレクトリに移動し、レポート・ログを生成する
|
||||
*/
|
||||
|
||||
@ -66,3 +66,12 @@ export interface SummarizeOptions {
|
||||
/** Use LLM for summarization (default: true). If false, uses romanization. */
|
||||
useLLM?: boolean;
|
||||
}
|
||||
|
||||
/** pending/failedタスクのリストアイテム */
|
||||
export interface TaskListItem {
|
||||
kind: 'pending' | 'failed';
|
||||
name: string;
|
||||
createdAt: string;
|
||||
filePath: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
@ -16,7 +16,12 @@ interactive:
|
||||
summarizeFailed: "Failed to summarize conversation. Please try again."
|
||||
continuePrompt: "Okay, continue describing your task."
|
||||
proposed: "Proposed task instruction:"
|
||||
confirm: "Use this task instruction?"
|
||||
actionPrompt: "What would you like to do?"
|
||||
actions:
|
||||
execute: "Execute now"
|
||||
createIssue: "Create GitHub Issue"
|
||||
saveTask: "Save as Task"
|
||||
continue: "Continue editing"
|
||||
cancelled: "Cancelled"
|
||||
playNoTask: "Please specify task content: /play <task>"
|
||||
previousTask:
|
||||
|
||||
@ -16,7 +16,12 @@ interactive:
|
||||
summarizeFailed: "会話の要約に失敗しました。再度お試しください。"
|
||||
continuePrompt: "続けてタスク内容を入力してください。"
|
||||
proposed: "提案されたタスク指示:"
|
||||
confirm: "このタスク指示で進めますか?"
|
||||
actionPrompt: "どうしますか?"
|
||||
actions:
|
||||
execute: "実行する"
|
||||
createIssue: "GitHub Issueを建てる"
|
||||
saveTask: "タスクにつむ"
|
||||
continue: "会話を続ける"
|
||||
cancelled: "キャンセルしました"
|
||||
playNoTask: "タスク内容を指定してください: /play <タスク内容>"
|
||||
previousTask:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user