diff --git a/.github/workflows/cleanup-skipped-runs.yml b/.github/workflows/cleanup-skipped-runs.yml new file mode 100644 index 0000000..97db529 --- /dev/null +++ b/.github/workflows/cleanup-skipped-runs.yml @@ -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 {} diff --git a/CHANGELOG.md b/CHANGELOG.md index 004e56d..66547e2 100644 --- a/CHANGELOG.md +++ b/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 の内容を正式リリース。機能変更なし。 diff --git a/README.md b/README.md index 720c2ec..7ecfdf0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/README.ja.md b/docs/README.ja.md index fb187db..eb344f4 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -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` でピースを切り替えられます。 ## ビルトインエージェント diff --git a/docs/data-flow-diagrams.md b/docs/data-flow-diagrams.md index 99936b0..f2d403f 100644 --- a/docs/data-flow-diagrams.md +++ b/docs/data-flow-diagrams.md @@ -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) diff --git a/docs/data-flow.md b/docs/data-flow.md index 245dad1..9ed61c8 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -355,7 +355,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され **データ出力**: - `InteractiveModeResult`: - - `confirmed: boolean` + - `action: InteractiveModeAction` (`'execute' | 'save_task' | 'create_issue' | 'cancel'`) - `task: string` (会話履歴全体を結合した文字列) --- diff --git a/e2e/specs/eject.e2e.ts b/e2e/specs/eject.e2e.ts new file mode 100644 index 0000000..fbf5c85 --- /dev/null +++ b/e2e/specs/eject.e2e.ts @@ -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/'); + }); +}); diff --git a/package-lock.json b/package-lock.json index ff5b6ab..fed9ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4a770c5..9e59a11 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/global/en/default-categories.yaml b/resources/global/en/piece-categories.yaml similarity index 52% rename from resources/global/en/default-categories.yaml rename to resources/global/en/piece-categories.yaml index ded9597..a06cfb7 100644 --- a/resources/global/en/default-categories.yaml +++ b/resources/global/en/piece-categories.yaml @@ -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 diff --git a/resources/global/en/pieces/default-hybrid-codex.yaml b/resources/global/en/pieces/default-hybrid-codex.yaml new file mode 100644 index 0000000..3baadbc --- /dev/null +++ b/resources/global/en/pieces/default-hybrid-codex.yaml @@ -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 + ``` + ``` diff --git a/resources/global/en/pieces/default.yaml b/resources/global/en/pieces/default.yaml index 39d4a72..5774abc 100644 --- a/resources/global/en/pieces/default.yaml +++ b/resources/global/en/pieces/default.yaml @@ -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 diff --git a/resources/global/en/pieces/expert-cqrs-hybrid-codex.yaml b/resources/global/en/pieces/expert-cqrs-hybrid-codex.yaml new file mode 100644 index 0000000..05e5917 --- /dev/null +++ b/resources/global/en/pieces/expert-cqrs-hybrid-codex.yaml @@ -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 diff --git a/resources/global/en/pieces/expert-hybrid-codex.yaml b/resources/global/en/pieces/expert-hybrid-codex.yaml new file mode 100644 index 0000000..7db1ca6 --- /dev/null +++ b/resources/global/en/pieces/expert-hybrid-codex.yaml @@ -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 diff --git a/resources/global/en/pieces/minimal-hybrid-codex.yaml b/resources/global/en/pieces/minimal-hybrid-codex.yaml new file mode 100644 index 0000000..16c004c --- /dev/null +++ b/resources/global/en/pieces/minimal-hybrid-codex.yaml @@ -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} diff --git a/resources/global/en/pieces/passthrough-hybrid-codex.yaml b/resources/global/en/pieces/passthrough-hybrid-codex.yaml new file mode 100644 index 0000000..fe4c1d6 --- /dev/null +++ b/resources/global/en/pieces/passthrough-hybrid-codex.yaml @@ -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. diff --git a/resources/global/en/pieces/passthrough.yaml b/resources/global/en/pieces/passthrough.yaml new file mode 100644 index 0000000..801c12c --- /dev/null +++ b/resources/global/en/pieces/passthrough.yaml @@ -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. diff --git a/resources/global/en/pieces/review-fix-minimal-hybrid-codex.yaml b/resources/global/en/pieces/review-fix-minimal-hybrid-codex.yaml new file mode 100644 index 0000000..6342d29 --- /dev/null +++ b/resources/global/en/pieces/review-fix-minimal-hybrid-codex.yaml @@ -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} diff --git a/resources/global/ja/default-categories.yaml b/resources/global/ja/piece-categories.yaml similarity index 50% rename from resources/global/ja/default-categories.yaml rename to resources/global/ja/piece-categories.yaml index bfd4ace..ced6285 100644 --- a/resources/global/ja/default-categories.yaml +++ b/resources/global/ja/piece-categories.yaml @@ -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 diff --git a/resources/global/ja/pieces/coding-hybrid-codex.yaml b/resources/global/ja/pieces/coding-hybrid-codex.yaml new file mode 100644 index 0000000..8b0ee98 --- /dev/null +++ b/resources/global/ja/pieces/coding-hybrid-codex.yaml @@ -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. 修正内容を具体的に報告する + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + ## 証拠 + - {確認したファイル/検索/差分/ログの要点を列挙} diff --git a/resources/global/ja/pieces/default-hybrid-codex.yaml b/resources/global/ja/pieces/default-hybrid-codex.yaml new file mode 100644 index 0000000..4a26972 --- /dev/null +++ b/resources/global/ja/pieces/default-hybrid-codex.yaml @@ -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 + ``` + ``` diff --git a/resources/global/ja/pieces/default.yaml b/resources/global/ja/pieces/default.yaml index 154cf48..b384a79 100644 --- a/resources/global/ja/pieces/default.yaml +++ b/resources/global/ja/pieces/default.yaml @@ -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 diff --git a/resources/global/ja/pieces/expert-cqrs-hybrid-codex.yaml b/resources/global/ja/pieces/expert-cqrs-hybrid-codex.yaml new file mode 100644 index 0000000..b2582fd --- /dev/null +++ b/resources/global/ja/pieces/expert-cqrs-hybrid-codex.yaml @@ -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 diff --git a/resources/global/ja/pieces/expert-hybrid-codex.yaml b/resources/global/ja/pieces/expert-hybrid-codex.yaml new file mode 100644 index 0000000..b63b379 --- /dev/null +++ b/resources/global/ja/pieces/expert-hybrid-codex.yaml @@ -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 diff --git a/resources/global/ja/pieces/minimal-hybrid-codex.yaml b/resources/global/ja/pieces/minimal-hybrid-codex.yaml new file mode 100644 index 0000000..3e08e61 --- /dev/null +++ b/resources/global/ja/pieces/minimal-hybrid-codex.yaml @@ -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: | + 監督者からの指摘を修正してください。 + + 監督者は全体を俯瞰した視点から問題を指摘しています。 + 優先度の高い項目から順に対応してください。 + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + ## 証拠 + - {確認したファイル/検索/差分/ログの要点を列挙} diff --git a/resources/global/ja/pieces/passthrough-hybrid-codex.yaml b/resources/global/ja/pieces/passthrough-hybrid-codex.yaml new file mode 100644 index 0000000..a68627b --- /dev/null +++ b/resources/global/ja/pieces/passthrough-hybrid-codex.yaml @@ -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: | + タスクをこなしてください。 diff --git a/resources/global/ja/pieces/passthrough.yaml b/resources/global/ja/pieces/passthrough.yaml new file mode 100644 index 0000000..3f54314 --- /dev/null +++ b/resources/global/ja/pieces/passthrough.yaml @@ -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: | + タスクをこなしてください。 diff --git a/resources/global/ja/pieces/review-fix-minimal-hybrid-codex.yaml b/resources/global/ja/pieces/review-fix-minimal-hybrid-codex.yaml new file mode 100644 index 0000000..780df0c --- /dev/null +++ b/resources/global/ja/pieces/review-fix-minimal-hybrid-codex.yaml @@ -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: | + 監督者からの指摘を修正してください。 + + 監督者は全体を俯瞰した視点から問題を指摘しています。 + 優先度の高い項目から順に対応してください。 + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + ## 証拠 + - {確認したファイル/検索/差分/ログの要点を列挙} diff --git a/resources/skill/SKILL.md b/resources/skill/SKILL.md new file mode 100644 index 0000000..dd5c6b6 --- /dev/null +++ b/resources/skill/SKILL.md @@ -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の構造定義とフィールド説明 | diff --git a/resources/skill/references/engine.md b/resources/skill/references/engine.md new file mode 100644 index 0000000..ef6e95a --- /dev/null +++ b/resources/skill/references/engine.md @@ -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 +│ ↓ +└──────────────────────────────────────────────┘ +``` diff --git a/resources/skill/references/yaml-schema.md b/resources/skill/references/yaml-schema.md new file mode 100644 index 0000000..edd455f --- /dev/null +++ b/resources/skill/references/yaml-schema.md @@ -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` フィールドの方を権限制御に使用する。 diff --git a/resources/skill/takt-command.md b/resources/skill/takt-command.md new file mode 100644 index 0000000..d3d4641 --- /dev/null +++ b/resources/skill/takt-command.md @@ -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 の「手順」に従って処理を開始する。 diff --git a/src/__tests__/addTask.test.ts b/src/__tests__/addTask.test.ts index e6f74c7..49036e7 100644 --- a/src/__tests__/addTask.test.ts +++ b/src/__tests__/addTask.test.ts @@ -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(); + }); + }); }); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index fe1567b..a54afea 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -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(); }); diff --git a/src/__tests__/createIssue.test.ts b/src/__tests__/createIssue.test.ts new file mode 100644 index 0000000..3f1f05b --- /dev/null +++ b/src/__tests__/createIssue.test.ts @@ -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>()), + 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'); + }); +}); diff --git a/src/__tests__/createIssueFromTask.test.ts b/src/__tests__/createIssueFromTask.test.ts new file mode 100644 index 0000000..2d15a87 --- /dev/null +++ b/src/__tests__/createIssueFromTask.test.ts @@ -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>()), + 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, + }); + }); +}); diff --git a/src/__tests__/cycle-detector.test.ts b/src/__tests__/cycle-detector.test.ts new file mode 100644 index 0000000..059ff94 --- /dev/null +++ b/src/__tests__/cycle-detector.test.ts @@ -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); + }); + }); +}); diff --git a/src/__tests__/engine-loop-monitors.test.ts b/src/__tests__/engine-loop-monitors.test.ts new file mode 100644 index 0000000..4a505b8 --- /dev/null +++ b/src/__tests__/engine-loop-monitors.test.ts @@ -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>()), + 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 = {}, +): 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); + }); + }); +}); diff --git a/src/__tests__/i18n.test.ts b/src/__tests__/i18n.test.ts index 5e37919..149c530 100644 --- a/src/__tests__/i18n.test.ts +++ b/src/__tests__/i18n.test.ts @@ -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'); }); diff --git a/src/__tests__/interactive.test.ts b/src/__tests__/interactive.test.ts index fba65ce..ef398d3 100644 --- a/src/__tests__/interactive.test.ts +++ b/src/__tests__/interactive.test.ts @@ -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 }; 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 }; 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 }; 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'); + }); + }); }); diff --git a/src/__tests__/listNonInteractive.test.ts b/src/__tests__/listNonInteractive.test.ts new file mode 100644 index 0000000..b25ab07 --- /dev/null +++ b/src/__tests__/listNonInteractive.test.ts @@ -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(); + }); +}); diff --git a/src/__tests__/listTasks.test.ts b/src/__tests__/listTasks.test.ts index ecc1729..ba3b182 100644 --- a/src/__tests__/listTasks.test.ts +++ b/src/__tests__/listTasks.test.ts @@ -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(); + }); +}); diff --git a/src/__tests__/piece-category-config.test.ts b/src/__tests__/piece-category-config.test.ts index 6d4deb5..aa649bb 100644 --- a/src/__tests__/piece-category-config.test.ts +++ b/src/__tests__/piece-category-config.test.ts @@ -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; 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; +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; - 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'); + }); +}); diff --git a/src/__tests__/piece-selection.test.ts b/src/__tests__/piece-selection.test.ts index 70269bb..ae250df 100644 --- a/src/__tests__/piece-selection.test.ts +++ b/src/__tests__/piece-selection.test.ts @@ -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; + 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 { + const map = new Map(); + 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); + }); +}); diff --git a/src/__tests__/saveTaskFile.test.ts b/src/__tests__/saveTaskFile.test.ts new file mode 100644 index 0000000..6c087ad --- /dev/null +++ b/src/__tests__/saveTaskFile.test.ts @@ -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>()), + 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); + }); +}); diff --git a/src/__tests__/taskDeleteActions.test.ts b/src/__tests__/taskDeleteActions.test.ts new file mode 100644 index 0000000..47cecdb --- /dev/null +++ b/src/__tests__/taskDeleteActions.test.ts @@ -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>()), + 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(); + }); +}); diff --git a/src/app/cli/commands.ts b/src/app/cli/commands.ts index c8a6512..b0fab4c 100644 --- a/src/app/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -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(); + }); diff --git a/src/app/cli/routing.ts b/src/app/cli/routing.ts index 5148d6e..4731de5 100644 --- a/src/app/cli/routing.ts +++ b/src/app/cli/routing.ts @@ -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 { 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 diff --git a/src/core/models/index.ts b/src/core/models/index.ts index 263d888..d821fec 100644 --- a/src/core/models/index.ts +++ b/src/core/models/index.ts @@ -11,6 +11,9 @@ export type { PieceRule, PieceMovement, LoopDetectionConfig, + LoopMonitorConfig, + LoopMonitorJudge, + LoopMonitorRule, PieceConfig, PieceState, CustomAgentConfig, diff --git a/src/core/models/piece-types.ts b/src/core/models/piece-types.ts index c62b575..e9cb3e2 100644 --- a/src/core/models/piece-types.ts +++ b/src/core/models/piece-types.ts @@ -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 diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index e8beaad..58db65b 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -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(), }); diff --git a/src/core/models/types.ts b/src/core/models/types.ts index f0dca5f..efc4ef2 100644 --- a/src/core/models/types.ts +++ b/src/core/models/types.ts @@ -30,6 +30,9 @@ export type { ReportObjectConfig, PieceMovement, LoopDetectionConfig, + LoopMonitorConfig, + LoopMonitorJudge, + LoopMonitorRule, PieceConfig, PieceState, } from './piece-types.js'; diff --git a/src/core/piece/engine/PieceEngine.ts b/src/core/piece/engine/PieceEngine.ts index bfe4921..bfea300 100644 --- a/src/core/piece/engine/PieceEngine.ts +++ b/src/core/piece/engine/PieceEngine.ts @@ -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 { + 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 { 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); diff --git a/src/core/piece/engine/cycle-detector.ts b/src/core/piece/engine/cycle-detector.ts new file mode 100644 index 0000000..f60355d --- /dev/null +++ b/src/core/piece/engine/cycle-detector.ts @@ -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; + } +} diff --git a/src/core/piece/engine/index.ts b/src/core/piece/engine/index.ts index 3c5bf20..f94c0b4 100644 --- a/src/core/piece/engine/index.ts +++ b/src/core/piece/engine/index.ts @@ -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'; diff --git a/src/core/piece/index.ts b/src/core/piece/index.ts index 6a08774..bbb1a3b 100644 --- a/src/core/piece/index.ts +++ b/src/core/piece/index.ts @@ -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, diff --git a/src/core/piece/types.ts b/src/core/piece/types.ts index f5e179a..2527a50 100644 --- a/src/core/piece/types.ts +++ b/src/core/piece/types.ts @@ -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 */ diff --git a/src/features/config/deploySkill.ts b/src/features/config/deploySkill.ts new file mode 100644 index 0000000..c24bad4 --- /dev/null +++ b/src/features/config/deploySkill.ts @@ -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 { + 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 '); + 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); + } + } +} diff --git a/src/features/config/ejectBuiltin.ts b/src/features/config/ejectBuiltin.ts index d2816a5..635072b 100644 --- a/src/features/config/ejectBuiltin.ts +++ b/src/features/config/ejectBuiltin.ts @@ -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 { +export async function ejectBuiltin(name?: string, options: EjectOptions = {}): Promise { header('Eject Builtin'); const lang = getLanguage(); @@ -29,7 +39,7 @@ export async function ejectBuiltin(name?: string): Promise { if (!name) { // List available builtins - listAvailableBuiltins(builtinPiecesDir); + listAvailableBuiltins(builtinPiecesDir, options.global); return; } @@ -40,24 +50,24 @@ export async function ejectBuiltin(name?: string): Promise { 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 { 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 { } /** 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/'); + } } /** diff --git a/src/features/config/index.ts b/src/features/config/index.ts index a0b4f0f..39b2ccc 100644 --- a/src/features/config/index.ts +++ b/src/features/config/index.ts @@ -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'; diff --git a/src/features/config/resetCategories.ts b/src/features/config/resetCategories.ts new file mode 100644 index 0000000..ae15490 --- /dev/null +++ b/src/features/config/resetCategories.ts @@ -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 { + header('Reset Categories'); + + const defaultPath = getDefaultCategoriesPath(); + resetPieceCategories(defaultPath); + + const userPath = getPieceCategoriesPath(); + success('Categories reset to builtin defaults.'); + info(` ${userPath}`); +} diff --git a/src/features/config/switchPiece.ts b/src/features/config/switchPiece.ts index 1551fad..7b9206c 100644 --- a/src/features/config/switchPiece.ts +++ b/src/features/config/switchPiece.ts @@ -28,7 +28,7 @@ export async function switchPiece(cwd: string, pieceName?: string): Promise { +type PostSummaryAction = InteractiveModeAction | 'continue'; + +async function selectPostSummaryAction( + task: string, + proposedLabel: string, + ui: InteractiveUIText, +): Promise { blankLine(); - info(message); + info(proposedLabel); console.log(task); - const decision = await selectOption(confirmLabel, [ - { label: yesLabel, value: 'yes' }, - { label: noLabel, value: 'no' }, + + return selectOption(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(); diff --git a/src/features/pieceSelection/index.ts b/src/features/pieceSelection/index.ts index 5bb6359..689c096 100644 --- a/src/features/pieceSelection/index.ts +++ b/src/features/pieceSelection/index.ts @@ -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, - sourceFilter: PieceSource, -): number { - const categorizedPieces = new Set(); - 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 { - const uncategorizedCustom = getRootLevelPieces( - categorized.categories, - categorized.allPieces, - 'user' - ); - const builtinCount = countPiecesIncludingCategories( - categorized.builtinCategories, - categorized.allPieces, - 'builtin' - ); - const buildOptions = (): SelectOptionItem[] => { const options: SelectOptionItem[] = []; - 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('Select piece:', buildOptions(), { onKeyPress: (key: string, value: string): SelectOptionItem[] | 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, - sourceFilter: PieceSource, -): string[] { - const categorizedPieces = new Set(); - 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 { 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('Select piece:', buildFlatOptions(), { - cancelLabel: '← Go back', - onKeyPress: (key: string, value: string): SelectOptionItem[] | 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; } } diff --git a/src/features/tasks/add/index.ts b/src/features/tasks/add/index.ts index 910d192..d90788d 100644 --- a/src/features/tasks/add/index.ts +++ b/src/features/tasks/add/index.ts @@ -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 { + 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 { + 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 { // 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 { 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 { } } - // 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) { diff --git a/src/features/tasks/execute/selectAndExecute.ts b/src/features/tasks/execute/selectAndExecute.ts index bfa039c..d39923a 100644 --- a/src/features/tasks/execute/selectAndExecute.ts +++ b/src/features/tasks/execute/selectAndExecute.ts @@ -58,7 +58,7 @@ async function selectPieceWithDirectoryCategories(cwd: string): Promise { - const categoryConfig = getPieceCategories(cwd); + const categoryConfig = getPieceCategories(); if (categoryConfig) { const current = getCurrentPiece(cwd); const allPieces = loadAllPiecesWithSources(cwd); diff --git a/src/features/tasks/index.ts b/src/features/tasks/index.ts index 04b8805..b647201 100644 --- a/src/features/tasks/index.ts +++ b/src/features/tasks/index.ts @@ -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, diff --git a/src/features/tasks/list/index.ts b/src/features/tasks/list/index.ts index 5195654..140787a 100644 --- a/src/features/tasks/list/index.ts +++ b/src/features/tasks/list/index.ts @@ -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 { + 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 { - 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( + `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 { - 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( - '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.'); } diff --git a/src/features/tasks/list/listNonInteractive.ts b/src/features/tasks/list/listNonInteractive.ts new file mode 100644 index 0000000..9c133f4 --- /dev/null +++ b/src/features/tasks/list/listNonInteractive.ts @@ -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 { + 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; + } +} diff --git a/src/features/tasks/list/taskActions.ts b/src/features/tasks/list/taskActions.ts index bdc1002..59a32c1 100644 --- a/src/features/tasks/list/taskActions.ts +++ b/src/features/tasks/list/taskActions.ts @@ -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 { 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); } } + diff --git a/src/features/tasks/list/taskDeleteActions.ts b/src/features/tasks/list/taskDeleteActions.ts new file mode 100644 index 0000000..128dfe1 --- /dev/null +++ b/src/features/tasks/list/taskDeleteActions.ts @@ -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 { + 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 { + 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; +} diff --git a/src/infra/config/global/index.ts b/src/infra/config/global/index.ts index f866866..e48ebc5 100644 --- a/src/infra/config/global/index.ts +++ b/src/infra/config/global/index.ts @@ -26,12 +26,9 @@ export { } from './bookmarks.js'; export { - getPieceCategoriesConfig, - setPieceCategoriesConfig, - getShowOthersCategory, - setShowOthersCategory, - getOthersCategoryName, - setOthersCategoryName, + getPieceCategoriesPath, + ensureUserCategoriesFile, + resetPieceCategories, } from './pieceCategories.js'; export { diff --git a/src/infra/config/global/pieceCategories.ts b/src/infra/config/global/pieceCategories.ts index 4bc818f..576b7bc 100644 --- a/src/infra/config/global/pieceCategories.ts +++ b/src/infra/config/global/pieceCategories.ts @@ -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); } diff --git a/src/infra/config/loaders/index.ts b/src/infra/config/loaders/index.ts index bd9cd98..3e8f53e 100644 --- a/src/infra/config/loaders/index.ts +++ b/src/infra/config/loaders/index.ts @@ -19,6 +19,7 @@ export { export { loadDefaultCategories, + getDefaultCategoriesPath, getPieceCategories, buildCategorizedPieces, findPieceCategories, diff --git a/src/infra/config/loaders/pieceCategories.ts b/src/infra/config/loaders/pieceCategories.ts index ad2277b..f5669db 100644 --- a/src/infra/config/loaders/pieceCategories.ts +++ b/src/infra/config/loaders/pieceCategories.ts @@ -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; 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, - sourceFilter: (source: PieceSource) => boolean, categorized: Set, ): 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, categorized: Set, - 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, @@ -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(); - const categories = buildCategoryTreeForSource( + const categorized = new Set(); + const categories = buildCategoryTree( config.pieceCategories, allPieces, - isCustom, - categorizedCustom, - ); - - const categorizedBuiltin = new Set(); - 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, }; diff --git a/src/infra/config/loaders/pieceParser.ts b/src/infra/config/loaders/pieceParser.ts index f960769..1d3e114 100644 --- a/src/infra/config/loaders/pieceParser.ts +++ b/src/infra/config/loaders/pieceParser.ts @@ -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; @@ -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, }; } diff --git a/src/infra/config/paths.ts b/src/infra/config/paths.ts index 8b49b54..93847a5 100644 --- a/src/infra/config/paths.ts +++ b/src/infra/config/paths.ts @@ -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'); diff --git a/src/infra/github/index.ts b/src/infra/github/index.ts index d0e2fc6..b085622 100644 --- a/src/infra/github/index.ts +++ b/src/infra/github/index.ts @@ -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'; diff --git a/src/infra/github/issue.ts b/src/infra/github/issue.ts index a8cb65a..7f55ba9 100644 --- a/src/infra/github/issue.ts +++ b/src/infra/github/issue.ts @@ -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 }; + } +} diff --git a/src/infra/github/types.ts b/src/infra/github/types.ts index e318d40..07e0376 100644 --- a/src/infra/github/types.ts +++ b/src/infra/github/types.ts @@ -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; +} diff --git a/src/infra/task/index.ts b/src/infra/task/index.ts index 25596de..e6ea017 100644 --- a/src/infra/task/index.ts +++ b/src/infra/task/index.ts @@ -11,6 +11,7 @@ export type { BranchInfo, BranchListItem, SummarizeOptions, + TaskListItem, } from './types.js'; // Classes diff --git a/src/infra/task/runner.ts b/src/infra/task/runner.ts index 8102854..a443b8b 100644 --- a/src/infra/task/runner.ts +++ b/src/infra/task/runner.ts @@ -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 ''; + } + /** * タスクファイルを指定ディレクトリに移動し、レポート・ログを生成する */ diff --git a/src/infra/task/types.ts b/src/infra/task/types.ts index 1cb038e..f142a02 100644 --- a/src/infra/task/types.ts +++ b/src/infra/task/types.ts @@ -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; +} diff --git a/src/shared/i18n/labels_en.yaml b/src/shared/i18n/labels_en.yaml index af0c664..00387a3 100644 --- a/src/shared/i18n/labels_en.yaml +++ b/src/shared/i18n/labels_en.yaml @@ -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 " previousTask: diff --git a/src/shared/i18n/labels_ja.yaml b/src/shared/i18n/labels_ja.yaml index efbc22f..da7c810 100644 --- a/src/shared/i18n/labels_ja.yaml +++ b/src/shared/i18n/labels_ja.yaml @@ -16,7 +16,12 @@ interactive: summarizeFailed: "会話の要約に失敗しました。再度お試しください。" continuePrompt: "続けてタスク内容を入力してください。" proposed: "提案されたタスク指示:" - confirm: "このタスク指示で進めますか?" + actionPrompt: "どうしますか?" + actions: + execute: "実行する" + createIssue: "GitHub Issueを建てる" + saveTask: "タスクにつむ" + continue: "会話を続ける" cancelled: "キャンセルしました" playNoTask: "タスク内容を指定してください: /play <タスク内容>" previousTask: