diff --git a/AGENTS.md b/AGENTS.md index c9eed3b..5f46801 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,4 +37,4 @@ npm run test:watch # テスト実行をウォッチ - 脆弱性は公開 Issue ではなくメンテナへ直接報告します。 - `.takt/logs/` など機密情報を含む可能性のあるファイルは共有しないでください。 - `~/.takt/config.yaml` の `trusted` ディレクトリは最小限にし、不要なパスは登録しないでください。 -- 新しいワークフローを追加する場合は `~/.takt/workflows/` の既存スキーマを踏襲し、不要な拡張を避けます。 +- 新しいピースを追加する場合は `~/.takt/pieces/` の既存スキーマを踏襲し、不要な拡張を避けます。 diff --git a/CLAUDE.md b/CLAUDE.md index e43753e..397b72b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Claude Code. It enables YAML-based workflow definitions that coordinate multiple AI agents through state machine transitions with rule-based routing. +TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Claude Code. It enables YAML-based piece definitions that coordinate multiple AI agents through state machine transitions with rule-based routing. ## Development Commands @@ -23,21 +23,21 @@ TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Cl | Command | Description | |---------|-------------| -| `takt {task}` | Execute task with current workflow | +| `takt {task}` | Execute task with current piece | | `takt` | Interactive task input mode (chat with AI to refine requirements) | | `takt run` | Execute all pending tasks from `.takt/tasks/` once | | `takt watch` | Watch `.takt/tasks/` and auto-execute tasks (resident process) | | `takt add` | Add a new task via AI conversation | | `takt list` | List task branches (try merge, merge & cleanup, or delete) | -| `takt switch` | Switch workflow interactively | +| `takt switch` | Switch piece interactively | | `takt clear` | Clear agent conversation sessions (reset state) | -| `takt eject` | Copy builtin workflow/agents to `~/.takt/` for customization | +| `takt eject` | Copy builtin piece/agents to `~/.takt/` for customization | | `takt config` | Configure settings (permission mode) | | `takt --help` | Show help message | -**Interactive mode:** Running `takt` (without arguments) or `takt {initial message}` starts an interactive planning session. The AI helps refine task requirements through conversation. Type `/go` to execute the task with the selected workflow, or `/cancel` to abort. Implemented in `src/features/interactive/`. +**Interactive mode:** Running `takt` (without arguments) or `takt {initial message}` starts an interactive planning session. The AI helps refine task requirements through conversation. Type `/go` to execute the task with the selected piece, or `/cancel` to abort. Implemented in `src/features/interactive/`. -**Pipeline mode:** Specifying `--pipeline` enables non-interactive mode suitable for CI/CD. Automatically creates a branch, runs the workflow, commits, and pushes. Use `--auto-pr` to also create a pull request. Use `--skip-git` to run workflow only (no git operations). Implemented in `src/features/pipeline/`. +**Pipeline mode:** Specifying `--pipeline` enables non-interactive mode suitable for CI/CD. Automatically creates a branch, runs the piece, commits, and pushes. Use `--auto-pr` to also create a pull request. Use `--skip-git` to run piece only (no git operations). Implemented in `src/features/pipeline/`. **GitHub issue references:** `takt #6` fetches issue #6 and executes it as a task. @@ -48,10 +48,10 @@ TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Cl | `--pipeline` | Enable pipeline (non-interactive) mode — required for CI/automation | | `-t, --task ` | Task content (as alternative to GitHub issue) | | `-i, --issue ` | GitHub issue number (equivalent to `#N` in interactive mode) | -| `-w, --workflow ` | Workflow name or path to workflow YAML file (v0.3.8+) | +| `-w, --piece ` | Piece name or path to piece YAML file (v0.3.8+) | | `-b, --branch ` | Branch name (auto-generated if omitted) | | `--auto-pr` | Create PR after execution (interactive: skip confirmation, pipeline: enable PR) | -| `--skip-git` | Skip branch creation, commit, and push (pipeline mode, workflow-only) | +| `--skip-git` | Skip branch creation, commit, and push (pipeline mode, piece-only) | | `--repo ` | Repository for PR creation | | `--create-worktree ` | Skip worktree confirmation prompt | | `-q, --quiet` | **Minimal output mode: suppress AI output (for CI)** (v0.3.8+) | @@ -66,7 +66,7 @@ TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Cl ``` CLI (cli.ts) → Slash commands or executeTask() - → WorkflowEngine (workflow/engine.ts) + → PieceEngine (piece/engine.ts) → Per step: 3-phase execution Phase 1: runAgent() → main work Phase 2: runReportPhase() → report output (if step.report defined) @@ -85,7 +85,7 @@ Each step executes in up to 3 phases (session is resumed across phases): | Phase 2 | Report output | Write only | When `step.report` is defined | | Phase 3 | Status judgment | None (judgment only) | When step has tag-based rules | -Phase 2/3 are implemented in `src/core/workflow/engine/phase-runner.ts`. The session is resumed so the agent retains context from Phase 1. +Phase 2/3 are implemented in `src/core/piece/engine/phase-runner.ts`. The session is resumed so the agent retains context from Phase 1. ### Rule Evaluation (5-Stage Fallback) @@ -97,40 +97,40 @@ After step execution, rules are evaluated to determine the next step. Evaluation 4. **AI judge (ai() only)** - AI evaluates `ai("condition text")` rules 5. **AI judge fallback** - AI evaluates ALL conditions as final resort -Implemented in `src/core/workflow/evaluation/RuleEvaluator.ts`. The matched method is tracked as `RuleMatchMethod` type. +Implemented in `src/core/piece/evaluation/RuleEvaluator.ts`. The matched method is tracked as `RuleMatchMethod` type. ### Key Components -**WorkflowEngine** (`src/core/workflow/engine/WorkflowEngine.ts`) +**PieceEngine** (`src/core/piece/engine/PieceEngine.ts`) - State machine that orchestrates agent execution via EventEmitter - Manages step transitions based on rule evaluation results -- Emits events: `step:start`, `step:complete`, `step:blocked`, `step:loop_detected`, `workflow:complete`, `workflow:abort`, `iteration:limit` +- Emits events: `step:start`, `step:complete`, `step:blocked`, `step:loop_detected`, `piece:complete`, `piece:abort`, `iteration:limit` - Supports loop detection (`LoopDetector`) and iteration limits - Maintains agent sessions per step for conversation continuity - Delegates to `StepExecutor` (normal steps) and `ParallelRunner` (parallel steps) -**StepExecutor** (`src/core/workflow/engine/StepExecutor.ts`) -- Executes a single workflow step through the 3-phase model +**StepExecutor** (`src/core/piece/engine/StepExecutor.ts`) +- Executes a single piece step through the 3-phase model - Phase 1: Main agent execution (with tools) - Phase 2: Report output (Write-only, optional) - Phase 3: Status judgment (no tools, optional) - Builds instructions via `InstructionBuilder`, detects matched rules via `RuleEvaluator` -**ParallelRunner** (`src/core/workflow/engine/ParallelRunner.ts`) +**ParallelRunner** (`src/core/piece/engine/ParallelRunner.ts`) - Executes parallel sub-steps concurrently via `Promise.all()` - Aggregates sub-step results for parent rule evaluation - Supports `all()` / `any()` aggregate conditions -**RuleEvaluator** (`src/core/workflow/evaluation/RuleEvaluator.ts`) +**RuleEvaluator** (`src/core/piece/evaluation/RuleEvaluator.ts`) - 5-stage fallback evaluation: aggregate → Phase 3 tag → Phase 1 tag → ai() judge → all-conditions AI judge - Returns `RuleMatch` with index and detection method (`aggregate`, `phase3_tag`, `phase1_tag`, `ai_judge`, `ai_fallback`) - Fail-fast: throws if rules exist but no rule matched - **v0.3.8+:** Tag detection now uses **last match** instead of first match when multiple `[STEP:N]` tags appear in output -**Instruction Builder** (`src/core/workflow/instruction/InstructionBuilder.ts`) +**Instruction Builder** (`src/core/piece/instruction/InstructionBuilder.ts`) - Auto-injects standard sections into every instruction (no need for `{task}` or `{previous_response}` placeholders in templates): 1. Execution context (working dir, edit permission rules) - 2. Workflow context (iteration counts, report dir) + 2. Piece context (iteration counts, report dir) 3. User request (`{task}` — auto-injected unless placeholder present) 4. Previous response (auto-injected if `pass_previous_response: true`) 5. User inputs (auto-injected unless `{user_inputs}` placeholder present) @@ -161,9 +161,9 @@ Implemented in `src/core/workflow/evaluation/RuleEvaluator.ts`. The matched meth **Configuration** (`src/infra/config/`) - `loaders/loader.ts` - Custom agent loading from `.takt/agents.yaml` -- `loaders/workflowParser.ts` - YAML parsing, step/rule normalization with Zod validation -- `loaders/workflowResolver.ts` - **3-layer resolution with correct priority** (v0.3.8+: user → project → builtin) -- `loaders/workflowCategories.ts` - Workflow categorization and filtering +- `loaders/pieceParser.ts` - YAML parsing, step/rule normalization with Zod validation +- `loaders/pieceResolver.ts` - **3-layer resolution with correct priority** (v0.3.8+: user → project → builtin) +- `loaders/pieceCategories.ts` - Piece categorization and filtering - `loaders/agentLoader.ts` - Agent prompt file loading - `paths.ts` - Directory structure (`.takt/`, `~/.takt/`), session management - `global/globalConfig.ts` - Global configuration (provider, model, trusted dirs, **quiet mode** v0.3.8+) @@ -171,7 +171,7 @@ Implemented in `src/core/workflow/evaluation/RuleEvaluator.ts`. The matched meth **Task Management** (`src/features/tasks/`) - `execute/taskExecution.ts` - Main task execution orchestration -- `execute/workflowExecution.ts` - Workflow execution wrapper +- `execute/pieceExecution.ts` - Piece execution wrapper - `add/index.ts` - Interactive task addition via AI conversation - `list/index.ts` - List task branches with merge/delete actions - `watch/index.ts` - Watch for task files and auto-execute @@ -183,18 +183,18 @@ Implemented in `src/core/workflow/evaluation/RuleEvaluator.ts`. The matched meth ### Data Flow 1. User provides task (text or `#N` issue reference) or slash command → CLI -2. CLI loads workflow with **correct priority** (v0.3.8+): user `~/.takt/workflows/` → project `.takt/workflows/` → builtin `resources/global/{lang}/workflows/` -3. WorkflowEngine starts at `initial_step` +2. CLI loads piece with **correct priority** (v0.3.8+): user `~/.takt/pieces/` → project `.takt/pieces/` → builtin `resources/global/{lang}/pieces/` +3. PieceEngine starts at `initial_step` 4. Each step: `buildInstruction()` → Phase 1 (main) → Phase 2 (report) → Phase 3 (status) → `detectMatchedRule()` → `determineNextStep()` 5. Rule evaluation determines next step name (v0.3.8+: uses **last match** when multiple `[STEP:N]` tags appear) -6. Special transitions: `COMPLETE` ends workflow successfully, `ABORT` ends with failure +6. Special transitions: `COMPLETE` ends piece successfully, `ABORT` ends with failure ## Directory Structure ``` ~/.takt/ # Global user config (created on first run) - config.yaml # Trusted dirs, default workflow, log level, language - workflows/ # User workflow YAML files (override builtins) + config.yaml # Trusted dirs, default piece, log level, language + pieces/ # User piece YAML files (override builtins) agents/ # User agent prompt files (.md) .takt/ # Project-level config @@ -205,16 +205,16 @@ Implemented in `src/core/workflow/evaluation/RuleEvaluator.ts`. The matched meth resources/ # Bundled defaults (builtin, read from dist/ at runtime) global/ - en/ # English agents and workflows - ja/ # Japanese agents and workflows + en/ # English agents and pieces + ja/ # Japanese agents and pieces ``` Builtin resources are embedded in the npm package (`dist/resources/`). User files in `~/.takt/` take priority. Use `/eject` to copy builtins to `~/.takt/` for customization. -## Workflow YAML Schema +## Piece YAML Schema ```yaml -name: workflow-name +name: piece-name description: Optional description max_iterations: 10 initial_step: plan # First step to execute @@ -288,48 +288,48 @@ Key points about parallel steps: | Variable | Description | |----------|-------------| | `{task}` | Original user request (auto-injected if not in template) | -| `{iteration}` | Workflow-wide iteration count | +| `{iteration}` | Piece-wide iteration count | | `{max_iterations}` | Maximum iterations allowed | | `{step_iteration}` | Per-step iteration count | | `{previous_response}` | Previous step output (auto-injected if not in template) | | `{user_inputs}` | Accumulated user inputs (auto-injected if not in template) | | `{report_dir}` | Report directory name | -### Workflow Categories +### Piece Categories -Workflows can be organized into categories for better UI presentation. Categories are configured in: +Pieces can be organized into categories for better UI presentation. Categories are configured in: - `resources/global/{lang}/default-categories.yaml` - Default builtin categories -- `~/.takt/config.yaml` - User-defined categories (via `workflow_categories` field) +- `~/.takt/config.yaml` - User-defined categories (via `piece_categories` field) Category configuration supports: - Nested categories (unlimited depth) -- Per-category workflow lists -- "Others" category for uncategorized workflows (can be disabled via `show_others_category: false`) -- Builtin workflow filtering (disable via `builtin_workflows_enabled: false`, or selectively via `disabled_builtins: [name1, name2]`) +- Per-category piece lists +- "Others" category for uncategorized pieces (can be disabled via `show_others_category: false`) +- Builtin piece filtering (disable via `builtin_pieces_enabled: false`, or selectively via `disabled_builtins: [name1, name2]`) Example category config: ```yaml -workflow_categories: +piece_categories: Development: - workflows: [default, simple] + pieces: [default, simple] children: Backend: - workflows: [expert-cqrs] + pieces: [expert-cqrs] Frontend: - workflows: [expert] + pieces: [expert] Research: - workflows: [research, magi] + pieces: [research, magi] show_others_category: true -others_category_name: "Other Workflows" +others_category_name: "Other Pieces" ``` -Implemented in `src/infra/config/loaders/workflowCategories.ts`. +Implemented in `src/infra/config/loaders/pieceCategories.ts`. ### Model Resolution Model is resolved in the following priority order: -1. **Workflow step `model`** - Highest priority (specified in step YAML) +1. **Piece step `model`** - Highest priority (specified in step YAML) 2. **Custom agent `model`** - Agent-level model in `.takt/agents.yaml` 3. **Global config `model`** - Default model in `~/.takt/config.yaml` 4. **Provider default** - Falls back to provider's default (Claude: sonnet, Codex: gpt-5.2-codex) @@ -346,11 +346,11 @@ Session logs use NDJSON (`.jsonl`) format for real-time append-only writes. Reco | Record | Description | |--------|-------------| -| `workflow_start` | Workflow initialization with task, workflow name | +| `piece_start` | Piece initialization with task, piece name | | `step_start` | Step execution start | | `step_complete` | Step result with status, content, matched rule info | -| `workflow_complete` | Successful completion | -| `workflow_abort` | Abort with reason | +| `piece_complete` | Successful completion | +| `piece_abort` | Abort with reason | Files: `.takt/logs/{sessionId}.jsonl`, with `latest.json` pointer. Legacy `.json` format is still readable via `loadSessionLog()`. @@ -365,26 +365,26 @@ Files: `.takt/logs/{sessionId}.jsonl`, with `latest.json` pointer. Legacy `.json **Keep commands minimal.** One command per concept. Use arguments/modes instead of multiple similar commands. Before adding a new command, consider if existing commands can be extended. -**Do NOT expand schemas carelessly.** Rule conditions are free-form text (not enum-restricted). However, the engine's behavior depends on specific patterns (`ai()`, `all()`, `any()`). Do not add new special syntax without updating the loader's regex parsing in `workflowParser.ts`. +**Do NOT expand schemas carelessly.** Rule conditions are free-form text (not enum-restricted). However, the engine's behavior depends on specific patterns (`ai()`, `all()`, `any()`). Do not add new special syntax without updating the loader's regex parsing in `pieceParser.ts`. **Instruction auto-injection over explicit placeholders.** The instruction builder auto-injects `{task}`, `{previous_response}`, `{user_inputs}`, and status rules. Templates should contain only step-specific instructions, not boilerplate. -**Agent prompts contain only domain knowledge.** Agent prompt files (`resources/global/{lang}/agents/**/*.md`) must contain only domain expertise and behavioral principles — never workflow-specific procedures. Workflow-specific details (which reports to read, step routing, specific templates with hardcoded step names) belong in the workflow YAML's `instruction_template`. This keeps agents reusable across different workflows. +**Agent prompts contain only domain knowledge.** Agent prompt files (`resources/global/{lang}/agents/**/*.md`) must contain only domain expertise and behavioral principles — never piece-specific procedures. Piece-specific details (which reports to read, step routing, specific templates with hardcoded step names) belong in the piece YAML's `instruction_template`. This keeps agents reusable across different pieces. What belongs in agent prompts: - Role definition ("You are a ... specialist") - Domain expertise, review criteria, judgment standards - Do / Don't behavioral rules -- Tool usage knowledge (general, not workflow-specific) +- Tool usage knowledge (general, not piece-specific) -What belongs in workflow `instruction_template`: +What belongs in piece `instruction_template`: - Step-specific procedures ("Read these specific reports") - References to other steps or their outputs - Specific report file names or formats - Comment/output templates with hardcoded review type names -**Separation of concerns in workflow engine:** -- `WorkflowEngine` - Orchestration, state management, event emission +**Separation of concerns in piece engine:** +- `PieceEngine` - Orchestration, state management, event emission - `StepExecutor` - Single step execution (3-phase model) - `ParallelRunner` - Parallel step execution - `RuleEvaluator` - Rule matching and evaluation @@ -413,10 +413,10 @@ Key constraints: **Error handling flow:** 1. Provider error (Claude SDK / Codex) → `AgentResponse.error` -2. `StepExecutor` captures error → `WorkflowEngine` emits `step:complete` with error +2. `StepExecutor` captures error → `PieceEngine` emits `step:complete` with error 3. Error logged to session log (`.takt/logs/{sessionId}.jsonl`) 4. Console output shows error details -5. Workflow transitions to `ABORT` step if error is unrecoverable +5. Piece transitions to `ABORT` step if error is unrecoverable ## Debugging @@ -429,25 +429,25 @@ Debug logs are written to `.takt/logs/debug.log` (ndjson format). Log levels: `d **Verbose mode:** Create `.takt/verbose` file (empty file) to enable verbose console output. This automatically enables debug logging and sets log level to `debug`. -**Session logs:** All workflow executions are logged to `.takt/logs/{sessionId}.jsonl`. Use `tail -f .takt/logs/{sessionId}.jsonl` to monitor in real-time. +**Session logs:** All piece executions are logged to `.takt/logs/{sessionId}.jsonl`. Use `tail -f .takt/logs/{sessionId}.jsonl` to monitor in real-time. -**Testing with mocks:** Use `--provider mock` to test workflows without calling real AI APIs. Mock responses are deterministic and configurable via test fixtures. +**Testing with mocks:** Use `--provider mock` to test pieces without calling real AI APIs. Mock responses are deterministic and configurable via test fixtures. ## Testing Notes - Vitest for testing framework - Tests use file system fixtures in `__tests__/` subdirectories -- Mock workflows and agent configs for integration tests +- Mock pieces and agent configs for integration tests - Test single files: `npx vitest run src/__tests__/filename.test.ts` - Pattern matching: `npx vitest run -t "test pattern"` -- Integration tests: Tests with `it-` prefix are integration tests that simulate full workflow execution -- Engine tests: Tests with `engine-` prefix test specific WorkflowEngine scenarios (happy path, error handling, parallel execution, etc.) +- Integration tests: Tests with `it-` prefix are integration tests that simulate full piece execution +- Engine tests: Tests with `engine-` prefix test specific PieceEngine scenarios (happy path, error handling, parallel execution, etc.) ## Important Implementation Notes **Agent prompt resolution:** -- Agent paths in workflow YAML are resolved relative to the workflow file's directory -- `../agents/default/coder.md` resolves from workflow file location +- Agent paths in piece YAML are resolved relative to the piece file's directory +- `../agents/default/coder.md` resolves from piece file location - Built-in agents are loaded from `dist/resources/global/{lang}/agents/` - User agents are loaded from `~/.takt/agents/` or `.takt/agents.yaml` - If agent file doesn't exist, the agent string is used as inline system prompt @@ -476,7 +476,7 @@ Debug logs are written to `.takt/logs/debug.log` (ndjson format). Log levels: `d - **v0.3.8+:** When multiple `[STEP:N]` tags appear in output, **last match wins** (not first) - `ai()` conditions are evaluated by Claude/Codex, not by string matching - Aggregate conditions (`all()`, `any()`) only work in parallel parent steps -- Fail-fast: if rules exist but no rule matches, workflow aborts +- Fail-fast: if rules exist but no rule matches, piece aborts - Interactive-only rules are skipped in pipeline mode (`rule.interactiveOnly === true`) **Provider-specific behavior:** diff --git a/README.md b/README.md index 1b17697..d866196 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,32 @@ **T**ask **A**gent **K**oordination **T**ool - A governance-first orchestrator for running coding agents safely and responsibly -TAKT coordinates AI agents like Claude Code and Codex according to your organization's rules and workflows. It clarifies who is responsible, what is permitted, and how to recover from failures, while automating complex development tasks. +TAKT coordinates AI agents like Claude Code and Codex according to your organization's rules and pieces. It clarifies who is responsible, what is permitted, and how to recover from failures, while automating complex development tasks. TAKT is built with TAKT itself (dogfooding). +## Metaphor + +TAKT uses a music metaphor to describe orchestration: + +- **Piece**: A task execution definition (what to do and how agents coordinate) +- **Movement**: A step inside a piece (a single stage in the flow) +- **Orchestration**: The engine that coordinates agents across movements + +You can read every term as standard workflow language (piece = workflow, movement = step), but the metaphor is used to keep the system conceptually consistent. + ## TAKT is For Teams That Need -- **Want to integrate AI into CI/CD but fear runaway execution** — Clarify control scope with workflow definitions +- **Want to integrate AI into CI/CD but fear runaway execution** — Clarify control scope with piece definitions - **Want automated PR generation but need audit logs** — Record and track all execution history -- **Want to use multiple AI models but manage them uniformly** — Control Claude/Codex/Mock with the same workflow +- **Want to use multiple AI models but manage them uniformly** — Control Claude/Codex/Mock with the same piece - **Want to reproduce and debug agent failures** — Maintain complete history with session logs and reports ## What TAKT is NOT - **Not an autonomous engineer** — TAKT doesn't complete implementations itself; it governs and coordinates multiple agents -- **Not competing with Claude Code Swarm** — While leveraging Swarm's execution power, TAKT provides "operational guardrails" such as workflow definitions, permission controls, and audit logs -- **Not just a workflow engine** — TAKT is designed to address AI-specific challenges (non-determinism, accountability, audit requirements, and reproducibility) +- **Not competing with Claude Code Swarm** — While leveraging Swarm's execution power, TAKT provides "operational guardrails" such as piece definitions, permission controls, and audit logs +- **Not just a piece engine** — TAKT is designed to address AI-specific challenges (non-determinism, accountability, audit requirements, and reproducibility) ## Requirements @@ -71,17 +81,17 @@ takt hello **Note:** If you specify a string with spaces, Issue reference (`#6`), or `--task` / `--issue` options, interactive mode will be skipped and the task will be executed directly. **Flow:** -1. Select workflow +1. Select piece 2. Refine task content through conversation with AI 3. Finalize task instructions with `/go` (you can also add additional instructions like `/go additional instructions`) -4. Execute (create worktree, run workflow, create PR) +4. Execute (create worktree, run piece, create PR) #### Execution Example ``` $ takt -Select workflow: +Select piece: ❯ 🎼 default (current) 📁 Development/ 📁 Research/ @@ -110,7 +120,7 @@ Proceed with these task instructions? (Y/n) y ? Create worktree? (Y/n) y -[Workflow execution starts...] +[Piece execution starts...] ``` ### Direct Task Execution @@ -124,8 +134,8 @@ takt "Add login feature" # Specify task content with --task option takt --task "Fix bug" -# Specify workflow -takt "Add authentication" --workflow expert +# Specify piece +takt "Add authentication" --piece expert # Auto-create PR takt "Fix bug" --auto-pr @@ -140,8 +150,8 @@ You can execute GitHub Issues directly as tasks. Issue title, body, labels, and takt #6 takt --issue 6 -# Issue + workflow specification -takt #6 --workflow expert +# Issue + piece specification +takt #6 --piece expert # Issue + auto-create PR takt #6 --auto-pr @@ -186,7 +196,7 @@ takt list ### Pipeline Mode (for CI/Automation) -Specifying `--pipeline` enables non-interactive pipeline mode. Automatically creates branch → runs workflow → commits & pushes. Suitable for CI/CD automation. +Specifying `--pipeline` enables non-interactive pipeline mode. Automatically creates branch → runs piece → commits & pushes. Suitable for CI/CD automation. ```bash # Execute task in pipeline mode @@ -198,13 +208,13 @@ takt --pipeline --task "Fix bug" --auto-pr # Link issue information takt --pipeline --issue 99 --auto-pr -# Specify workflow and branch +# Specify piece and branch takt --pipeline --task "Fix bug" -w magi -b feat/fix-bug # Specify repository (for PR creation) takt --pipeline --task "Fix bug" --auto-pr --repo owner/repo -# Workflow execution only (skip branch creation, commit, push) +# Piece execution only (skip branch creation, commit, push) takt --pipeline --task "Fix bug" --skip-git # Minimal output mode (for CI) @@ -218,10 +228,10 @@ In pipeline mode, PRs are not created unless `--auto-pr` is specified. ### Other Commands ```bash -# Interactively switch workflows +# Interactively switch pieces takt switch -# Copy builtin workflows/agents to ~/.takt/ for customization +# Copy builtin pieces/agents to ~/.takt/ for customization takt eject # Clear agent conversation sessions @@ -231,13 +241,13 @@ takt clear takt config ``` -### Recommended Workflows +### Recommended Pieces -| Workflow | Recommended Use | +| Piece | Recommended Use | |----------|-----------------| | `default` | Serious development tasks. Used for TAKT's own development. Multi-stage review with parallel reviews (architect + security). | -| `minimal` | Simple fixes and straightforward tasks. Minimal workflow with basic review. | -| `review-fix-minimal` | Review & fix workflow. Specialized for iterative improvement based on review feedback. | +| `minimal` | Simple fixes and straightforward tasks. Minimal piece with basic review. | +| `review-fix-minimal` | Review & fix piece. Specialized for iterative improvement based on review feedback. | | `research` | Investigation and research. Autonomously executes research without asking questions. | ### Main Options @@ -247,23 +257,23 @@ takt config | `--pipeline` | **Enable pipeline (non-interactive) mode** — Required for CI/automation | | `-t, --task ` | Task content (alternative to GitHub Issue) | | `-i, --issue ` | GitHub issue number (same as `#N` in interactive mode) | -| `-w, --workflow ` | Workflow name or path to workflow YAML file | +| `-w, --piece ` | Piece name or path to piece YAML file | | `-b, --branch ` | Specify branch name (auto-generated if omitted) | | `--auto-pr` | Create PR (interactive: skip confirmation, pipeline: enable PR) | -| `--skip-git` | Skip branch creation, commit, and push (pipeline mode, workflow-only) | +| `--skip-git` | Skip branch creation, commit, and push (pipeline mode, piece-only) | | `--repo ` | Specify repository (for PR creation) | | `--create-worktree ` | Skip worktree confirmation prompt | | `-q, --quiet` | Minimal output mode: suppress AI output (for CI) | | `--provider ` | Override agent provider (claude\|codex\|mock) | | `--model ` | Override agent model | -## Workflows +## Pieces -TAKT uses YAML-based workflow definitions and rule-based routing. Builtin workflows are embedded in the package, with user workflows in `~/.takt/workflows/` taking priority. Use `takt eject` to copy builtins to `~/.takt/` for customization. +TAKT uses YAML-based piece definitions and rule-based routing. Builtin pieces are embedded in the package, with user pieces in `~/.takt/pieces/` taking priority. Use `takt eject` to copy builtins to `~/.takt/` for customization. -> **Note (v0.4.0)**: Internal terminology has changed from "step" to "movement" for workflow components. User-facing workflow files remain compatible, but if you customize workflows, you may see `movements:` instead of `steps:` in YAML files. The functionality remains the same. +> **Note (v0.4.0)**: Internal terminology has changed from "step" to "movement" for piece components. User-facing piece files remain compatible, but if you customize pieces, you may see `movements:` instead of `steps:` in YAML files. The functionality remains the same. -### Workflow Example +### Piece Example ```yaml name: default @@ -370,22 +380,22 @@ Execute sub-movements in parallel within a movement and evaluate with aggregate | AI judge | `ai("condition text")` | AI evaluates condition against agent output | | Aggregate | `all("X")` / `any("X")` | Aggregates parallel sub-movement matched conditions | -## Builtin Workflows +## Builtin Pieces -TAKT includes multiple builtin workflows: +TAKT includes multiple builtin pieces: -| Workflow | Description | +| Piece | Description | |----------|-------------| -| `default` | Full development workflow: plan → architecture design → implement → AI review → parallel review (architect + security) → supervisor approval. Includes fix loops at each review stage. | -| `minimal` | Quick workflow: plan → implement → review → supervisor. Minimal steps for fast iteration. | -| `review-fix-minimal` | Review-focused workflow: review → fix → supervisor. For iterative improvement based on review feedback. | -| `research` | Research workflow: planner → digger → supervisor. Autonomously executes research without asking questions. | -| `expert` | Full-stack development workflow: architecture, frontend, security, QA reviews with fix loops. | -| `expert-cqrs` | Full-stack development workflow (CQRS+ES specialized): CQRS+ES, frontend, security, QA reviews with fix loops. | +| `default` | Full development piece: plan → architecture design → implement → AI review → parallel review (architect + security) → supervisor approval. Includes fix loops at each review stage. | +| `minimal` | Quick piece: plan → implement → review → supervisor. Minimal steps for fast iteration. | +| `review-fix-minimal` | Review-focused piece: review → fix → supervisor. For iterative improvement based on review feedback. | +| `research` | Research piece: planner → digger → supervisor. Autonomously executes research without asking questions. | +| `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. | -| `review-only` | Read-only code review workflow that makes no changes. | +| `review-only` | Read-only code review piece that makes no changes. | -Use `takt switch` to switch workflows. +Use `takt switch` to switch pieces. ## Builtin Agents @@ -415,7 +425,7 @@ You are a code reviewer specialized in security. ## Model Selection -The `model` field (in workflow movements, agent config, or global config) is passed directly to the provider (Claude Code CLI / Codex SDK). TAKT does not resolve model aliases. +The `model` field (in piece movements, agent config, or global config) is passed directly to the provider (Claude Code CLI / Codex SDK). TAKT does not resolve model aliases. ### Claude Code @@ -429,14 +439,14 @@ The model string is passed to the Codex SDK. If unspecified, defaults to `codex` ``` ~/.takt/ # Global configuration directory -├── config.yaml # Global config (provider, model, workflow, etc.) -├── workflows/ # User workflow definitions (override builtins) +├── config.yaml # Global config (provider, model, piece, etc.) +├── pieces/ # User piece definitions (override builtins) │ └── custom.yaml └── agents/ # User agent prompt files (.md) └── my-agent.md .takt/ # Project-level configuration -├── config.yaml # Project config (current workflow, etc.) +├── config.yaml # Project config (current piece, etc.) ├── tasks/ # Pending task files (.yaml, .md) ├── completed/ # Completed tasks and reports ├── reports/ # Execution reports (auto-generated) @@ -444,7 +454,7 @@ The model string is passed to the Codex SDK. If unspecified, defaults to `codex` └── logs/ # NDJSON format session logs ├── latest.json # Pointer to current/latest session ├── previous.json # Pointer to previous session - └── {sessionId}.jsonl # NDJSON session log per workflow execution + └── {sessionId}.jsonl # NDJSON session log per piece execution ``` Builtin resources are embedded in the npm package (`dist/resources/`). User files in `~/.takt/` take priority. @@ -456,7 +466,7 @@ Configure default provider and model in `~/.takt/config.yaml`: ```yaml # ~/.takt/config.yaml language: en -default_workflow: default +default_piece: default log_level: info provider: claude # Default provider: claude or codex model: sonnet # Default model (optional) @@ -505,10 +515,10 @@ Priority: Environment variables > `config.yaml` settings | `{title}` | Commit message | Issue title | | `{issue}` | Commit message, PR body | Issue number | | `{issue_body}` | PR body | Issue body | -| `{report}` | PR body | Workflow execution report | +| `{report}` | PR body | Piece execution report | **Model Resolution Priority:** -1. Workflow movement `model` (highest priority) +1. Piece movement `model` (highest priority) 2. Custom agent `model` 3. Global config `model` 4. Provider default (Claude: sonnet, Codex: codex) @@ -519,14 +529,14 @@ Priority: Environment variables > `config.yaml` settings TAKT supports batch processing with task files in `.takt/tasks/`. Both `.yaml`/`.yml` and `.md` file formats are supported. -**YAML format** (recommended, supports worktree/branch/workflow options): +**YAML format** (recommended, supports worktree/branch/piece options): ```yaml # .takt/tasks/add-auth.yaml task: "Add authentication feature" worktree: true # Execute in isolated shared clone branch: "feat/add-auth" # Branch name (auto-generated if omitted) -workflow: "default" # Workflow specification (uses current if omitted) +piece: "default" # Piece specification (uses current if omitted) ``` **Markdown format** (simple, backward compatible): @@ -561,25 +571,25 @@ TAKT writes session logs in NDJSON (`.jsonl`) format to `.takt/logs/`. Each reco - `.takt/logs/latest.json` - Pointer to current (or latest) session - `.takt/logs/previous.json` - Pointer to previous session -- `.takt/logs/{sessionId}.jsonl` - NDJSON session log per workflow execution +- `.takt/logs/{sessionId}.jsonl` - NDJSON session log per piece execution -Record types: `workflow_start`, `step_start`, `step_complete`, `workflow_complete`, `workflow_abort` +Record types: `piece_start`, `step_start`, `step_complete`, `piece_complete`, `piece_abort` Agents can read `previous.json` to inherit context from the previous execution. Session continuation is automatic — just run `takt "task"` to continue from the previous session. -### Adding Custom Workflows +### Adding Custom Pieces -Add YAML files to `~/.takt/workflows/` or customize builtins with `takt eject`: +Add YAML files to `~/.takt/pieces/` or customize builtins with `takt eject`: ```bash -# Copy default workflow to ~/.takt/workflows/ and edit +# Copy default piece to ~/.takt/pieces/ and edit takt eject default ``` ```yaml -# ~/.takt/workflows/my-workflow.yaml -name: my-workflow -description: Custom workflow +# ~/.takt/pieces/my-piece.yaml +name: my-piece +description: Custom piece max_iterations: 5 initial_movement: analyze @@ -609,10 +619,10 @@ movements: ### Specifying Agents by Path -In workflow definitions, specify agents using file paths: +In piece definitions, specify agents using file paths: ```yaml -# Relative path from workflow file +# Relative path from piece file agent: ../agents/default/coder.md # Home directory @@ -622,24 +632,24 @@ agent: ~/.takt/agents/default/coder.md agent: /path/to/custom/agent.md ``` -### Workflow Variables +### Piece Variables Variables available in `instruction_template`: | Variable | Description | |----------|-------------| | `{task}` | Original user request (auto-injected if not in template) | -| `{iteration}` | Workflow-wide turn count (total steps executed) | +| `{iteration}` | Piece-wide turn count (total steps executed) | | `{max_iterations}` | Maximum iteration count | | `{movement_iteration}` | Per-movement iteration count (times this movement has been executed) | | `{previous_response}` | Output from previous movement (auto-injected if not in template) | -| `{user_inputs}` | Additional user inputs during workflow (auto-injected if not in template) | +| `{user_inputs}` | Additional user inputs during piece (auto-injected if not in template) | | `{report_dir}` | Report directory path (e.g., `.takt/reports/20250126-143052-task-summary`) | | `{report:filename}` | Expands to `{report_dir}/filename` (e.g., `{report:00-plan.md}`) | -### Workflow Design +### Piece Design -Elements needed for each workflow movement: +Elements needed for each piece movement: **1. Agent** - Markdown file containing system prompt: @@ -675,13 +685,13 @@ Special `next` values: `COMPLETE` (success), `ABORT` (failure) ## API Usage Example ```typescript -import { WorkflowEngine, loadWorkflow } from 'takt'; // npm install takt +import { PieceEngine, loadPiece } from 'takt'; // npm install takt -const config = loadWorkflow('default'); +const config = loadPiece('default'); if (!config) { - throw new Error('Workflow not found'); + throw new Error('Piece not found'); } -const engine = new WorkflowEngine(config, process.cwd(), 'My task'); +const engine = new PieceEngine(config, process.cwd(), 'My task'); engine.on('step:complete', (step, response) => { console.log(`${step.name}: ${response.status}`); @@ -700,7 +710,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md) for details. TAKT provides a GitHub Action for automating PR reviews and task execution. See [takt-action](https://github.com/nrslib/takt-action) for details. -**Workflow example** (see [.github/workflows/takt-action.yml](../.github/workflows/takt-action.yml) in this repository): +**Piece example** (see [.github/workflows/takt-action.yml](../.github/workflows/takt-action.yml) in this repository): ```yaml name: TAKT @@ -755,7 +765,7 @@ export TAKT_OPENAI_API_KEY=sk-... ## Documentation -- [Workflow Guide](./docs/workflows.md) - Creating and customizing workflows +- [Piece Guide](./docs/pieces.md) - Creating and customizing pieces - [Agent Guide](./docs/agents.md) - Configuring custom agents - [Changelog](../CHANGELOG.md) - Version history - [Security Policy](../SECURITY.md) - Vulnerability reporting diff --git a/SECURITY.md b/SECURITY.md index e81401c..f72dda6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -39,12 +39,12 @@ TAKT orchestrates AI agents that can execute code and access files. Users should - **Trusted Directories**: TAKT requires explicit configuration of trusted directories in `~/.takt/config.yaml` - **Agent Permissions**: Agents have access to tools like Bash, Edit, Write based on their configuration -- **Workflow Definitions**: Only use workflow files from trusted sources +- **Piece Definitions**: Only use piece files from trusted sources - **Session Logs**: Session logs in `.takt/logs/` may contain sensitive information ### Best Practices -1. Review workflow YAML files before using them +1. Review piece YAML files before using them 2. Keep TAKT updated to the latest version 3. Limit trusted directories to necessary paths only 4. Be cautious when using custom agents from untrusted sources diff --git a/docs/README.ja.md b/docs/README.ja.md index 14b6298..1c785ac 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -2,22 +2,30 @@ **T**ask **A**gent **K**oordination **T**ool - AIエージェントを「安全に」「責任を持って」運用するための協調制御システム -TAKTは、Claude CodeやCodexなどのAIエージェントを、組織のルールとワークフローに従って協調させます。誰が責任を持つか・どこまで許可するか・失敗時にどう戻すか を明確にしながら、複雑な開発タスクを自動化します。 +TAKTは、Claude CodeやCodexなどのAIエージェントを、組織のルールとピースに従って協調させます。誰が責任を持つか・どこまで許可するか・失敗時にどう戻すか を明確にしながら、複雑な開発タスクを自動化します。 TAKTはTAKT自身で開発されています(ドッグフーディング)。 +## メタファ + +TAKTはオーケストラをイメージした音楽メタファで用語を統一しています。 + +- **Piece**: タスク実行定義(何をどう協調させるか) +- **Movement**: ピース内の1ステップ(実行フローの1段階) +- **Orchestration**: ムーブメント間でエージェントを協調させるエンジン + ## TAKTが向いているチーム -- **CI/CDにAIを組み込みたいが、暴走が怖い** — ワークフロー定義で制御範囲を明確化 +- **CI/CDにAIを組み込みたいが、暴走が怖い** — ピース定義で制御範囲を明確化 - **PRの自動生成をしたいが、監査ログが必要** — 全ての実行履歴を記録・追跡可能 -- **複数のAIモデルを使い分けたいが、統一的に管理したい** — Claude/Codex/モックを同じワークフローで制御 +- **複数のAIモデルを使い分けたいが、統一的に管理したい** — Claude/Codex/モックを同じピースで制御 - **エージェントの失敗を再現・デバッグしたい** — セッションログとレポートで完全な履歴を保持 ## TAKTとは何でないか - **自律型AIエンジニアの代替ではありません** — TAKT自身が実装を完結するのではなく、複数のエージェントを統治・協調させます -- **Claude Code Swarmの競合ではありません** — Swarmの実行力を活かしつつ、TAKTはワークフロー/権限/監査ログなど「運用のガードレール」を提供します -- **単なるワークフローエンジンではありません** — 非決定性、責任所在、監査要件、再現性といったAI特有の課題に対応した設計です +- **Claude Code Swarmの競合ではありません** — Swarmの実行力を活かしつつ、TAKTはピース/権限/監査ログなど「運用のガードレール」を提供します +- **単なるピースエンジンではありません** — 非決定性、責任所在、監査要件、再現性といったAI特有の課題に対応した設計です ## 必要条件 @@ -69,17 +77,17 @@ takt hello **注意:** スペースを含む文字列や Issue 参照(`#6`)、`--task` / `--issue` オプションを指定すると、対話モードをスキップして直接タスク実行されます。 **フロー:** -1. ワークフロー選択 +1. ピース選択 2. AI との会話でタスク内容を整理 3. `/go` でタスク指示を確定(`/go 追加の指示` のように指示を追加することも可能) -4. 実行(worktree 作成、ワークフロー実行、PR 作成) +4. 実行(worktree 作成、ピース実行、PR 作成) #### 実行例 ``` $ takt -Select workflow: +Select piece: ❯ 🎼 default (current) 📁 Development/ 📁 Research/ @@ -108,7 +116,7 @@ Select workflow: ? Create worktree? (Y/n) y -[ワークフロー実行開始...] +[ピース実行開始...] ``` ### 直接タスク実行 @@ -122,8 +130,8 @@ takt "ログイン機能を追加する" # --task オプションでタスク内容を指定 takt --task "バグを修正" -# ワークフロー指定 -takt "認証機能を追加" --workflow expert +# ピース指定 +takt "認証機能を追加" --piece expert # PR 自動作成 takt "バグを修正" --auto-pr @@ -138,8 +146,8 @@ GitHub Issue を直接タスクとして実行できます。Issue のタイト takt #6 takt --issue 6 -# Issue + ワークフロー指定 -takt #6 --workflow expert +# Issue + ピース指定 +takt #6 --piece expert # Issue + PR自動作成 takt #6 --auto-pr @@ -184,7 +192,7 @@ takt list ### パイプラインモード(CI/自動化向け) -`--pipeline` を指定すると非対話のパイプラインモードに入ります。ブランチ作成 → ワークフロー実行 → commit & push を自動で行います。CI/CD での自動化に適しています。 +`--pipeline` を指定すると非対話のパイプラインモードに入ります。ブランチ作成 → ピース実行 → commit & push を自動で行います。CI/CD での自動化に適しています。 ```bash # タスクをパイプライン実行 @@ -196,13 +204,13 @@ takt --pipeline --task "バグを修正" --auto-pr # Issue情報を紐付け takt --pipeline --issue 99 --auto-pr -# ワークフロー・ブランチ指定 +# ピース・ブランチ指定 takt --pipeline --task "バグを修正" -w magi -b feat/fix-bug # リポジトリ指定(PR作成時) takt --pipeline --task "バグを修正" --auto-pr --repo owner/repo -# ワークフロー実行のみ(ブランチ作成・commit・pushをスキップ) +# ピース実行のみ(ブランチ作成・commit・pushをスキップ) takt --pipeline --task "バグを修正" --skip-git # 最小限の出力モード(CI向け) @@ -216,10 +224,10 @@ takt --pipeline --task "バグを修正" --quiet ### その他のコマンド ```bash -# ワークフローを対話的に切り替え +# ピースを対話的に切り替え takt switch -# ビルトインのワークフロー/エージェントを~/.takt/にコピーしてカスタマイズ +# ビルトインのピース/エージェントを~/.takt/にコピーしてカスタマイズ takt eject # エージェントの会話セッションをクリア @@ -229,13 +237,13 @@ takt clear takt config ``` -### おすすめワークフロー +### おすすめピース -| ワークフロー | おすすめ用途 | +| ピース | おすすめ用途 | |------------|------------| | `default` | 本格的な開発タスク。TAKT自身の開発で使用。アーキテクト+セキュリティの並列レビュー付き多段階レビュー。 | -| `minimal` | 簡単な修正やシンプルなタスク。基本的なレビュー付きの最小限のワークフロー。 | -| `review-fix-minimal` | レビュー&修正ワークフロー。レビューフィードバックに基づく反復的な改善に特化。 | +| `minimal` | 簡単な修正やシンプルなタスク。基本的なレビュー付きの最小限のピース。 | +| `review-fix-minimal` | レビュー&修正ピース。レビューフィードバックに基づく反復的な改善に特化。 | | `research` | 調査・リサーチ。質問せずに自律的にリサーチを実行。 | ### 主要なオプション @@ -245,23 +253,23 @@ takt config | `--pipeline` | **パイプライン(非対話)モードを有効化** — CI/自動化に必須 | | `-t, --task ` | タスク内容(GitHub Issueの代わり) | | `-i, --issue ` | GitHub Issue番号(対話モードでは `#N` と同じ) | -| `-w, --workflow ` | ワークフロー名、またはワークフローYAMLファイルのパス | +| `-w, --piece ` | ピース名、またはピースYAMLファイルのパス | | `-b, --branch ` | ブランチ名指定(省略時は自動生成) | | `--auto-pr` | PR作成(対話: 確認スキップ、パイプライン: PR有効化) | -| `--skip-git` | ブランチ作成・commit・pushをスキップ(パイプラインモード、ワークフロー実行のみ) | +| `--skip-git` | ブランチ作成・commit・pushをスキップ(パイプラインモード、ピース実行のみ) | | `--repo ` | リポジトリ指定(PR作成時) | | `--create-worktree ` | worktree確認プロンプトをスキップ | | `-q, --quiet` | 最小限の出力モード: AIの出力を抑制(CI向け) | | `--provider ` | エージェントプロバイダーを上書き(claude\|codex\|mock) | | `--model ` | エージェントモデルを上書き | -## ワークフロー +## ピース -TAKTはYAMLベースのワークフロー定義とルールベースルーティングを使用します。ビルトインワークフローはパッケージに埋め込まれており、`~/.takt/workflows/` のユーザーワークフローが優先されます。`takt eject` でビルトインを`~/.takt/`にコピーしてカスタマイズできます。 +TAKTはYAMLベースのピース定義とルールベースルーティングを使用します。ビルトインピースはパッケージに埋め込まれており、`~/.takt/pieces/` のユーザーピースが優先されます。`takt eject` でビルトインを`~/.takt/`にコピーしてカスタマイズできます。 -> **注記 (v0.4.0)**: ワークフローコンポーネントの内部用語が "step" から "movement" に変更されました。ユーザー向けのワークフローファイルは引き続き互換性がありますが、ワークフローをカスタマイズする場合、YAMLファイルで `movements:` の代わりに `movements:` が使用されることがあります。機能は同じです。 +> **注記 (v0.4.0)**: ピースコンポーネントの内部用語が "step" から "movement" に変更されました。ユーザー向けのピースファイルは引き続き互換性がありますが、ピースをカスタマイズする場合、YAMLファイルで `movements:` の代わりに `movements:` が使用されることがあります。機能は同じです。 -### ワークフローの例 +### ピースの例 ```yaml name: default @@ -368,22 +376,22 @@ movements: | AI判定 | `ai("条件テキスト")` | AIが条件をエージェント出力に対して評価 | | 集約 | `all("X")` / `any("X")` | パラレルサブムーブメントの結果を集約 | -## ビルトインワークフロー +## ビルトインピース -TAKTには複数のビルトインワークフローが同梱されています: +TAKTには複数のビルトインピースが同梱されています: -| ワークフロー | 説明 | +| ピース | 説明 | |------------|------| -| `default` | フル開発ワークフロー: 計画 → アーキテクチャ設計 → 実装 → AI レビュー → 並列レビュー(アーキテクト+セキュリティ)→ スーパーバイザー承認。各レビュー段階に修正ループあり。 | -| `minimal` | クイックワークフロー: 計画 → 実装 → レビュー → スーパーバイザー。高速イテレーション向けの最小構成。 | -| `review-fix-minimal` | レビュー重視ワークフロー: レビュー → 修正 → スーパーバイザー。レビューフィードバックに基づく反復改善向け。 | -| `research` | リサーチワークフロー: プランナー → ディガー → スーパーバイザー。質問せずに自律的にリサーチを実行。 | -| `expert` | フルスタック開発ワークフロー: アーキテクチャ、フロントエンド、セキュリティ、QA レビューと修正ループ。 | -| `expert-cqrs` | フルスタック開発ワークフロー(CQRS+ES特化): CQRS+ES、フロントエンド、セキュリティ、QA レビューと修正ループ。 | +| `default` | フル開発ピース: 計画 → アーキテクチャ設計 → 実装 → AI レビュー → 並列レビュー(アーキテクト+セキュリティ)→ スーパーバイザー承認。各レビュー段階に修正ループあり。 | +| `minimal` | クイックピース: 計画 → 実装 → レビュー → スーパーバイザー。高速イテレーション向けの最小構成。 | +| `review-fix-minimal` | レビュー重視ピース: レビュー → 修正 → スーパーバイザー。レビューフィードバックに基づく反復改善向け。 | +| `research` | リサーチピース: プランナー → ディガー → スーパーバイザー。質問せずに自律的にリサーチを実行。 | +| `expert` | フルスタック開発ピース: アーキテクチャ、フロントエンド、セキュリティ、QA レビューと修正ループ。 | +| `expert-cqrs` | フルスタック開発ピース(CQRS+ES特化): CQRS+ES、フロントエンド、セキュリティ、QA レビューと修正ループ。 | | `magi` | エヴァンゲリオンにインスパイアされた審議システム。3つの AI ペルソナ(MELCHIOR、BALTHASAR、CASPER)が分析し投票。 | -| `review-only` | 変更を加えない読み取り専用のコードレビューワークフロー。 | +| `review-only` | 変更を加えない読み取り専用のコードレビューピース。 | -`takt switch` でワークフローを切り替えられます。 +`takt switch` でピースを切り替えられます。 ## ビルトインエージェント @@ -413,7 +421,7 @@ Markdown ファイルでエージェントプロンプトを作成: ## モデル選択 -`model` フィールド(ワークフローのムーブメント、エージェント設定、グローバル設定)はプロバイダー(Claude Code CLI / Codex SDK)にそのまま渡されます。TAKTはモデルエイリアスの解決を行いません。 +`model` フィールド(ピースのムーブメント、エージェント設定、グローバル設定)はプロバイダー(Claude Code CLI / Codex SDK)にそのまま渡されます。TAKTはモデルエイリアスの解決を行いません。 ### Claude Code @@ -427,14 +435,14 @@ Claude Code はエイリアス(`opus`、`sonnet`、`haiku`、`opusplan`、`def ``` ~/.takt/ # グローバル設定ディレクトリ -├── config.yaml # グローバル設定(プロバイダー、モデル、ワークフロー等) -├── workflows/ # ユーザーワークフロー定義(ビルトインを上書き) +├── config.yaml # グローバル設定(プロバイダー、モデル、ピース等) +├── pieces/ # ユーザーピース定義(ビルトインを上書き) │ └── custom.yaml └── agents/ # ユーザーエージェントプロンプトファイル(.md) └── my-agent.md .takt/ # プロジェクトレベルの設定 -├── config.yaml # プロジェクト設定(現在のワークフロー等) +├── config.yaml # プロジェクト設定(現在のピース等) ├── tasks/ # 保留中のタスクファイル(.yaml, .md) ├── completed/ # 完了したタスクとレポート ├── reports/ # 実行レポート(自動生成) @@ -442,7 +450,7 @@ Claude Code はエイリアス(`opus`、`sonnet`、`haiku`、`opusplan`、`def └── logs/ # NDJSON 形式のセッションログ ├── latest.json # 現在/最新セッションへのポインタ ├── previous.json # 前回セッションへのポインタ - └── {sessionId}.jsonl # ワークフロー実行ごとの NDJSON セッションログ + └── {sessionId}.jsonl # ピース実行ごとの NDJSON セッションログ ``` ビルトインリソースはnpmパッケージ(`dist/resources/`)に埋め込まれています。`~/.takt/` のユーザーファイルが優先されます。 @@ -454,7 +462,7 @@ Claude Code はエイリアス(`opus`、`sonnet`、`haiku`、`opusplan`、`def ```yaml # ~/.takt/config.yaml language: ja -default_workflow: default +default_piece: default log_level: info provider: claude # デフォルトプロバイダー: claude または codex model: sonnet # デフォルトモデル(オプション) @@ -503,10 +511,10 @@ trusted_directories: | `{title}` | コミットメッセージ | Issueタイトル | | `{issue}` | コミットメッセージ、PR本文 | Issue番号 | | `{issue_body}` | PR本文 | Issue本文 | -| `{report}` | PR本文 | ワークフロー実行レポート | +| `{report}` | PR本文 | ピース実行レポート | **モデル解決の優先順位:** -1. ワークフローのムーブメントの `model`(最優先) +1. ピースのムーブメントの `model`(最優先) 2. カスタムエージェントの `model` 3. グローバル設定の `model` 4. プロバイダーデフォルト(Claude: sonnet、Codex: codex) @@ -517,14 +525,14 @@ trusted_directories: TAKT は `.takt/tasks/` 内のタスクファイルによるバッチ処理をサポートしています。`.yaml`/`.yml` と `.md` の両方のファイル形式に対応しています。 -**YAML形式**(推奨、worktree/branch/workflowオプション対応): +**YAML形式**(推奨、worktree/branch/pieceオプション対応): ```yaml # .takt/tasks/add-auth.yaml task: "認証機能を追加する" worktree: true # 隔離された共有クローンで実行 branch: "feat/add-auth" # ブランチ名(省略時は自動生成) -workflow: "default" # ワークフロー指定(省略時は現在のもの) +piece: "default" # ピース指定(省略時は現在のもの) ``` **Markdown形式**(シンプル、後方互換): @@ -559,25 +567,25 @@ TAKTはセッションログをNDJSON(`.jsonl`)形式で`.takt/logs/`に書 - `.takt/logs/latest.json` - 現在(または最新の)セッションへのポインタ - `.takt/logs/previous.json` - 前回セッションへのポインタ -- `.takt/logs/{sessionId}.jsonl` - ワークフロー実行ごとのNDJSONセッションログ +- `.takt/logs/{sessionId}.jsonl` - ピース実行ごとのNDJSONセッションログ -レコード種別: `workflow_start`, `step_start`, `step_complete`, `workflow_complete`, `workflow_abort` +レコード種別: `piece_start`, `step_start`, `step_complete`, `piece_complete`, `piece_abort` エージェントは`previous.json`を読み取って前回の実行コンテキストを引き継ぐことができます。セッション継続は自動的に行われます — `takt "タスク"`を実行するだけで前回のセッションから続行されます。 -### カスタムワークフローの追加 +### カスタムピースの追加 -`~/.takt/workflows/` に YAML ファイルを追加するか、`takt eject` でビルトインをカスタマイズします: +`~/.takt/pieces/` に YAML ファイルを追加するか、`takt eject` でビルトインをカスタマイズします: ```bash -# defaultワークフローを~/.takt/workflows/にコピーして編集 +# defaultピースを~/.takt/pieces/にコピーして編集 takt eject default ``` ```yaml -# ~/.takt/workflows/my-workflow.yaml -name: my-workflow -description: カスタムワークフロー +# ~/.takt/pieces/my-piece.yaml +name: my-piece +description: カスタムピース max_iterations: 5 initial_movement: analyze @@ -607,10 +615,10 @@ movements: ### エージェントをパスで指定する -ワークフロー定義ではファイルパスを使ってエージェントを指定します: +ピース定義ではファイルパスを使ってエージェントを指定します: ```yaml -# ワークフローファイルからの相対パス +# ピースファイルからの相対パス agent: ../agents/default/coder.md # ホームディレクトリ @@ -620,24 +628,24 @@ agent: ~/.takt/agents/default/coder.md agent: /path/to/custom/agent.md ``` -### ワークフロー変数 +### ピース変数 `instruction_template`で使用可能な変数: | 変数 | 説明 | |------|------| | `{task}` | 元のユーザーリクエスト(テンプレートになければ自動注入) | -| `{iteration}` | ワークフロー全体のターン数(実行された全ムーブメント数) | +| `{iteration}` | ピース全体のターン数(実行された全ムーブメント数) | | `{max_iterations}` | 最大イテレーション数 | | `{movement_iteration}` | ムーブメントごとのイテレーション数(このムーブメントが実行された回数) | | `{previous_response}` | 前のムーブメントの出力(テンプレートになければ自動注入) | -| `{user_inputs}` | ワークフロー中の追加ユーザー入力(テンプレートになければ自動注入) | +| `{user_inputs}` | ピース中の追加ユーザー入力(テンプレートになければ自動注入) | | `{report_dir}` | レポートディレクトリパス(例: `.takt/reports/20250126-143052-task-summary`) | | `{report:filename}` | `{report_dir}/filename` に展開(例: `{report:00-plan.md}`) | -### ワークフローの設計 +### ピースの設計 -各ワークフローのムーブメントに必要な要素: +各ピースのムーブメントに必要な要素: **1. エージェント** - システムプロンプトを含むMarkdownファイル: @@ -673,13 +681,13 @@ rules: ## API使用例 ```typescript -import { WorkflowEngine, loadWorkflow } from 'takt'; // npm install takt +import { PieceEngine, loadPiece } from 'takt'; // npm install takt -const config = loadWorkflow('default'); +const config = loadPiece('default'); if (!config) { - throw new Error('Workflow not found'); + throw new Error('Piece not found'); } -const engine = new WorkflowEngine(config, process.cwd(), 'My task'); +const engine = new PieceEngine(config, process.cwd(), 'My task'); engine.on('step:complete', (step, response) => { console.log(`${step.name}: ${response.status}`); @@ -698,7 +706,7 @@ await engine.run(); TAKTはPRレビューやタスク実行を自動化するGitHub Actionを提供しています。詳細は [takt-action](https://github.com/nrslib/takt-action) を参照してください。 -**ワークフロー例** (このリポジトリの [.github/workflows/takt-action.yml](../.github/workflows/takt-action.yml) を参照): +**ピース例** (このリポジトリの [.github/workflows/takt-action.yml](../.github/workflows/takt-action.yml) を参照): ```yaml name: TAKT @@ -716,7 +724,7 @@ jobs: issues: write pull-requests: write - movements: + steps: - name: Checkout uses: actions/checkout@v4 @@ -753,7 +761,7 @@ export TAKT_OPENAI_API_KEY=sk-... ## ドキュメント -- [Workflow Guide](./workflows.md) - ワークフローの作成とカスタマイズ +- [Piece Guide](./pieces.md) - ピースの作成とカスタマイズ - [Agent Guide](./agents.md) - カスタムエージェントの設定 - [Changelog](../CHANGELOG.md) - バージョン履歴 - [Security Policy](../SECURITY.md) - 脆弱性報告 diff --git a/docs/agents.md b/docs/agents.md index 09392a0..e9babaa 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -17,10 +17,10 @@ TAKT includes six built-in agents (located in `resources/global/{lang}/agents/de ## Specifying Agents -In workflow YAML, agents are specified by file path: +In piece YAML, agents are specified by file path: ```yaml -# Relative to workflow file directory +# Relative to piece file directory agent: ../agents/default/coder.md # Home directory @@ -52,7 +52,7 @@ You are a security-focused code reviewer. - Verify proper error handling ``` -> **Note**: Agents do NOT need to output status markers manually. The workflow engine auto-injects status output rules into agent instructions based on the step's `rules` configuration. Agents output `[STEP:N]` tags (where N is the 0-based rule index) which the engine uses for routing. +> **Note**: Agents do NOT need to output status markers manually. The piece engine auto-injects status output rules into agent instructions based on the step's `rules` configuration. Agents output `[STEP:N]` tags (where N is the 0-based rule index) which the engine uses for routing. ### Using agents.yaml @@ -74,7 +74,7 @@ agents: | Field | Description | |-------|-------------| -| `name` | Agent identifier (referenced in workflow steps) | +| `name` | Agent identifier (referenced in piece steps) | | `prompt_file` | Path to Markdown prompt file | | `prompt` | Inline prompt text (alternative to `prompt_file`) | | `allowed_tools` | List of tools the agent can use | @@ -113,7 +113,7 @@ agents: ``` ```yaml -# workflow.yaml +# piece.yaml steps: - name: implement agent: ../agents/default/coder.md diff --git a/docs/data-flow-diagrams.md b/docs/data-flow-diagrams.md index 1188dee..99936b0 100644 --- a/docs/data-flow-diagrams.md +++ b/docs/data-flow-diagrams.md @@ -4,14 +4,14 @@ ## 目次 -1. [シーケンス図: インタラクティブモードからワークフロー実行まで](#シーケンス図-インタラクティブモードからワークフロー実行まで) +1. [シーケンス図: インタラクティブモードからピース実行まで](#シーケンス図-インタラクティブモードからピース実行まで) 2. [フローチャート: 3フェーズステップ実行](#フローチャート-3フェーズステップ実行) 3. [フローチャート: ルール評価の5段階フォールバック](#フローチャート-ルール評価の5段階フォールバック) -4. [ステートマシン図: WorkflowEngineのステップ遷移](#ステートマシン図-workflowengineのステップ遷移) +4. [ステートマシン図: PieceEngineのステップ遷移](#ステートマシン図-pieceengineのステップ遷移) --- -## シーケンス図: インタラクティブモードからワークフロー実行まで +## シーケンス図: インタラクティブモードからピース実行まで ```mermaid sequenceDiagram @@ -20,8 +20,8 @@ sequenceDiagram participant Interactive as Interactive Layer participant Orchestration as Execution Orchestration participant TaskExec as Task Execution - participant WorkflowExec as Workflow Execution - participant Engine as WorkflowEngine + participant PieceExec as Piece Execution + participant Engine as PieceEngine participant StepExec as StepExecutor participant Provider as Provider Layer @@ -42,8 +42,8 @@ sequenceDiagram CLI->>Orchestration: selectAndExecuteTask(cwd, task) - Orchestration->>Orchestration: determineWorkflow() - Note over Orchestration: ワークフロー選択
(interactive or override) + Orchestration->>Orchestration: determinePiece() + Note over Orchestration: ピース選択
(interactive or override) Orchestration->>Orchestration: confirmAndCreateWorktree() Orchestration->>Provider: summarizeTaskName(task) @@ -51,17 +51,17 @@ sequenceDiagram Orchestration->>Orchestration: createSharedClone() Orchestration->>TaskExec: executeTask(options) - TaskExec->>TaskExec: loadWorkflowByIdentifier() - TaskExec->>WorkflowExec: executeWorkflow(config, task, cwd) + TaskExec->>TaskExec: loadPieceByIdentifier() + TaskExec->>PieceExec: executePiece(config, task, cwd) - WorkflowExec->>WorkflowExec: セッション管理初期化 - Note over WorkflowExec: loadAgentSessions()
generateSessionId()
initNdjsonLog() + PieceExec->>PieceExec: セッション管理初期化 + Note over PieceExec: loadAgentSessions()
generateSessionId()
initNdjsonLog() - WorkflowExec->>Engine: new WorkflowEngine(config, cwd, task, options) - WorkflowExec->>Engine: イベント購読 (step:start, step:complete, etc.) - WorkflowExec->>Engine: engine.run() + PieceExec->>Engine: new PieceEngine(config, cwd, task, options) + PieceExec->>Engine: イベント購読 (step:start, step:complete, etc.) + PieceExec->>Engine: engine.run() - loop ワークフローステップ + loop ピースステップ Engine->>StepExec: runStep(step) StepExec->>StepExec: InstructionBuilder.build() @@ -89,15 +89,15 @@ sequenceDiagram Engine->>Engine: resolveNextStep() alt nextStep === COMPLETE - Engine-->>WorkflowExec: ワークフロー完了 + Engine-->>PieceExec: ピース完了 else nextStep === ABORT - Engine-->>WorkflowExec: ワークフロー中断 + Engine-->>PieceExec: ピース中断 else 通常ステップ Engine->>Engine: state.currentStep = nextStep end end - WorkflowExec-->>TaskExec: { success: boolean } + PieceExec-->>TaskExec: { success: boolean } TaskExec-->>Orchestration: taskSuccess opt taskSuccess && isWorktree @@ -247,11 +247,11 @@ flowchart TD --- -## ステートマシン図: WorkflowEngineのステップ遷移 +## ステートマシン図: PieceEngineのステップ遷移 ```mermaid stateDiagram-v2 - [*] --> Initializing: new WorkflowEngine + [*] --> Initializing: new PieceEngine Initializing --> Running: engine.run() note right of Initializing @@ -312,20 +312,20 @@ stateDiagram-v2 Transition --> CheckAbort: state.currentStep = nextStep } - Running --> Completed: workflow:complete - Running --> Aborted: workflow:abort + Running --> Completed: piece:complete + Running --> Aborted: piece:abort Completed --> [*]: return state Aborted --> [*]: return state note right of Completed state.status = 'completed' - emit workflow:complete + emit piece:complete end note note right of Aborted state.status = 'aborted' - emit workflow:abort + emit piece:abort 原因: - User abort (Ctrl+C) - Iteration limit @@ -356,23 +356,23 @@ flowchart LR end subgraph Transform2 ["変換2: 環境準備"] - D1[determineWorkflow] + D1[determinePiece] D2[summarizeTaskName
AI呼び出し] D3[createSharedClone] end subgraph Execution ["実行環境"] - E1[workflowIdentifier] + E1[pieceIdentifier] E2[execCwd, branch] end subgraph Transform3 ["変換3: 設定読み込み"] - F1[loadWorkflowByIdentifier] + F1[loadPieceByIdentifier] F2[loadAgentSessions] end subgraph Config ["設定"] - G1[WorkflowConfig] + G1[PieceConfig] G2[initialSessions] end @@ -381,7 +381,7 @@ flowchart LR end subgraph State ["実行状態"] - I[WorkflowState] + I[PieceState] end subgraph Transform5 ["変換5: インストラクション"] @@ -555,7 +555,7 @@ flowchart TB 1. **シーケンス図**: 時系列での各レイヤー間のやりとり 2. **3フェーズフローチャート**: ステップ実行の詳細な処理フロー 3. **ルール評価フローチャート**: 5段階フォールバックの意思決定ロジック -4. **ステートマシン**: WorkflowEngineの状態遷移 +4. **ステートマシン**: PieceEngineの状態遷移 5. **データ変換図**: 各段階でのデータ形式変換 6. **コンテキスト蓄積図**: 実行が進むにつれてコンテキストが蓄積される様子 diff --git a/docs/data-flow.md b/docs/data-flow.md index 265b0e5..245dad1 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -1,6 +1,6 @@ # TAKTデータフロー解析 -このドキュメントでは、TAKTにおけるデータフロー、特にインタラクティブモードからワークフロー実行に至るまでのデータの流れを説明します。 +このドキュメントでは、TAKTにおけるデータフロー、特にインタラクティブモードからピース実行に至るまでのデータの流れを説明します。 ## 目次 @@ -18,8 +18,8 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され 1. **CLI Layer** - ユーザー入力の受付 2. **Interactive Layer** - タスクの対話的な明確化 -3. **Execution Orchestration Layer** - ワークフロー選択とworktree管理 -4. **Workflow Execution Layer** - セッション管理とイベント処理 +3. **Execution Orchestration Layer** - ピース選択とworktree管理 +4. **Piece Execution Layer** - セッション管理とイベント処理 5. **Engine Layer** - ステートマシンによるステップ実行 6. **Instruction Building Layer** - プロンプト生成 7. **Provider Layer** - AIプロバイダーとの通信 @@ -67,9 +67,9 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され │ (selectAndExecute.ts) │ │ │ │ ┌──────────────────────┐ │ -│ │ determineWorkflow() │ ← workflow選択 (interactive/override) │ +│ │ determinePiece() │ ← piece選択 (interactive/override) │ │ └─────────┬────────────┘ │ -│ │ workflowIdentifier: string │ +│ │ pieceIdentifier: string │ │ ▼ │ │ ┌──────────────────────────────────┐ │ │ │ confirmAndCreateWorktree() │ │ @@ -82,19 +82,19 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され │ │ executeTask() │ │ │ │ - task: string │ │ │ │ - cwd: string (実行ディレクトリ) │ │ -│ │ - workflowIdentifier: string │ │ +│ │ - pieceIdentifier: string │ │ │ │ - projectCwd: string (.takt/在処) │ │ │ └─────────┬────────────────────────┘ │ └────────────┼────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ -│ 4. Workflow Execution Layer │ -│ (workflowExecution.ts, taskExecution.ts) │ +│ 4. Piece Execution Layer │ +│ (pieceExecution.ts, taskExecution.ts) │ │ │ │ ┌────────────────────────────────┐ │ -│ │ loadWorkflowByIdentifier() │ │ -│ │ → WorkflowConfig │ │ +│ │ loadPieceByIdentifier() │ │ +│ │ → PieceConfig │ │ │ └────────┬───────────────────────┘ │ │ │ │ │ ▼ │ @@ -108,10 +108,10 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され │ │ │ │ ▼ │ │ ┌────────────────────────────────┐ │ -│ │ WorkflowEngine initialization │ │ +│ │ PieceEngine initialization │ │ │ │ │ │ -│ │ new WorkflowEngine( │ │ -│ │ config: WorkflowConfig, │ │ +│ │ new PieceEngine( │ │ +│ │ config: PieceConfig, │ │ │ │ cwd: string, │ │ │ │ task: string, │ │ │ │ options: { │ │ @@ -132,8 +132,8 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され │ │ - step:start │ │ │ │ - step:complete │ │ │ │ - step:report │ │ -│ │ - workflow:complete │ │ -│ │ - workflow:abort │ │ +│ │ - piece:complete │ │ +│ │ - piece:abort │ │ │ └────────┬───────────────────────┘ │ │ │ │ │ ▼ │ @@ -144,7 +144,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され │ ▼ ┌─────────────────────────────────────────────────────────────────┐ -│ 5. Engine Layer (WorkflowEngine.ts) │ +│ 5. Engine Layer (PieceEngine.ts) │ │ │ │ ┌────────────────────────────────────────┐ │ │ │ State Machine Loop │ │ @@ -222,7 +222,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され │ │ │ │ ││ │ │ │ InstructionBuilder.build() │ ││ │ │ │ ├─ Execution Context (cwd, permission) │ ││ -│ │ │ ├─ Workflow Context (iteration, step, report) │ ││ +│ │ │ ├─ Piece Context (iteration, step, report) │ ││ │ │ │ ├─ User Request ({task}) │ ││ │ │ │ ├─ Previous Response ({previous_response}) │ ││ │ │ │ ├─ Additional User Inputs ({user_inputs}) │ ││ @@ -317,11 +317,11 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され - パイプラインモード vs 通常モードの判定 **データ入力**: -- CLI引数: `task`, `--workflow`, `--issue`, など +- CLI引数: `task`, `--piece`, `--issue`, など **データ出力**: - `task: string` (タスク記述) -- `workflow: string | undefined` (ワークフロー名またはパス) +- `piece: string | undefined` (ピース名またはパス) - `createWorktree: boolean | undefined` - その他オプション @@ -362,15 +362,15 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され ### 3. Execution Orchestration Layer (`src/features/tasks/execute/selectAndExecute.ts`) -**役割**: ワークフロー選択とworktree管理 +**役割**: ピース選択とworktree管理 **主要な処理**: -1. **ワークフロー決定** (`determineWorkflow()`): +1. **ピース決定** (`determinePiece()`): - オーバーライド指定がある場合: - パス形式 → そのまま使用 - 名前形式 → バリデーション - - オーバーライドなし → インタラクティブ選択 (`selectWorkflow()`) + - オーバーライドなし → インタラクティブ選択 (`selectPiece()`) 2. **Worktree作成** (`confirmAndCreateWorktree()`): - ユーザー確認 (または `--create-worktree` フラグ) @@ -385,7 +385,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され **データ入力**: - `task: string` - `options?: SelectAndExecuteOptions`: - - `workflow?: string` + - `piece?: string` - `createWorktree?: boolean` - `autoPr?: boolean` - `agentOverrides?: TaskExecutionOptions` @@ -396,35 +396,35 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され --- -### 4. Workflow Execution Layer +### 4. Piece Execution Layer #### 4.1 Task Execution (`src/features/tasks/execute/taskExecution.ts`) -**役割**: ワークフロー読み込みと実行の橋渡し +**役割**: ピース読み込みと実行の橋渡し **主要な処理**: -1. `loadWorkflowByIdentifier()`: YAMLまたは名前からワークフロー設定を読み込み -2. `executeWorkflow()` を呼び出し +1. `loadPieceByIdentifier()`: YAMLまたは名前からピース設定を読み込み +2. `executePiece()` を呼び出し **データ入力**: - `ExecuteTaskOptions`: - `task: string` - `cwd: string` (実行ディレクトリ、cloneまたはプロジェクトルート) - - `workflowIdentifier: string` + - `pieceIdentifier: string` - `projectCwd: string` (`.takt/`がある場所) - `agentOverrides?: TaskExecutionOptions` **データ出力**: - `boolean` (成功/失敗) -#### 4.2 Workflow Execution (`src/features/tasks/execute/workflowExecution.ts`) +#### 4.2 Piece Execution (`src/features/tasks/execute/pieceExecution.ts`) **役割**: セッション管理、イベント購読、ログ記録 **主要な処理**: 1. **セッション管理**: - - `generateSessionId()`: ワークフローセッションID生成 + - `generateSessionId()`: ピースセッションID生成 - `loadAgentSessions()` / `loadWorktreeSessions()`: エージェントセッション復元 - `updateAgentSession()` / `updateWorktreeSession()`: セッション保存 @@ -433,9 +433,9 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され - `initNdjsonLog()`: NDJSON形式のログファイル初期化 - `updateLatestPointer()`: `latest.json` ポインタ更新 -3. **WorkflowEngine初期化**: +3. **PieceEngine初期化**: ```typescript - new WorkflowEngine(workflowConfig, cwd, task, { + new PieceEngine(pieceConfig, cwd, task, { onStream: streamHandler, // UI表示用ストリームハンドラ initialSessions: savedSessions, // 保存済みセッションID onSessionUpdate: sessionUpdateHandler, @@ -451,36 +451,36 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され - `step:start`: ステップ開始 → UI表示、NDJSON記録 - `step:complete`: ステップ完了 → UI表示、NDJSON記録、セッション更新 - `step:report`: レポートファイル出力 - - `workflow:complete`: ワークフロー完了 → 通知 - - `workflow:abort`: ワークフロー中断 → エラー通知 + - `piece:complete`: ピース完了 → 通知 + - `piece:abort`: ピース中断 → エラー通知 5. **SIGINT処理**: - 1回目: Graceful abort (`engine.abort()`) - 2回目: 強制終了 **データ入力**: -- `WorkflowConfig` +- `PieceConfig` - `task: string` - `cwd: string` -- `WorkflowExecutionOptions` +- `PieceExecutionOptions` **データ出力**: -- `WorkflowExecutionResult`: +- `PieceExecutionResult`: - `success: boolean` - `reason?: string` --- -### 5. Engine Layer (`src/core/workflow/engine/WorkflowEngine.ts`) +### 5. Engine Layer (`src/core/piece/engine/PieceEngine.ts`) -**役割**: ステートマシンによるワークフロー実行制御 +**役割**: ステートマシンによるピース実行制御 **主要な構成要素**: -1. **State管理** (`WorkflowState`): +1. **State管理** (`PieceState`): - `status`: 'running' | 'completed' | 'aborted' - `currentStep`: 現在実行中のステップ名 - - `iteration`: ワークフロー全体のイテレーション数 + - `iteration`: ピース全体のイテレーション数 - `stepIterations`: Map (ステップごとの実行回数) - `agentSessions`: Map (エージェントごとのセッションID) - `stepOutputs`: Map (各ステップの出力) @@ -540,20 +540,20 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され - `determineNextStepByRules()` で次ステップ名を取得 **データ入力**: -- `WorkflowConfig` +- `PieceConfig` - `cwd: string` - `task: string` -- `WorkflowEngineOptions` +- `PieceEngineOptions` **データ出力**: -- `WorkflowState` (最終状態) +- `PieceState` (最終状態) - イベント発行 (各ステップの進捗) --- ### 6. Instruction Building & Step Execution Layer -#### 6.1 Step Execution (`src/core/workflow/engine/StepExecutor.ts`) +#### 6.1 Step Execution (`src/core/piece/engine/StepExecutor.ts`) **役割**: 3フェーズモデルによるステップ実行 @@ -604,7 +604,7 @@ const match = await detectMatchedRule(step, response.content, tagContent, {...}) - `InstructionBuilder` を使用してインストラクション文字列を生成 - コンテキスト情報を渡す -#### 6.2 Instruction Building (`src/core/workflow/instruction/InstructionBuilder.ts`) +#### 6.2 Instruction Building (`src/core/piece/instruction/InstructionBuilder.ts`) **役割**: Phase 1用のインストラクション文字列生成 @@ -614,8 +614,8 @@ const match = await detectMatchedRule(step, response.content, tagContent, {...}) - Working directory - Permission rules (edit mode) -2. **Workflow Context**: - - Iteration (workflow-wide) +2. **Piece Context**: + - Iteration (piece-wide) - Step Iteration (per-step) - Step name - Report Directory/File info @@ -642,7 +642,7 @@ const match = await detectMatchedRule(step, response.content, tagContent, {...}) - `{task}`: ユーザーリクエスト - `{previous_response}`: 前ステップの出力 - `{user_inputs}`: 追加ユーザー入力 -- `{iteration}`: ワークフロー全体のイテレーション +- `{iteration}`: ピース全体のイテレーション - `{max_iterations}`: 最大イテレーション - `{step_iteration}`: ステップのイテレーション - `{report_dir}`: レポートディレクトリ @@ -753,9 +753,9 @@ async call( ### ステージ2: 実行環境準備 -**ワークフロー選択**: -- `--workflow` フラグ → 検証 -- なし → インタラクティブ選択 (`selectWorkflow()`) +**ピース選択**: +- `--piece` フラグ → 検証 +- なし → インタラクティブ選択 (`selectPiece()`) **Worktree作成** (オプション): - `confirmAndCreateWorktree()`: @@ -764,21 +764,21 @@ async call( - `createSharedClone()`: git clone --shared **データ**: -- `workflowIdentifier: string` +- `pieceIdentifier: string` - `{ execCwd, isWorktree, branch }` --- -### ステージ3: ワークフロー実行初期化 +### ステージ3: ピース実行初期化 **セッション管理**: - `loadAgentSessions()`: 保存済みセッション復元 -- `generateSessionId()`: ワークフローセッションID生成 +- `generateSessionId()`: ピースセッションID生成 - `initNdjsonLog()`: NDJSON ログファイル作成 -**WorkflowEngine作成**: +**PieceEngine作成**: ```typescript -new WorkflowEngine(workflowConfig, cwd, task, { +new PieceEngine(pieceConfig, cwd, task, { onStream, initialSessions, onSessionUpdate, @@ -790,7 +790,7 @@ new WorkflowEngine(workflowConfig, cwd, task, { ``` **データ**: -- `WorkflowState`: 初期状態 +- `PieceState`: 初期状態 - `currentStep = config.initialStep` - `iteration = 0` - `agentSessions = initialSessions` @@ -871,8 +871,8 @@ new WorkflowEngine(workflowConfig, cwd, task, { **遷移**: - `determineNextStepByRules()`: `rules[index].next` を取得 - 特殊ステップ: - - `COMPLETE`: ワークフロー完了 - - `ABORT`: ワークフロー中断 + - `COMPLETE`: ピース完了 + - `ABORT`: ピース中断 - 通常ステップ: `state.currentStep = nextStep` --- @@ -891,7 +891,7 @@ function buildTaskFromHistory(history: ConversationMessage[]): string { } ``` -**重要性**: インタラクティブモードで蓄積された会話全体が、後続のワークフロー実行で単一の `task` 文字列として扱われる。 +**重要性**: インタラクティブモードで蓄積された会話全体が、後続のピース実行で単一の `task` 文字列として扱われる。 --- @@ -912,15 +912,15 @@ await summarizeTaskName(task, { cwd }) --- -### 3. ワークフロー設定 → WorkflowState +### 3. ピース設定 → PieceState -**場所**: `src/core/workflow/engine/state-manager.ts` +**場所**: `src/core/piece/engine/state-manager.ts` ```typescript function createInitialState( - config: WorkflowConfig, - options: WorkflowEngineOptions -): WorkflowState { + config: PieceConfig, + options: PieceEngineOptions +): PieceState { return { status: 'running', currentStep: config.initialStep, @@ -939,10 +939,10 @@ function createInitialState( ### 4. コンテキスト → インストラクション文字列 -**場所**: `src/core/workflow/instruction/InstructionBuilder.ts` +**場所**: `src/core/piece/instruction/InstructionBuilder.ts` **入力**: -- `step: WorkflowStep` +- `step: PieceStep` - `context: InstructionContext` (task, iteration, previousOutput, userInputs, など) **処理**: @@ -958,13 +958,13 @@ function createInitialState( ### 5. AgentResponse → ルールマッチ -**場所**: `src/core/workflow/evaluation/RuleEvaluator.ts` +**場所**: `src/core/piece/evaluation/RuleEvaluator.ts` **入力**: -- `step: WorkflowStep` +- `step: PieceStep` - `content: string` (Phase 1 output) - `tagContent: string` (Phase 3 output) -- `state: WorkflowState` +- `state: PieceState` **処理**: 1. タグ検出 (`[STEP:0]`, `[STEP:1]`, ...) @@ -979,11 +979,11 @@ function createInitialState( ### 6. ルールマッチ → 次ステップ名 -**場所**: `src/core/workflow/engine/transitions.ts` +**場所**: `src/core/piece/engine/transitions.ts` ```typescript function determineNextStepByRules( - step: WorkflowStep, + step: PieceStep, matchedRuleIndex: number ): string | null { const rule = step.rules?.[matchedRuleIndex]; @@ -1022,7 +1022,7 @@ TAKTのデータフローは、**7つのレイヤー**を通じて、ユーザ 1. **Progressive Transformation**: データは各レイヤーで少しずつ変換され、次のレイヤーに渡される 2. **Context Accumulation**: タスク、イテレーション、ユーザー入力などのコンテキストが蓄積される 3. **Session Continuity**: エージェントセッションIDが保存・復元され、会話の継続性を保つ -4. **Event-Driven Architecture**: WorkflowEngineがイベントを発行し、UI、ログ、通知が連携 +4. **Event-Driven Architecture**: PieceEngineがイベントを発行し、UI、ログ、通知が連携 5. **3-Phase Execution**: メイン実行、レポート出力、ステータス判断の3段階で、明確な責任分離 6. **Rule-Based Routing**: ルール評価の5段階フォールバックで、柔軟かつ予測可能な遷移 diff --git a/docs/workflows.md b/docs/pieces.md similarity index 86% rename from docs/workflows.md rename to docs/pieces.md index 5510c76..ea193e6 100644 --- a/docs/workflows.md +++ b/docs/pieces.md @@ -1,29 +1,29 @@ -# Workflow Guide +# Piece Guide -This guide explains how to create and customize TAKT workflows. +This guide explains how to create and customize TAKT pieces. -## Workflow Basics +## Piece Basics -A workflow is a YAML file that defines a sequence of steps executed by AI agents. Each step specifies: +A piece is a YAML file that defines a sequence of steps executed by AI agents. Each step specifies: - Which agent to use - What instructions to give - Rules for routing to the next step ## File Locations -- Builtin workflows are embedded in the npm package (`dist/resources/`) -- `~/.takt/workflows/` — User workflows (override builtins with the same name) -- Use `takt eject ` to copy a builtin to `~/.takt/workflows/` for customization +- Builtin pieces are embedded in the npm package (`dist/resources/`) +- `~/.takt/pieces/` — User pieces (override builtins with the same name) +- Use `takt eject ` to copy a builtin to `~/.takt/pieces/` for customization -## Workflow Categories +## Piece Categories -ワークフローの選択 UI をカテゴリ分けしたい場合は、`workflow_categories` を設定します。 -詳細は `docs/workflow-categories.md` を参照してください。 +ピースの選択 UI をカテゴリ分けしたい場合は、`piece_categories` を設定します。 +詳細は `docs/piece-categories.md` を参照してください。 -## Workflow Schema +## Piece Schema ```yaml -name: my-workflow +name: my-piece description: Optional description max_iterations: 10 initial_step: first-step # Optional, defaults to first step @@ -54,11 +54,11 @@ steps: | Variable | Description | |----------|-------------| | `{task}` | Original user request (auto-injected if not in template) | -| `{iteration}` | Workflow-wide turn count (total steps executed) | +| `{iteration}` | Piece-wide turn count (total steps executed) | | `{max_iterations}` | Maximum iterations allowed | | `{step_iteration}` | Per-step iteration count (how many times THIS step has run) | | `{previous_response}` | Previous step's output (auto-injected if not in template) | -| `{user_inputs}` | Additional user inputs during workflow (auto-injected if not in template) | +| `{user_inputs}` | Additional user inputs during piece (auto-injected if not in template) | | `{report_dir}` | Report directory path (e.g., `.takt/reports/20250126-143052-task-summary`) | | `{report:filename}` | Resolves to `{report_dir}/filename` (e.g., `{report:00-plan.md}`) | @@ -88,8 +88,8 @@ rules: ### Special `next` Values -- `COMPLETE` — End workflow successfully -- `ABORT` — End workflow with failure +- `COMPLETE` — End piece successfully +- `ABORT` — End piece with failure ### Rule Field: `appendix` @@ -166,7 +166,7 @@ report: ## Examples -### Simple Implementation Workflow +### Simple Implementation Piece ```yaml name: simple-impl @@ -250,8 +250,8 @@ steps: ## Best Practices -1. **Keep iterations reasonable** — 10-30 is typical for development workflows +1. **Keep iterations reasonable** — 10-30 is typical for development pieces 2. **Use `edit: false` for review steps** — Prevent reviewers from modifying code 3. **Use descriptive step names** — Makes logs easier to read -4. **Test workflows incrementally** — Start simple, add complexity +4. **Test pieces incrementally** — Start simple, add complexity 5. **Use `/eject` to customize** — Copy a builtin as starting point rather than writing from scratch diff --git a/docs/plan.md b/docs/plan.md index 8e20f7e..131ed4d 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -10,11 +10,11 @@ - これ、エージェントのデータを挿入してないの……? - 全体的に - 音楽にひもづける - - つまり、workflowsをやめて pieces にする - - 現workflowファイルにあるstepsもmovementsにする(全ファイルの修正) + - つまり、piecesをやめて pieces にする + - 現pieceファイルにあるstepsもmovementsにする(全ファイルの修正) - stepという言葉はmovementになる。phaseもmovementが適しているだろう(これは interactive における phase のことをいっていない) - _language パラメータは消せ - - ワークフローを指定すると実際に送られるプロンプトを組み立てて表示する機能かツールを作れるか + - ピースを指定すると実際に送られるプロンプトを組み立てて表示する機能かツールを作れるか - メタ領域を用意して説明、どこで利用されるかの説明、使えるテンプレートとその説明をかいて、その他必要な情報あれば入れて。 - 英語と日本語が共通でもかならずファイルはわけて同じ文章を書いておく - 無駄な空行とか消してほしい diff --git a/package.json b/package.json index 6175086..2027b61 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "takt", "version": "0.4.1", - "description": "TAKT: Task Agent Koordination Tool - AI Agent Workflow Orchestration", + "description": "TAKT: Task Agent Koordination Tool - AI Agent Piece Orchestration", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { @@ -25,7 +25,7 @@ "ai", "agent", "orchestration", - "workflow", + "piece", "automation", "llm", "anthropic" diff --git a/resources/global/en/agents/default/ai-antipattern-reviewer.md b/resources/global/en/agents/default/ai-antipattern-reviewer.md index 741e495..68fa438 100644 --- a/resources/global/en/agents/default/ai-antipattern-reviewer.md +++ b/resources/global/en/agents/default/ai-antipattern-reviewer.md @@ -130,8 +130,8 @@ AI is confidently wrong—code that looks plausible but doesn't work, solutions ```typescript // ❌ Bad example - All callers omit -function loadWorkflow(name: string, cwd = process.cwd()) { ... } -// All callers: loadWorkflow('default') ← not passing cwd +function loadPiece(name: string, cwd = process.cwd()) { ... } +// All callers: loadPiece('default') ← not passing cwd // Problem: Can't tell where cwd value comes from by reading call sites // Fix: Make cwd required, pass explicitly at call sites diff --git a/resources/global/en/agents/default/architecture-reviewer.md b/resources/global/en/agents/default/architecture-reviewer.md index 728d2c3..1d50b39 100644 --- a/resources/global/en/agents/default/architecture-reviewer.md +++ b/resources/global/en/agents/default/architecture-reviewer.md @@ -56,7 +56,7 @@ Code is read far more often than it is written. Poorly structured code destroys **To avoid false positives:** 1. Before flagging "hardcoded values", **verify if the file is source or report** -2. Files under `.takt/reports/` are generated during workflow execution - not review targets +2. Files under `.takt/reports/` are generated during piece execution - not review targets 3. Ignore generated files even if they appear in git diff ## Review Perspectives @@ -186,7 +186,7 @@ for (const transition of step.transitions) { export function matchesCondition(status: Status, condition: TransitionCondition): boolean { // ✅ OK - Design decision (Why) -// User interruption takes priority over workflow-defined transitions +// User interruption takes priority over piece-defined transitions if (status === 'interrupted') { return ABORT_STEP; } @@ -361,7 +361,7 @@ function createUser(data: UserData) { - Documentation schema descriptions are updated - Existing config files are compatible with new schema -3. When workflow definitions are modified: +3. When piece definitions are modified: - Correct fields used for step type (normal vs. parallel) - No unnecessary fields remaining (e.g., `next` on parallel sub-steps) diff --git a/resources/global/en/agents/default/supervisor.md b/resources/global/en/agents/default/supervisor.md index 8bf6e78..989fd9a 100644 --- a/resources/global/en/agents/default/supervisor.md +++ b/resources/global/en/agents/default/supervisor.md @@ -20,7 +20,7 @@ you verify "**was the right thing built (Validation)**". ## Human-in-the-Loop Checkpoint -You are the **human proxy** in the automated workflow. Before approval, verify the following. +You are the **human proxy** in the automated piece. Before approval, verify the following. **Ask yourself what a human reviewer would check:** - Does this really solve the user's problem? @@ -92,16 +92,16 @@ Check: **REJECT if spec violations are found.** Don't assume "probably correct"—actually read and cross-reference the specs. -### 7. Workflow Overall Review +### 7. Piece Overall Review -**Check all reports in the report directory and verify overall workflow consistency.** +**Check all reports in the report directory and verify overall piece consistency.** Check: - Does implementation match the plan (00-plan.md)? - Were all review step issues properly addressed? - Was the original task objective achieved? -**Workflow-wide issues:** +**Piece-wide issues:** | Issue | Action | |-------|--------| | Plan-implementation gap | REJECT - Request plan revision or implementation fix | diff --git a/resources/global/en/config.yaml b/resources/global/en/config.yaml index b9a5427..56b56fc 100644 --- a/resources/global/en/config.yaml +++ b/resources/global/en/config.yaml @@ -7,8 +7,8 @@ language: en # Trusted directories - projects in these directories skip confirmation prompts trusted_directories: [] -# Default workflow to use when no workflow is specified -default_workflow: default +# Default piece to use when no piece is specified +default_piece: default # Log level: debug, info, warn, error log_level: info @@ -16,8 +16,8 @@ log_level: info # Provider runtime: claude or codex provider: claude -# Builtin workflows (resources/global/{lang}/workflows) -# enable_builtin_workflows: true +# Builtin pieces (resources/global/{lang}/pieces) +# enable_builtin_pieces: true # Default model (optional) # Claude: opus, sonnet, haiku, opusplan, default, or full model name diff --git a/resources/global/en/default-categories.yaml b/resources/global/en/default-categories.yaml index 2f19296..ded9597 100644 --- a/resources/global/en/default-categories.yaml +++ b/resources/global/en/default-categories.yaml @@ -1,11 +1,11 @@ -workflow_categories: +piece_categories: "🚀 Quick Start": - workflows: + pieces: - minimal - default "🔍 Review & Fix": - workflows: + pieces: - review-fix-minimal "🎨 Frontend": @@ -16,12 +16,12 @@ workflow_categories: "🔧 Expert": "Full Stack": - workflows: + pieces: - expert - expert-cqrs "Others": - workflows: + pieces: - research - magi - review-only diff --git a/resources/global/en/pieces/default.yaml b/resources/global/en/pieces/default.yaml index d938555..8d2d321 100644 --- a/resources/global/en/pieces/default.yaml +++ b/resources/global/en/pieces/default.yaml @@ -1,26 +1,26 @@ -# Default TAKT Workflow +# Default TAKT Piece # Plan -> Architect -> Implement -> AI Review -> Reviewers (parallel: Architect + Security) -> Supervisor Approval # -# Boilerplate sections (Workflow Context, User Request, Previous Response, +# 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} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 Workflow Context) +# 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 -description: Standard development workflow with planning and specialized reviews +description: Standard development piece with planning and specialized reviews max_iterations: 30 @@ -183,7 +183,7 @@ movements: - Plan: {report:00-plan.md} - Design: {report:01-architecture.md} (if exists) - Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. + 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. @@ -515,7 +515,7 @@ movements: instruction_template: | Run tests, verify the build, and perform final approval. - **Workflow Overall Review:** + **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? diff --git a/resources/global/en/pieces/expert-cqrs.yaml b/resources/global/en/pieces/expert-cqrs.yaml index 15e561f..a6c503f 100644 --- a/resources/global/en/pieces/expert-cqrs.yaml +++ b/resources/global/en/pieces/expert-cqrs.yaml @@ -1,5 +1,5 @@ -# Expert CQRS Review Workflow -# Review workflow with CQRS+ES, Frontend, Security, and QA experts +# Expert CQRS Review Piece +# Review piece with CQRS+ES, Frontend, Security, and QA experts # # Flow: # plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE @@ -10,12 +10,12 @@ # any("needs_fix") → fix → reviewers # # Template Variables: -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow +# {user_inputs} - Accumulated user inputs during piece # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") name: expert-cqrs @@ -101,7 +101,7 @@ movements: 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 Workflow Context. Do not search or open reports outside that directory. + 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 @@ -550,7 +550,7 @@ movements: Run tests, verify the build, and perform final approval. - **Workflow Overall Review:** + **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? diff --git a/resources/global/en/pieces/expert.yaml b/resources/global/en/pieces/expert.yaml index 0fea35d..6baf001 100644 --- a/resources/global/en/pieces/expert.yaml +++ b/resources/global/en/pieces/expert.yaml @@ -1,5 +1,5 @@ -# Expert Review Workflow -# Review workflow with Architecture, Frontend, Security, and QA experts +# Expert Review Piece +# Review piece with Architecture, Frontend, Security, and QA experts # # Flow: # plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE @@ -12,19 +12,19 @@ # AI review runs immediately after implementation to catch AI-specific issues early, # before expert reviews begin. # -# Boilerplate sections (Workflow Context, User Request, Previous Response, +# 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} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 Workflow Context) +# 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 @@ -113,7 +113,7 @@ movements: 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 Workflow Context. Do not search or open reports outside that directory. + 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 @@ -563,7 +563,7 @@ movements: Run tests, verify the build, and perform final approval. - **Workflow Overall Review:** + **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? diff --git a/resources/global/en/pieces/magi.yaml b/resources/global/en/pieces/magi.yaml index 4133ab8..036f099 100644 --- a/resources/global/en/pieces/magi.yaml +++ b/resources/global/en/pieces/magi.yaml @@ -1,14 +1,14 @@ -# MAGI System Workflow -# A deliberation workflow modeled after Evangelion's MAGI system +# MAGI System Piece +# A deliberation piece modeled after Evangelion's MAGI system # Three personas (scientist, nurturer, pragmatist) analyze from different perspectives and vote # # Template Variables: -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow +# {user_inputs} - Accumulated user inputs during piece # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") name: magi diff --git a/resources/global/en/pieces/minimal.yaml b/resources/global/en/pieces/minimal.yaml index be41bf7..45dd484 100644 --- a/resources/global/en/pieces/minimal.yaml +++ b/resources/global/en/pieces/minimal.yaml @@ -1,18 +1,18 @@ -# Minimal TAKT Workflow +# Minimal TAKT Piece # Implement -> Parallel Review (AI + Supervisor) -> Fix if needed -> Complete # (Simplest configuration - no plan, no architect review) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow (auto-injected) +# {user_inputs} - Accumulated user inputs during piece (auto-injected) # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") name: minimal -description: Minimal development workflow (implement -> parallel review -> fix if needed -> complete) +description: Minimal development piece (implement -> parallel review -> fix if needed -> complete) max_iterations: 20 @@ -37,7 +37,7 @@ movements: permission_mode: edit instruction_template: | Implement the task. - Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. + 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 @@ -154,7 +154,7 @@ movements: instruction_template: | Run tests, verify the build, and perform final approval. - **Workflow Overall Review:** + **Piece Overall Review:** 1. Does the implementation meet the original request? 2. Were AI Review issues addressed? 3. Was the original task objective achieved? diff --git a/resources/global/en/pieces/research.yaml b/resources/global/en/pieces/research.yaml index 299d93e..89f5766 100644 --- a/resources/global/en/pieces/research.yaml +++ b/resources/global/en/pieces/research.yaml @@ -1,5 +1,5 @@ -# Research Workflow -# A workflow that autonomously executes research tasks +# Research Piece +# A piece that autonomously executes research tasks # Planner creates the plan, Digger executes, Supervisor verifies # # Flow: @@ -7,16 +7,16 @@ # -> plan (rejected: restart from planning) # # Template Variables: -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow +# {user_inputs} - Accumulated user inputs during piece # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") name: research -description: Research workflow - autonomously executes research without asking questions +description: Research piece - autonomously executes research without asking questions max_iterations: 10 @@ -30,8 +30,8 @@ movements: - WebSearch - WebFetch instruction_template: | - ## Workflow Status - - Iteration: {iteration}/{max_iterations} (workflow-wide) + ## Piece Status + - Iteration: {iteration}/{max_iterations} (piece-wide) - Movement Iteration: {movement_iteration} (times this movement has run) - Movement: plan @@ -67,8 +67,8 @@ movements: - WebSearch - WebFetch instruction_template: | - ## Workflow Status - - Iteration: {iteration}/{max_iterations} (workflow-wide) + ## Piece Status + - Iteration: {iteration}/{max_iterations} (piece-wide) - Movement Iteration: {movement_iteration} (times this movement has run) - Movement: dig @@ -109,8 +109,8 @@ movements: - WebSearch - WebFetch instruction_template: | - ## Workflow Status - - Iteration: {iteration}/{max_iterations} (workflow-wide) + ## Piece Status + - Iteration: {iteration}/{max_iterations} (piece-wide) - Movement Iteration: {movement_iteration} (times this movement has run) - Movement: supervise (research quality evaluation) diff --git a/resources/global/en/pieces/review-fix-minimal.yaml b/resources/global/en/pieces/review-fix-minimal.yaml index 0a8beff..b1ea072 100644 --- a/resources/global/en/pieces/review-fix-minimal.yaml +++ b/resources/global/en/pieces/review-fix-minimal.yaml @@ -1,18 +1,18 @@ -# Review-Fix Minimal TAKT Workflow +# Review-Fix Minimal TAKT Piece # Review -> Fix (if needed) -> Re-review -> Complete # (Starts with review, no implementation movement) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow (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 -description: Review and fix workflow for existing code (starts with review, no implementation) +description: Review and fix piece for existing code (starts with review, no implementation) max_iterations: 20 @@ -37,7 +37,7 @@ movements: permission_mode: edit instruction_template: | Implement the task. - Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory. + 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 @@ -154,7 +154,7 @@ movements: instruction_template: | Run tests, verify the build, and perform final approval. - **Workflow Overall Review:** + **Piece Overall Review:** 1. Does the implementation meet the original request? 2. Were AI Review issues addressed? 3. Was the original task objective achieved? diff --git a/resources/global/en/pieces/review-only.yaml b/resources/global/en/pieces/review-only.yaml index 73fe0da..a113930 100644 --- a/resources/global/en/pieces/review-only.yaml +++ b/resources/global/en/pieces/review-only.yaml @@ -1,4 +1,4 @@ -# Review-Only Workflow +# Review-Only Piece # Reviews code or PRs without making any edits # Local: console output only. PR specified: posts inline comments + summary to PR # @@ -11,7 +11,7 @@ # All movements have edit: false (no file modifications) # # Template Variables: -# {iteration} - Workflow-wide turn count +# {iteration} - Piece-wide turn count # {max_iterations} - Maximum iterations allowed # {movement_iteration} - Per-movement iteration count # {task} - Original user request @@ -20,7 +20,7 @@ # {report_dir} - Report directory name name: review-only -description: Review-only workflow - reviews code without making edits +description: Review-only piece - reviews code without making edits max_iterations: 10 @@ -54,7 +54,7 @@ movements: Analyze the review request and create a review plan. - **This is a review-only workflow.** No code edits will be made. + **This is a review-only piece.** No code edits will be made. Focus on: 1. Identify which files/modules to review 2. Determine review focus areas (architecture, security, AI patterns, etc.) @@ -239,7 +239,7 @@ movements: ## Review Results {previous_response} - **This is a review-only workflow.** Do NOT run tests or builds. + **This is a review-only piece.** Do NOT run tests or builds. Your role is to synthesize the review results and produce a final summary. **Tasks:** @@ -326,5 +326,5 @@ movements: {Consolidated suggestions} --- - *Generated by [takt](https://github.com/toruticas/takt) review-only workflow* + *Generated by [takt](https://github.com/toruticas/takt) review-only piece* ``` diff --git a/resources/global/en/prompts/interactive-summary.md b/resources/global/en/prompts/interactive-summary.md index 3b1d8bf..392d13e 100644 --- a/resources/global/en/prompts/interactive-summary.md +++ b/resources/global/en/prompts/interactive-summary.md @@ -1,9 +1,9 @@ -You are responsible for instruction creation in TAKT's interactive mode. Convert the conversation into a concrete task instruction for workflow execution. +You are responsible for instruction creation in TAKT's interactive mode. Convert the conversation into a concrete task instruction for piece execution. ## Your position - You: Interactive mode (task organization and instruction creation) -- Next step: Your instruction will be passed to the workflow, where multiple AI agents execute sequentially -- Your output (instruction) becomes the input (task) for the entire workflow +- Next step: Your instruction will be passed to the piece, where multiple AI agents execute sequentially +- Your output (instruction) becomes the input (task) for the entire piece ## Requirements - Output only the final task instruction (no preamble). @@ -13,4 +13,4 @@ You are responsible for instruction creation in TAKT's interactive mode. Convert - Do not include constraints proposed or inferred by the assistant. - Do NOT include assistant/system operational constraints (tool limits, execution prohibitions). - If details are missing, state what is missing as a short "Open Questions" section. -- Clearly specify the concrete work that the workflow will execute. +- Clearly specify the concrete work that the piece will execute. diff --git a/resources/global/en/prompts/interactive-system.md b/resources/global/en/prompts/interactive-system.md index 9bdfc47..5ca1b24 100644 --- a/resources/global/en/prompts/interactive-system.md +++ b/resources/global/en/prompts/interactive-system.md @@ -1,23 +1,23 @@ -You are the interactive mode of TAKT (AI Agent Workflow Orchestration Tool). +You are the interactive mode of TAKT (AI Agent Piece Orchestration Tool). ## How TAKT works -1. **Interactive mode (your role)**: Talk with the user to clarify and organize the task, creating a concrete instruction document for workflow execution -2. **Workflow execution**: Pass your instruction document to the workflow, where multiple AI agents execute sequentially (implementation, review, fixes, etc.) +1. **Interactive mode (your role)**: Talk with the user to clarify and organize the task, creating a concrete instruction document for piece execution +2. **Piece execution**: Pass your instruction document to the piece, where multiple AI agents execute sequentially (implementation, review, fixes, etc.) ## Your role - Ask clarifying questions about ambiguous requirements - Clarify and refine the user's request into a clear task instruction -- Create concrete instructions for workflow agents to follow +- Create concrete instructions for piece agents to follow - Summarize your understanding when appropriate - Keep responses concise and focused ## Critical: Understanding user intent -**The user is asking YOU to create a task instruction for the WORKFLOW, not asking you to execute the task.** +**The user is asking YOU to create a task instruction for the PIECE, not asking you to execute the task.** When the user says: -- "Review this code" → They want the WORKFLOW to review (you create the instruction) -- "Implement feature X" → They want the WORKFLOW to implement (you create the instruction) -- "Fix this bug" → They want the WORKFLOW to fix (you create the instruction) +- "Review this code" → They want the PIECE to review (you create the instruction) +- "Implement feature X" → They want the PIECE to implement (you create the instruction) +- "Fix this bug" → They want the PIECE to fix (you create the instruction) These are NOT requests for YOU to investigate. Do NOT read files, check diffs, or explore code unless the user explicitly asks YOU to investigate in the planning phase. @@ -28,13 +28,13 @@ Only investigate when the user explicitly asks YOU (the planning assistant) to c - "What does this project do?" ✓ ## When investigation is NOT appropriate (most cases) -Do NOT investigate when the user is describing a task for the workflow: -- "Review the changes" ✗ (workflow's job) -- "Fix the code" ✗ (workflow's job) -- "Implement X" ✗ (workflow's job) +Do NOT investigate when the user is describing a task for the piece: +- "Review the changes" ✗ (piece's job) +- "Fix the code" ✗ (piece's job) +- "Implement X" ✗ (piece's job) ## Strict constraints -- You are ONLY refining requirements. The actual work (implementation/investigation/review) is done by workflow agents. +- You are ONLY refining requirements. The actual work (implementation/investigation/review) is done by piece agents. - Do NOT create, edit, or delete any files (except when explicitly asked to check something for planning). - Do NOT run build, test, install, or any commands that modify state. - Do NOT use Read/Glob/Grep/Bash proactively. Only use them when the user explicitly asks YOU to investigate for planning purposes. diff --git a/resources/global/ja/agents/default/ai-antipattern-reviewer.md b/resources/global/ja/agents/default/ai-antipattern-reviewer.md index 253cc32..10e21a1 100644 --- a/resources/global/ja/agents/default/ai-antipattern-reviewer.md +++ b/resources/global/ja/agents/default/ai-antipattern-reviewer.md @@ -153,8 +153,8 @@ AIは自信を持って間違える——もっともらしく見えるが動か ```typescript // ❌ 悪い例 - 全呼び出し元が省略している -function loadWorkflow(name: string, cwd = process.cwd()) { ... } -// 全呼び出し元: loadWorkflow('default') ← cwd を渡していない +function loadPiece(name: string, cwd = process.cwd()) { ... } +// 全呼び出し元: loadPiece('default') ← cwd を渡していない // 問題: cwd の値がどこから来るか、呼び出し元を見ても分からない // 修正: cwd を必須引数にし、呼び出し元で明示的に渡す diff --git a/resources/global/ja/agents/default/architecture-reviewer.md b/resources/global/ja/agents/default/architecture-reviewer.md index d6f3230..7bde241 100644 --- a/resources/global/ja/agents/default/architecture-reviewer.md +++ b/resources/global/ja/agents/default/architecture-reviewer.md @@ -56,7 +56,7 @@ **誤検知を避けるために:** 1. 「ハードコードされた値」を指摘する前に、**そのファイルがソースかレポートか確認** -2. `.takt/reports/` 以下のファイルはワークフロー実行時に生成されるため、レビュー対象外 +2. `.takt/reports/` 以下のファイルはピース実行時に生成されるため、レビュー対象外 3. git diff に含まれていても、生成ファイルは無視する ## レビュー観点 @@ -186,7 +186,7 @@ for (const transition of step.transitions) { export function matchesCondition(status: Status, condition: TransitionCondition): boolean { // ✅ OK - 設計判断の理由(Why) -// ユーザー中断はワークフロー定義のトランジションより優先する +// ユーザー中断はピース定義のトランジションより優先する if (status === 'interrupted') { return ABORT_STEP; } @@ -481,7 +481,7 @@ function createOrder(data: OrderData) { - ドキュメントのスキーマ説明が更新されているか - 既存の設定ファイルが新しいスキーマと整合するか -3. ワークフロー定義を変更した場合: +3. ピース定義を変更した場合: - ムーブメント種別(通常/parallel)に応じた正しいフィールドが使われているか - 不要なフィールド(parallelサブムーブメントのnext等)が残っていないか @@ -515,13 +515,13 @@ function createOrder(data: OrderData) { ```typescript // ❌ 配線漏れ: projectCwd を受け取る口がない -export async function executeWorkflow(config, cwd, task) { - const engine = new WorkflowEngine(config, cwd, task); // options なし +export async function executePiece(config, cwd, task) { + const engine = new PieceEngine(config, cwd, task); // options なし } // ✅ 配線済み: projectCwd を渡せる -export async function executeWorkflow(config, cwd, task, options?) { - const engine = new WorkflowEngine(config, cwd, task, options); +export async function executePiece(config, cwd, task, options?) { + const engine = new PieceEngine(config, cwd, task, options); } ``` diff --git a/resources/global/ja/agents/default/supervisor.md b/resources/global/ja/agents/default/supervisor.md index f4ee2b8..3331312 100644 --- a/resources/global/ja/agents/default/supervisor.md +++ b/resources/global/ja/agents/default/supervisor.md @@ -20,7 +20,7 @@ Architectが「正しく作られているか(Verification)」を確認す ## Human-in-the-Loop チェックポイント -あなたは自動化されたワークフローにおける**人間の代理**です。承認前に以下を確認してください。 +あなたは自動化されたピースにおける**人間の代理**です。承認前に以下を確認してください。 **人間のレビュアーなら何をチェックするか自問する:** - これは本当にユーザーの問題を解決しているか? @@ -92,16 +92,16 @@ Architectが「正しく作られているか(Verification)」を確認す **仕様違反を見つけたら REJECT。** 仕様は「たぶん合ってる」ではなく、実際に読んで突合する。 -### 7. ワークフロー全体の見直し +### 7. ピース全体の見直し -**レポートディレクトリ内の全レポートを確認し、ワークフロー全体の整合性をチェックする。** +**レポートディレクトリ内の全レポートを確認し、ピース全体の整合性をチェックする。** 確認すること: - 計画(00-plan.md)と実装結果が一致しているか - 各レビュームーブメントの指摘が適切に対応されているか - タスクの本来の目的が達成されているか -**ワークフロー全体の問題:** +**ピース全体の問題:** | 問題 | 対応 | |------|------| | 計画と実装の乖離 | REJECT - 計画の見直しまたは実装の修正を指示 | diff --git a/resources/global/ja/config.yaml b/resources/global/ja/config.yaml index d5da6aa..f082c0f 100644 --- a/resources/global/ja/config.yaml +++ b/resources/global/ja/config.yaml @@ -7,8 +7,8 @@ language: ja # 信頼済みディレクトリ - これらのディレクトリ内のプロジェクトは確認プロンプトをスキップします trusted_directories: [] -# デフォルトワークフロー - ワークフローが指定されていない場合に使用します -default_workflow: default +# デフォルトピース - ピースが指定されていない場合に使用します +default_piece: default # ログレベル: debug, info, warn, error log_level: info @@ -16,8 +16,8 @@ log_level: info # プロバイダー: claude または codex provider: claude -# ビルトインワークフローの読み込み (resources/global/{lang}/workflows) -# enable_builtin_workflows: true +# ビルトインピースの読み込み (resources/global/{lang}/pieces) +# enable_builtin_pieces: true # デフォルトモデル (オプション) # Claude: opus, sonnet, haiku, opusplan, default, またはフルモデル名 diff --git a/resources/global/ja/default-categories.yaml b/resources/global/ja/default-categories.yaml index 41b7a9f..bfd4ace 100644 --- a/resources/global/ja/default-categories.yaml +++ b/resources/global/ja/default-categories.yaml @@ -1,11 +1,11 @@ -workflow_categories: +piece_categories: "🚀 クイックスタート": - workflows: + pieces: - default - minimal "🔍 レビュー&修正": - workflows: + pieces: - review-fix-minimal "🎨 フロントエンド": @@ -15,12 +15,12 @@ workflow_categories: {} "🔧 フルスタック": - workflows: + pieces: - expert - expert-cqrs "その他": - workflows: + pieces: - research - magi - review-only diff --git a/resources/global/ja/pieces/default.yaml b/resources/global/ja/pieces/default.yaml index b3c7a37..dac8712 100644 --- a/resources/global/ja/pieces/default.yaml +++ b/resources/global/ja/pieces/default.yaml @@ -1,17 +1,17 @@ -# Default TAKT Workflow +# Default TAKT Piece # Plan -> Architect -> Implement -> AI Review -> Reviewers (parallel: Architect + Security) -> Supervisor Approval # # Template Variables (auto-injected by buildInstruction): -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow +# {user_inputs} - Accumulated user inputs during piece # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") name: default -description: Standard development workflow with planning and specialized reviews +description: Standard development piece with planning and specialized reviews max_iterations: 30 @@ -174,7 +174,7 @@ movements: - 計画: {report:00-plan.md} - 設計: {report:01-architecture.md}(存在する場合) - Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 + Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **重要:** 設計判断はせず、architectムーブメントで決定された設計に従ってください。 不明点や設計の変更が必要な場合は報告してください。 @@ -512,7 +512,7 @@ movements: instruction_template: | テスト実行、ビルド確認、最終承認を行ってください。 - **ワークフロー全体の確認:** + **ピース全体の確認:** 1. 計画({report:00-plan.md})と設計({report:01-architecture.md}、存在する場合)に従った実装か 2. 各レビュームーブメントの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/pieces/expert-cqrs.yaml b/resources/global/ja/pieces/expert-cqrs.yaml index 87065e0..c885ac8 100644 --- a/resources/global/ja/pieces/expert-cqrs.yaml +++ b/resources/global/ja/pieces/expert-cqrs.yaml @@ -1,5 +1,5 @@ -# Expert Review Workflow -# CQRS+ES、フロントエンド、セキュリティ、QAの専門家によるレビューワークフロー +# Expert Review Piece +# CQRS+ES、フロントエンド、セキュリティ、QAの専門家によるレビューピース # # フロー: # plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE @@ -9,19 +9,19 @@ # └─ qa-review # any("needs_fix") → fix → reviewers # -# ボイラープレートセクション(Workflow Context, User Request, Previous Response, +# ボイラープレートセクション(Piece Context, User Request, Previous Response, # Additional User Inputs, Instructions heading)はbuildInstruction()が自動挿入。 # instruction_templateにはムーブメント固有の内容のみ記述。 # # テンプレート変数(instruction_template内で使用可能): -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) -# {max_iterations} - ワークフローの最大イテレーション数 +# {iteration} - ピース全体のターン数(全エージェントで実行されたムーブメントの合計) +# {max_iterations} - ピースの最大イテレーション数 # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) # {previous_response} - 前のムーブメントの出力(pass_previous_response: true の場合のみ) # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") # # ムーブメントレベルフィールド: -# report: - ムーブメントのレポートファイル(Workflow ContextにReport File/Filesとして自動挿入) +# report: - ムーブメントのレポートファイル(Piece ContextにReport File/Filesとして自動挿入) # 単一: report: 00-plan.md # 複数: report: # - Scope: 01-coder-scope.md @@ -110,7 +110,7 @@ movements: instruction_template: | planムーブメントで立てた計画に従って実装してください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 - Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 + Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **Scopeレポートフォーマット(実装開始時に作成):** ```markdown @@ -558,7 +558,7 @@ movements: テスト実行、ビルド確認、最終承認を行ってください。 - **ワークフロー全体の確認:** + **ピース全体の確認:** 1. 計画({report:00-plan.md})と実装結果が一致しているか 2. 各レビュームーブメントの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/pieces/expert.yaml b/resources/global/ja/pieces/expert.yaml index 7297706..0b7e7b3 100644 --- a/resources/global/ja/pieces/expert.yaml +++ b/resources/global/ja/pieces/expert.yaml @@ -1,5 +1,5 @@ -# Expert Review Workflow -# アーキテクチャ、フロントエンド、セキュリティ、QAの専門家によるレビューワークフロー +# Expert Review Piece +# アーキテクチャ、フロントエンド、セキュリティ、QAの専門家によるレビューピース # # フロー: # plan -> implement -> ai_review -> reviewers (parallel) -> supervise -> COMPLETE @@ -10,12 +10,12 @@ # any("needs_fix") → fix → reviewers # # テンプレート変数: -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) -# {max_iterations} - ワークフローの最大イテレーション数 +# {iteration} - ピース全体のターン数(全エージェントで実行されたムーブメントの合計) +# {max_iterations} - ピースの最大イテレーション数 # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) # {task} - 元のユーザー要求 # {previous_response} - 前のムーブメントの出力 -# {user_inputs} - ワークフロー中に蓄積されたユーザー入力 +# {user_inputs} - ピース中に蓄積されたユーザー入力 # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") name: expert @@ -101,7 +101,7 @@ movements: instruction_template: | planムーブメントで立てた計画に従って実装してください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 - Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 + Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **Scopeレポートフォーマット(実装開始時に作成):** ```markdown @@ -549,7 +549,7 @@ movements: テスト実行、ビルド確認、最終承認を行ってください。 - **ワークフロー全体の確認:** + **ピース全体の確認:** 1. 計画({report:00-plan.md})と実装結果が一致しているか 2. 各レビュームーブメントの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/pieces/magi.yaml b/resources/global/ja/pieces/magi.yaml index 540eb7e..3d0951c 100644 --- a/resources/global/ja/pieces/magi.yaml +++ b/resources/global/ja/pieces/magi.yaml @@ -1,14 +1,14 @@ -# MAGI System Workflow -# エヴァンゲリオンのMAGIシステムを模した合議制ワークフロー +# MAGI System Piece +# エヴァンゲリオンのMAGIシステムを模した合議制ピース # 3つの人格(科学者・育成者・実務家)が異なる観点から分析・投票する # # テンプレート変数: -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) -# {max_iterations} - ワークフローの最大イテレーション数 +# {iteration} - ピース全体のターン数(全エージェントで実行されたムーブメントの合計) +# {max_iterations} - ピースの最大イテレーション数 # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) # {task} - 元のユーザー要求 # {previous_response} - 前のムーブメントの出力 -# {user_inputs} - ワークフロー中に蓄積されたユーザー入力 +# {user_inputs} - ピース中に蓄積されたユーザー入力 # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") name: magi diff --git a/resources/global/ja/pieces/minimal.yaml b/resources/global/ja/pieces/minimal.yaml index 302c727..830873b 100644 --- a/resources/global/ja/pieces/minimal.yaml +++ b/resources/global/ja/pieces/minimal.yaml @@ -1,18 +1,18 @@ -# Simple TAKT Workflow +# Simple TAKT Piece # Implement -> AI Review -> Supervisor Approval # (最もシンプルな構成 - plan, architect review, fix ムーブメントなし) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow (auto-injected) +# {user_inputs} - Accumulated user inputs during piece (auto-injected) # {report_dir} - Report directory name (e.g., "20250126-143052-task-summary") name: minimal -description: Minimal development workflow (implement -> parallel review -> fix if needed -> complete) +description: Minimal development piece (implement -> parallel review -> fix if needed -> complete) max_iterations: 20 @@ -37,7 +37,7 @@ movements: permission_mode: edit instruction_template: | タスクを実装してください。 - Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 + Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **Scopeレポートフォーマット(実装開始時に作成):** ```markdown @@ -154,7 +154,7 @@ movements: instruction_template: | テスト実行、ビルド確認、最終承認を行ってください。 - **ワークフロー全体の確認:** + **ピース全体の確認:** 1. 実装結果が元の要求を満たしているか 2. AI Reviewの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/pieces/research.yaml b/resources/global/ja/pieces/research.yaml index d5871c3..86b5de7 100644 --- a/resources/global/ja/pieces/research.yaml +++ b/resources/global/ja/pieces/research.yaml @@ -1,5 +1,5 @@ -# Research Workflow -# 調査タスクを自律的に実行するワークフロー +# Research Piece +# 調査タスクを自律的に実行するピース # Planner が計画を立て、Digger が実行し、Supervisor が確認する # # フロー: @@ -7,16 +7,16 @@ # -> plan (rejected: 計画からやり直し) # # テンプレート変数: -# {iteration} - ワークフロー全体のターン数(全エージェントで実行されたムーブメントの合計) -# {max_iterations} - ワークフローの最大イテレーション数 +# {iteration} - ピース全体のターン数(全エージェントで実行されたムーブメントの合計) +# {max_iterations} - ピースの最大イテレーション数 # {movement_iteration} - ムーブメントごとのイテレーション数(このムーブメントが何回実行されたか) # {task} - 元のユーザー要求 # {previous_response} - 前のムーブメントの出力 -# {user_inputs} - ワークフロー中に蓄積されたユーザー入力 +# {user_inputs} - ピース中に蓄積されたユーザー入力 # {report_dir} - レポートディレクトリ名(例: "20250126-143052-task-summary") name: research -description: 調査ワークフロー - 質問せずに自律的に調査を実行 +description: 調査ピース - 質問せずに自律的に調査を実行 max_iterations: 10 @@ -30,8 +30,8 @@ movements: - WebSearch - WebFetch instruction_template: | - ## ワークフロー状況 - - イテレーション: {iteration}/{max_iterations}(ワークフロー全体) + ## ピース状況 + - イテレーション: {iteration}/{max_iterations}(ピース全体) - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数) - ムーブメント: plan @@ -67,8 +67,8 @@ movements: - WebSearch - WebFetch instruction_template: | - ## ワークフロー状況 - - イテレーション: {iteration}/{max_iterations}(ワークフロー全体) + ## ピース状況 + - イテレーション: {iteration}/{max_iterations}(ピース全体) - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数) - ムーブメント: dig @@ -109,8 +109,8 @@ movements: - WebSearch - WebFetch instruction_template: | - ## ワークフロー状況 - - イテレーション: {iteration}/{max_iterations}(ワークフロー全体) + ## ピース状況 + - イテレーション: {iteration}/{max_iterations}(ピース全体) - ムーブメント実行回数: {movement_iteration}(このムーブメントの実行回数) - ムーブメント: supervise (調査品質評価) diff --git a/resources/global/ja/pieces/review-fix-minimal.yaml b/resources/global/ja/pieces/review-fix-minimal.yaml index 3e17487..c068946 100644 --- a/resources/global/ja/pieces/review-fix-minimal.yaml +++ b/resources/global/ja/pieces/review-fix-minimal.yaml @@ -1,18 +1,18 @@ -# Review-Fix Minimal TAKT Workflow +# Review-Fix Minimal TAKT Piece # Review -> Fix (if needed) -> Re-review -> Complete # (レビューから開始、実装ムーブメントなし) # # Template Variables (auto-injected): -# {iteration} - Workflow-wide turn count (total movements executed across all agents) -# {max_iterations} - Maximum iterations allowed for the workflow +# {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 workflow (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 -description: 既存コードのレビューと修正ワークフロー(レビュー開始、実装なし) +description: 既存コードのレビューと修正ピース(レビュー開始、実装なし) max_iterations: 20 @@ -37,7 +37,7 @@ movements: permission_mode: edit instruction_template: | タスクを実装してください。 - Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 + Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **Scopeレポートフォーマット(実装開始時に作成):** ```markdown @@ -154,7 +154,7 @@ movements: instruction_template: | テスト実行、ビルド確認、最終承認を行ってください。 - **ワークフロー全体の確認:** + **ピース全体の確認:** 1. 実装結果が元の要求を満たしているか 2. AI Reviewの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/pieces/review-only.yaml b/resources/global/ja/pieces/review-only.yaml index 07eb069..b24181f 100644 --- a/resources/global/ja/pieces/review-only.yaml +++ b/resources/global/ja/pieces/review-only.yaml @@ -1,4 +1,4 @@ -# レビュー専用ワークフロー +# レビュー専用ピース # コードやPRをレビューするだけで編集は行わない # ローカル: コンソール出力のみ。PR指定時: PRにインラインコメント+サマリを投稿 # @@ -11,7 +11,7 @@ # 全ムーブメント edit: false(ファイル変更なし) # # テンプレート変数: -# {iteration} - ワークフロー全体のターン数 +# {iteration} - ピース全体のターン数 # {max_iterations} - 最大イテレーション数 # {movement_iteration} - ムーブメントごとのイテレーション数 # {task} - 元のユーザー要求 @@ -20,7 +20,7 @@ # {report_dir} - レポートディレクトリ名 name: review-only -description: レビュー専用ワークフロー - コードをレビューするだけで編集は行わない +description: レビュー専用ピース - コードをレビューするだけで編集は行わない max_iterations: 10 @@ -54,7 +54,7 @@ movements: レビュー依頼を分析し、レビュー方針を立ててください。 - **これはレビュー専用ワークフローです。** コード編集は行いません。 + **これはレビュー専用ピースです。** コード編集は行いません。 以下に集中してください: 1. レビュー対象のファイル/モジュールを特定 2. レビューの重点領域を決定(アーキテクチャ、セキュリティ、AIパターン等) @@ -239,7 +239,7 @@ movements: ## レビュー結果 {previous_response} - **これはレビュー専用ワークフローです。** テスト実行やビルドは行わないでください。 + **これはレビュー専用ピースです。** テスト実行やビルドは行わないでください。 レビュー結果を統合し、最終サマリーを作成する役割です。 **やること:** @@ -327,5 +327,5 @@ movements: {統合された提案} --- - *[takt](https://github.com/toruticas/takt) review-only ワークフローで生成* + *[takt](https://github.com/toruticas/takt) review-only ピースで生成* ``` diff --git a/resources/global/ja/prompts/interactive-summary.md b/resources/global/ja/prompts/interactive-summary.md index 74b913b..85126a6 100644 --- a/resources/global/ja/prompts/interactive-summary.md +++ b/resources/global/ja/prompts/interactive-summary.md @@ -1,9 +1,9 @@ -あなたはTAKTの対話モードでの指示書作成を担当しています。これまでの会話内容を、ワークフロー実行用の具体的なタスク指示書に変換してください。 +あなたはTAKTの対話モードでの指示書作成を担当しています。これまでの会話内容を、ピース実行用の具体的なタスク指示書に変換してください。 ## 立ち位置 - あなた: 対話モード(タスク整理・指示書作成) -- 次のステップ: あなたが作成した指示書がワークフローに渡され、複数のAIエージェントが順次実行する -- あなたの成果物(指示書)が、ワークフロー全体の入力(タスク)になる +- 次のステップ: あなたが作成した指示書がピースに渡され、複数のAIエージェントが順次実行する +- あなたの成果物(指示書)が、ピース全体の入力(タスク)になる ## 要件 - 出力はタスク指示書のみ(前置き不要) @@ -13,4 +13,4 @@ - アシスタントが提案・推測した制約は指示書に含めない - アシスタントの運用上の制約(実行禁止/ツール制限など)は指示に含めない - 情報不足があれば「Open Questions」セクションを短く付ける -- ワークフローが実行する具体的な作業内容を明記する +- ピースが実行する具体的な作業内容を明記する diff --git a/resources/global/ja/prompts/interactive-system.md b/resources/global/ja/prompts/interactive-system.md index 3374864..486bd26 100644 --- a/resources/global/ja/prompts/interactive-system.md +++ b/resources/global/ja/prompts/interactive-system.md @@ -1,23 +1,23 @@ -あなたはTAKT(AIエージェントワークフローオーケストレーションツール)の対話モードを担当しています。 +あなたはTAKT(AIエージェントピースオーケストレーションツール)の対話モードを担当しています。 ## TAKTの仕組み -1. **対話モード(今ここ・あなたの役割)**: ユーザーと会話してタスクを整理し、ワークフロー実行用の具体的な指示書を作成する -2. **ワークフロー実行**: あなたが作成した指示書をワークフローに渡し、複数のAIエージェントが順次実行する(実装、レビュー、修正など) +1. **対話モード(今ここ・あなたの役割)**: ユーザーと会話してタスクを整理し、ピース実行用の具体的な指示書を作成する +2. **ピース実行**: あなたが作成した指示書をピースに渡し、複数のAIエージェントが順次実行する(実装、レビュー、修正など) ## 役割 - あいまいな要求に対して確認質問をする - ユーザーの要求を明確化し、指示書として洗練させる -- ワークフローのエージェントが迷わないよう具体的な指示書を作成する +- ピースのエージェントが迷わないよう具体的な指示書を作成する - 必要に応じて理解した内容を簡潔にまとめる - 返答は簡潔で要点のみ ## 重要:ユーザーの意図を理解する -**ユーザーは「あなた」に作業を依頼しているのではなく、「ワークフロー」への指示書作成を依頼しています。** +**ユーザーは「あなた」に作業を依頼しているのではなく、「ピース」への指示書作成を依頼しています。** ユーザーが次のように言った場合: -- 「このコードをレビューして」→ ワークフローにレビューさせる(あなたは指示書を作成) -- 「機能Xを実装して」→ ワークフローに実装させる(あなたは指示書を作成) -- 「このバグを修正して」→ ワークフローに修正させる(あなたは指示書を作成) +- 「このコードをレビューして」→ ピースにレビューさせる(あなたは指示書を作成) +- 「機能Xを実装して」→ ピースに実装させる(あなたは指示書を作成) +- 「このバグを修正して」→ ピースに修正させる(あなたは指示書を作成) これらは「あなた」への調査依頼ではありません。ファイルを読んだり、差分を確認したり、コードを探索したりしないでください。 @@ -28,13 +28,13 @@ - 「このプロジェクトは何をするもの?」✓ ## 調査が不適切な場合(ほとんどのケース) -ユーザーがワークフロー向けのタスクを説明している場合は調査しない: -- 「変更をレビューして」✗(ワークフローの仕事) -- 「コードを修正して」✗(ワークフローの仕事) -- 「Xを実装して」✗(ワークフローの仕事) +ユーザーがピース向けのタスクを説明している場合は調査しない: +- 「変更をレビューして」✗(ピースの仕事) +- 「コードを修正して」✗(ピースの仕事) +- 「Xを実装して」✗(ピースの仕事) ## 厳守事項 -- あなたは要求の明確化のみを行う。実際の作業(実装/調査/レビュー等)はワークフローのエージェントが行う +- あなたは要求の明確化のみを行う。実際の作業(実装/調査/レビュー等)はピースのエージェントが行う - ファイルの作成/編集/削除はしない(計画目的で明示的に依頼された場合を除く) - build/test/install など状態を変えるコマンドは実行しない - Read/Glob/Grep/Bash を勝手に使わない。ユーザーが明示的に「あなた」に調査を依頼した場合のみ使用 diff --git a/resources/project/tasks/TASK-FORMAT b/resources/project/tasks/TASK-FORMAT index 9d31e80..fcf8740 100644 --- a/resources/project/tasks/TASK-FORMAT +++ b/resources/project/tasks/TASK-FORMAT @@ -9,13 +9,13 @@ Tasks placed in this directory (.takt/tasks/) will be processed by TAKT. task: "Task description" worktree: true # (optional) true | "/path/to/dir" branch: "feat/my-feature" # (optional) branch name - workflow: "default" # (optional) workflow name + piece: "default" # (optional) piece name Fields: task (required) Task description (string) worktree (optional) true: create shared clone, "/path": clone at path branch (optional) Branch name (auto-generated if omitted: takt/{timestamp}-{slug}) - workflow (optional) Workflow name (uses current workflow if omitted) + piece (optional) Piece name (uses current piece if omitted) ## Markdown Format (Simple) diff --git a/src/__tests__/addTask.test.ts b/src/__tests__/addTask.test.ts index 84fe063..f936c57 100644 --- a/src/__tests__/addTask.test.ts +++ b/src/__tests__/addTask.test.ts @@ -18,7 +18,7 @@ vi.mock('../infra/providers/index.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../shared/prompt/index.js', () => ({ @@ -46,11 +46,11 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ })); vi.mock('../features/tasks/execute/selectAndExecute.js', () => ({ - determineWorkflow: vi.fn(), + determinePiece: vi.fn(), })); -vi.mock('../infra/config/loaders/workflowResolver.js', () => ({ - getWorkflowDescription: vi.fn(() => ({ name: 'default', description: '' })), +vi.mock('../infra/config/loaders/pieceResolver.js', () => ({ + getPieceDescription: vi.fn(() => ({ name: 'default', description: '' })), })); vi.mock('../infra/github/issue.js', () => ({ @@ -71,8 +71,8 @@ vi.mock('../infra/github/issue.js', () => ({ import { interactiveMode } from '../features/interactive/index.js'; import { promptInput, confirm } from '../shared/prompt/index.js'; import { summarizeTaskName } from '../infra/task/summarize.js'; -import { determineWorkflow } from '../features/tasks/execute/selectAndExecute.js'; -import { getWorkflowDescription } from '../infra/config/loaders/workflowResolver.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 { addTask } from '../features/tasks/index.js'; @@ -81,8 +81,8 @@ const mockInteractiveMode = vi.mocked(interactiveMode); const mockPromptInput = vi.mocked(promptInput); const mockConfirm = vi.mocked(confirm); const mockSummarizeTaskName = vi.mocked(summarizeTaskName); -const mockDetermineWorkflow = vi.mocked(determineWorkflow); -const mockGetWorkflowDescription = vi.mocked(getWorkflowDescription); +const mockDeterminePiece = vi.mocked(determinePiece); +const mockGetPieceDescription = vi.mocked(getPieceDescription); function setupFullFlowMocks(overrides?: { task?: string; @@ -91,8 +91,8 @@ function setupFullFlowMocks(overrides?: { const task = overrides?.task ?? '# 認証機能追加\nJWT認証を実装する'; const slug = overrides?.slug ?? 'add-auth'; - mockDetermineWorkflow.mockResolvedValue('default'); - mockGetWorkflowDescription.mockReturnValue({ name: 'default', description: '' }); + mockDeterminePiece.mockResolvedValue('default'); + mockGetPieceDescription.mockReturnValue({ name: 'default', description: '' }); mockInteractiveMode.mockResolvedValue({ confirmed: true, task }); mockSummarizeTaskName.mockResolvedValue(slug); mockConfirm.mockResolvedValue(false); @@ -103,8 +103,8 @@ let testDir: string; beforeEach(() => { vi.clearAllMocks(); testDir = fs.mkdtempSync(path.join(tmpdir(), 'takt-test-')); - mockDetermineWorkflow.mockResolvedValue('default'); - mockGetWorkflowDescription.mockReturnValue({ name: 'default', description: '' }); + mockDeterminePiece.mockResolvedValue('default'); + mockGetPieceDescription.mockReturnValue({ name: 'default', description: '' }); mockConfirm.mockResolvedValue(false); }); @@ -117,7 +117,7 @@ afterEach(() => { describe('addTask', () => { it('should cancel when interactive mode is not confirmed', async () => { // Given: user cancels interactive mode - mockDetermineWorkflow.mockResolvedValue('default'); + mockDeterminePiece.mockResolvedValue('default'); mockInteractiveMode.mockResolvedValue({ confirmed: false, task: '' }); // When @@ -221,48 +221,48 @@ describe('addTask', () => { expect(content).toContain('branch: feat/my-branch'); }); - it('should include workflow selection in task file', async () => { - // Given: determineWorkflow returns a non-default workflow - setupFullFlowMocks({ slug: 'with-workflow' }); - mockDetermineWorkflow.mockResolvedValue('review'); - mockGetWorkflowDescription.mockReturnValue({ name: 'review', description: 'Code review workflow' }); + it('should include piece selection in task file', async () => { + // Given: determinePiece returns a non-default piece + setupFullFlowMocks({ slug: 'with-piece' }); + mockDeterminePiece.mockResolvedValue('review'); + mockGetPieceDescription.mockReturnValue({ name: 'review', description: 'Code review piece' }); mockConfirm.mockResolvedValue(false); // When await addTask(testDir); // Then - const taskFile = path.join(testDir, '.takt', 'tasks', 'with-workflow.yaml'); + const taskFile = path.join(testDir, '.takt', 'tasks', 'with-piece.yaml'); const content = fs.readFileSync(taskFile, 'utf-8'); - expect(content).toContain('workflow: review'); + expect(content).toContain('piece: review'); }); - it('should cancel when workflow selection returns null', async () => { - // Given: user cancels workflow selection - mockDetermineWorkflow.mockResolvedValue(null); + it('should cancel when piece selection returns null', async () => { + // Given: user cancels piece selection + mockDeterminePiece.mockResolvedValue(null); // When await addTask(testDir); - // Then: no task file created (cancelled at workflow selection) + // Then: no task file created (cancelled at piece selection) const tasksDir = path.join(testDir, '.takt', 'tasks'); const files = fs.readdirSync(tasksDir); expect(files.length).toBe(0); }); - it('should always include workflow from determineWorkflow', async () => { - // Given: determineWorkflow returns 'default' + it('should always include piece from determinePiece', async () => { + // Given: determinePiece returns 'default' setupFullFlowMocks({ slug: 'default-wf' }); - mockDetermineWorkflow.mockResolvedValue('default'); + mockDeterminePiece.mockResolvedValue('default'); mockConfirm.mockResolvedValue(false); // When await addTask(testDir); - // Then: workflow field is included + // Then: piece field is included const taskFile = path.join(testDir, '.takt', 'tasks', 'default-wf.yaml'); const content = fs.readFileSync(taskFile, 'utf-8'); - expect(content).toContain('workflow: default'); + expect(content).toContain('piece: default'); }); it('should fetch issue and use directly as task content when given issue reference', async () => { diff --git a/src/__tests__/apiKeyAuth.test.ts b/src/__tests__/apiKeyAuth.test.ts index a80c43e..59fc579 100644 --- a/src/__tests__/apiKeyAuth.test.ts +++ b/src/__tests__/apiKeyAuth.test.ts @@ -84,7 +84,7 @@ describe('GlobalConfig load/save with API keys', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', 'anthropic_api_key: sk-ant-from-yaml', @@ -101,7 +101,7 @@ describe('GlobalConfig load/save with API keys', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', ].join('\n'); @@ -117,7 +117,7 @@ describe('GlobalConfig load/save with API keys', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', ].join('\n'); @@ -137,7 +137,7 @@ describe('GlobalConfig load/save with API keys', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', ].join('\n'); @@ -174,7 +174,7 @@ describe('resolveAnthropicApiKey', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', 'anthropic_api_key: sk-ant-from-yaml', @@ -190,7 +190,7 @@ describe('resolveAnthropicApiKey', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', 'anthropic_api_key: sk-ant-from-yaml', @@ -206,7 +206,7 @@ describe('resolveAnthropicApiKey', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', ].join('\n'); @@ -248,7 +248,7 @@ describe('resolveOpenaiApiKey', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', 'openai_api_key: sk-openai-from-yaml', @@ -264,7 +264,7 @@ describe('resolveOpenaiApiKey', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', 'openai_api_key: sk-openai-from-yaml', @@ -280,7 +280,7 @@ describe('resolveOpenaiApiKey', () => { const yaml = [ 'language: en', 'trusted_directories: []', - 'default_workflow: default', + 'default_piece: default', 'log_level: info', 'provider: claude', ].join('\n'); diff --git a/src/__tests__/bookmark.test.ts b/src/__tests__/bookmark.test.ts index df65879..88bb25a 100644 --- a/src/__tests__/bookmark.test.ts +++ b/src/__tests__/bookmark.test.ts @@ -1,10 +1,10 @@ /** - * Tests for workflow bookmark functionality + * Tests for piece bookmark functionality */ import { describe, it, expect } from 'vitest'; import { handleKeyInput } from '../shared/prompt/index.js'; -import { applyBookmarks, type SelectionOption } from '../features/workflowSelection/index.js'; +import { applyBookmarks, type SelectionOption } from '../features/pieceSelection/index.js'; describe('handleKeyInput - bookmark action', () => { const totalItems = 4; @@ -88,7 +88,7 @@ describe('applyBookmarks', () => { { label: '📁 frontend/', value: '__category__:frontend' }, { label: '📁 backend/', value: '__category__:backend' }, ]; - // Only workflow values should match; categories are not bookmarkable + // Only piece values should match; categories are not bookmarkable const result = applyBookmarks(categoryOptions, ['simple']); expect(result[0]!.label).toBe('simple [*]'); expect(result.map((o) => o.value)).toEqual(['simple', '__category__:frontend', '__category__:backend']); diff --git a/src/__tests__/cli-worktree.test.ts b/src/__tests__/cli-worktree.test.ts index 50064e6..e4ad398 100644 --- a/src/__tests__/cli-worktree.test.ts +++ b/src/__tests__/cli-worktree.test.ts @@ -53,19 +53,19 @@ vi.mock('../infra/config/index.js', () => ({ vi.mock('../infra/config/paths.js', () => ({ clearAgentSessions: vi.fn(), - getCurrentWorkflow: vi.fn(() => 'default'), + getCurrentPiece: vi.fn(() => 'default'), isVerboseMode: vi.fn(() => false), })); -vi.mock('../infra/config/loaders/workflowLoader.js', () => ({ - listWorkflows: vi.fn(() => []), +vi.mock('../infra/config/loaders/pieceLoader.js', () => ({ + listPieces: vi.fn(() => []), })); vi.mock('../shared/constants.js', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - DEFAULT_WORKFLOW_NAME: 'default', + DEFAULT_PIECE_NAME: 'default', }; }); diff --git a/src/__tests__/clone.test.ts b/src/__tests__/clone.test.ts index ef3df3e..2b293da 100644 --- a/src/__tests__/clone.test.ts +++ b/src/__tests__/clone.test.ts @@ -32,7 +32,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn(() => ({})), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); import { execFileSync } from 'node:child_process'; diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index a2bc958..0a9bbf2 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -8,13 +8,13 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'node:crypto'; import { - getBuiltinWorkflow, - loadAllWorkflows, - loadWorkflow, - listWorkflows, + getBuiltinPiece, + loadAllPieces, + loadPiece, + listPieces, loadAgentPromptFromPath, - getCurrentWorkflow, - setCurrentWorkflow, + getCurrentPiece, + setCurrentPiece, getProjectConfigDir, getBuiltinAgentsDir, loadInputHistory, @@ -37,34 +37,34 @@ import { loadProjectConfig, } from '../infra/config/index.js'; -describe('getBuiltinWorkflow', () => { - it('should return builtin workflow when it exists in resources', () => { - const workflow = getBuiltinWorkflow('default'); - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('default'); +describe('getBuiltinPiece', () => { + it('should return builtin piece when it exists in resources', () => { + const piece = getBuiltinPiece('default'); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('default'); }); - it('should return null for non-existent workflow names', () => { - expect(getBuiltinWorkflow('passthrough')).toBeNull(); - expect(getBuiltinWorkflow('unknown')).toBeNull(); - expect(getBuiltinWorkflow('')).toBeNull(); + it('should return null for non-existent piece names', () => { + expect(getBuiltinPiece('passthrough')).toBeNull(); + expect(getBuiltinPiece('unknown')).toBeNull(); + expect(getBuiltinPiece('')).toBeNull(); }); }); -describe('default workflow parallel reviewers movement', () => { +describe('default piece parallel reviewers movement', () => { it('should have a reviewers movement with parallel sub-movements', () => { - const workflow = getBuiltinWorkflow('default'); - expect(workflow).not.toBeNull(); + const piece = getBuiltinPiece('default'); + expect(piece).not.toBeNull(); - const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers'); expect(reviewersMovement).toBeDefined(); expect(reviewersMovement!.parallel).toBeDefined(); expect(reviewersMovement!.parallel).toHaveLength(2); }); it('should have arch-review and security-review as parallel sub-movements', () => { - const workflow = getBuiltinWorkflow('default'); - const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; + const piece = getBuiltinPiece('default'); + const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!; const subMovementNames = reviewersMovement.parallel!.map((s) => s.name); expect(subMovementNames).toContain('arch-review'); @@ -72,8 +72,8 @@ describe('default workflow parallel reviewers movement', () => { }); it('should have aggregate conditions on the reviewers parent movement', () => { - const workflow = getBuiltinWorkflow('default'); - const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; + const piece = getBuiltinPiece('default'); + const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!; expect(reviewersMovement.rules).toBeDefined(); expect(reviewersMovement.rules).toHaveLength(2); @@ -90,8 +90,8 @@ describe('default workflow parallel reviewers movement', () => { }); it('should have matching conditions on sub-movements for aggregation', () => { - const workflow = getBuiltinWorkflow('default'); - const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; + const piece = getBuiltinPiece('default'); + const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!; for (const subMovement of reviewersMovement.parallel!) { expect(subMovement.rules).toBeDefined(); @@ -102,32 +102,32 @@ describe('default workflow parallel reviewers movement', () => { }); it('should have ai_review transitioning to reviewers movement', () => { - const workflow = getBuiltinWorkflow('default'); - const aiReviewMovement = workflow!.movements.find((s) => s.name === 'ai_review')!; + const piece = getBuiltinPiece('default'); + const aiReviewMovement = piece!.movements.find((s) => s.name === 'ai_review')!; const approveRule = aiReviewMovement.rules!.find((r) => r.next === 'reviewers'); expect(approveRule).toBeDefined(); }); it('should have ai_fix transitioning to ai_review movement', () => { - const workflow = getBuiltinWorkflow('default'); - const aiFixMovement = workflow!.movements.find((s) => s.name === 'ai_fix')!; + const piece = getBuiltinPiece('default'); + const aiFixMovement = piece!.movements.find((s) => s.name === 'ai_fix')!; const fixedRule = aiFixMovement.rules!.find((r) => r.next === 'ai_review'); expect(fixedRule).toBeDefined(); }); it('should have fix movement transitioning back to reviewers', () => { - const workflow = getBuiltinWorkflow('default'); - const fixMovement = workflow!.movements.find((s) => s.name === 'fix')!; + const piece = getBuiltinPiece('default'); + const fixMovement = piece!.movements.find((s) => s.name === 'fix')!; const fixedRule = fixMovement.rules!.find((r) => r.next === 'reviewers'); expect(fixedRule).toBeDefined(); }); it('should not have old separate review/security_review/improve movements', () => { - const workflow = getBuiltinWorkflow('default'); - const movementNames = workflow!.movements.map((s) => s.name); + const piece = getBuiltinPiece('default'); + const movementNames = piece!.movements.map((s) => s.name); expect(movementNames).not.toContain('review'); expect(movementNames).not.toContain('security_review'); @@ -136,8 +136,8 @@ describe('default workflow parallel reviewers movement', () => { }); it('should have sub-movements with correct agents', () => { - const workflow = getBuiltinWorkflow('default'); - const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; + const piece = getBuiltinPiece('default'); + const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!; const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!; expect(archReview.agent).toContain('architecture-reviewer'); @@ -147,8 +147,8 @@ describe('default workflow parallel reviewers movement', () => { }); it('should have reports configured on sub-movements', () => { - const workflow = getBuiltinWorkflow('default'); - const reviewersMovement = workflow!.movements.find((s) => s.name === 'reviewers')!; + const piece = getBuiltinPiece('default'); + const reviewersMovement = piece!.movements.find((s) => s.name === 'reviewers')!; const archReview = reviewersMovement.parallel!.find((s) => s.name === 'arch-review')!; expect(archReview.report).toBeDefined(); @@ -158,7 +158,7 @@ describe('default workflow parallel reviewers movement', () => { }); }); -describe('loadAllWorkflows', () => { +describe('loadAllPieces', () => { let testDir: string; beforeEach(() => { @@ -172,13 +172,13 @@ describe('loadAllWorkflows', () => { } }); - it('should load project-local workflows when cwd is provided', () => { - const workflowsDir = join(testDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); + it('should load project-local pieces when cwd is provided', () => { + const piecesDir = join(testDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); - const sampleWorkflow = ` -name: test-workflow -description: Test workflow + const samplePiece = ` +name: test-piece +description: Test piece max_iterations: 10 movements: - name: step1 @@ -188,38 +188,38 @@ movements: - condition: Task completed next: COMPLETE `; - writeFileSync(join(workflowsDir, 'test.yaml'), sampleWorkflow); + writeFileSync(join(piecesDir, 'test.yaml'), samplePiece); - const workflows = loadAllWorkflows(testDir); + const pieces = loadAllPieces(testDir); - expect(workflows.has('test')).toBe(true); + expect(pieces.has('test')).toBe(true); }); }); -describe('loadWorkflow (builtin fallback)', () => { - it('should load builtin workflow when user workflow does not exist', () => { - const workflow = loadWorkflow('default', process.cwd()); - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('default'); +describe('loadPiece (builtin fallback)', () => { + it('should load builtin piece when user piece does not exist', () => { + const piece = loadPiece('default', process.cwd()); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('default'); }); - it('should return null for non-existent workflow', () => { - const workflow = loadWorkflow('does-not-exist', process.cwd()); - expect(workflow).toBeNull(); + it('should return null for non-existent piece', () => { + const piece = loadPiece('does-not-exist', process.cwd()); + expect(piece).toBeNull(); }); - it('should load builtin workflows like minimal, research', () => { - const minimal = loadWorkflow('minimal', process.cwd()); + it('should load builtin pieces like minimal, research', () => { + const minimal = loadPiece('minimal', process.cwd()); expect(minimal).not.toBeNull(); expect(minimal!.name).toBe('minimal'); - const research = loadWorkflow('research', process.cwd()); + const research = loadPiece('research', process.cwd()); expect(research).not.toBeNull(); expect(research!.name).toBe('research'); }); }); -describe('listWorkflows (builtin fallback)', () => { +describe('listPieces (builtin fallback)', () => { let testDir: string; beforeEach(() => { @@ -233,20 +233,20 @@ describe('listWorkflows (builtin fallback)', () => { } }); - it('should include builtin workflows', () => { - const workflows = listWorkflows(testDir); - expect(workflows).toContain('default'); - expect(workflows).toContain('minimal'); + it('should include builtin pieces', () => { + const pieces = listPieces(testDir); + expect(pieces).toContain('default'); + expect(pieces).toContain('minimal'); }); it('should return sorted list', () => { - const workflows = listWorkflows(testDir); - const sorted = [...workflows].sort(); - expect(workflows).toEqual(sorted); + const pieces = listPieces(testDir); + const sorted = [...pieces].sort(); + expect(pieces).toEqual(sorted); }); }); -describe('loadAllWorkflows (builtin fallback)', () => { +describe('loadAllPieces (builtin fallback)', () => { let testDir: string; beforeEach(() => { @@ -260,10 +260,10 @@ describe('loadAllWorkflows (builtin fallback)', () => { } }); - it('should include builtin workflows in the map', () => { - const workflows = loadAllWorkflows(testDir); - expect(workflows.has('default')).toBe(true); - expect(workflows.has('minimal')).toBe(true); + it('should include builtin pieces in the map', () => { + const pieces = loadAllPieces(testDir); + expect(pieces.has('default')).toBe(true); + expect(pieces.has('minimal')).toBe(true); }); }); @@ -281,7 +281,7 @@ describe('loadAgentPromptFromPath (builtin paths)', () => { }); }); -describe('getCurrentWorkflow', () => { +describe('getCurrentPiece', () => { let testDir: string; beforeEach(() => { @@ -296,19 +296,19 @@ describe('getCurrentWorkflow', () => { }); it('should return default when no config exists', () => { - const workflow = getCurrentWorkflow(testDir); + const piece = getCurrentPiece(testDir); - expect(workflow).toBe('default'); + expect(piece).toBe('default'); }); - it('should return saved workflow name from config.yaml', () => { + it('should return saved piece name from config.yaml', () => { const configDir = getProjectConfigDir(testDir); mkdirSync(configDir, { recursive: true }); - writeFileSync(join(configDir, 'config.yaml'), 'workflow: default\n'); + writeFileSync(join(configDir, 'config.yaml'), 'piece: default\n'); - const workflow = getCurrentWorkflow(testDir); + const piece = getCurrentPiece(testDir); - expect(workflow).toBe('default'); + expect(piece).toBe('default'); }); it('should return default for empty config', () => { @@ -316,13 +316,13 @@ describe('getCurrentWorkflow', () => { mkdirSync(configDir, { recursive: true }); writeFileSync(join(configDir, 'config.yaml'), ''); - const workflow = getCurrentWorkflow(testDir); + const piece = getCurrentPiece(testDir); - expect(workflow).toBe('default'); + expect(piece).toBe('default'); }); }); -describe('setCurrentWorkflow', () => { +describe('setCurrentPiece', () => { let testDir: string; beforeEach(() => { @@ -336,30 +336,30 @@ describe('setCurrentWorkflow', () => { } }); - it('should save workflow name to config.yaml', () => { - setCurrentWorkflow(testDir, 'my-workflow'); + it('should save piece name to config.yaml', () => { + setCurrentPiece(testDir, 'my-piece'); const config = loadProjectConfig(testDir); - expect(config.workflow).toBe('my-workflow'); + expect(config.piece).toBe('my-piece'); }); it('should create config directory if not exists', () => { const configDir = getProjectConfigDir(testDir); expect(existsSync(configDir)).toBe(false); - setCurrentWorkflow(testDir, 'test'); + setCurrentPiece(testDir, 'test'); expect(existsSync(configDir)).toBe(true); }); - it('should overwrite existing workflow name', () => { - setCurrentWorkflow(testDir, 'first'); - setCurrentWorkflow(testDir, 'second'); + it('should overwrite existing piece name', () => { + setCurrentPiece(testDir, 'first'); + setCurrentPiece(testDir, 'second'); - const workflow = getCurrentWorkflow(testDir); + const piece = getCurrentPiece(testDir); - expect(workflow).toBe('second'); + expect(piece).toBe('second'); }); }); @@ -592,7 +592,7 @@ describe('saveProjectConfig - gitignore copy', () => { }); it('should copy .gitignore when creating new config', () => { - setCurrentWorkflow(testDir, 'test'); + setCurrentPiece(testDir, 'test'); const configDir = getProjectConfigDir(testDir); const gitignorePath = join(configDir, '.gitignore'); @@ -604,10 +604,10 @@ describe('saveProjectConfig - gitignore copy', () => { // Create config directory without .gitignore const configDir = getProjectConfigDir(testDir); mkdirSync(configDir, { recursive: true }); - writeFileSync(join(configDir, 'config.yaml'), 'workflow: existing\n'); + writeFileSync(join(configDir, 'config.yaml'), 'piece: existing\n'); // Save config should still copy .gitignore - setCurrentWorkflow(testDir, 'updated'); + setCurrentPiece(testDir, 'updated'); const gitignorePath = join(configDir, '.gitignore'); expect(existsSync(gitignorePath)).toBe(true); @@ -619,7 +619,7 @@ describe('saveProjectConfig - gitignore copy', () => { const customContent = '# Custom gitignore\nmy-custom-file'; writeFileSync(join(configDir, '.gitignore'), customContent); - setCurrentWorkflow(testDir, 'test'); + setCurrentPiece(testDir, 'test'); const gitignorePath = join(configDir, '.gitignore'); const content = readFileSync(gitignorePath, 'utf-8'); diff --git a/src/__tests__/engine-abort.test.ts b/src/__tests__/engine-abort.test.ts index edf2f99..f87abaa 100644 --- a/src/__tests__/engine-abort.test.ts +++ b/src/__tests__/engine-abort.test.ts @@ -1,8 +1,8 @@ /** - * WorkflowEngine tests: abort (SIGINT) scenarios. + * PieceEngine tests: abort (SIGINT) scenarios. * * Covers: - * - abort() sets state to aborted and emits workflow:abort + * - abort() sets state to aborted and emits piece:abort * - abort() during movement execution interrupts the movement * - isAbortRequested() reflects abort state * - Double abort() is idempotent @@ -10,7 +10,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { existsSync, rmSync } from 'node:fs'; -import type { WorkflowConfig } from '../core/models/index.js'; +import type { PieceConfig } from '../core/models/index.js'; // --- Mock setup (must be before imports that use these modules) --- @@ -18,11 +18,11 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../core/workflow/evaluation/index.js', () => ({ +vi.mock('../core/piece/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -35,7 +35,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; import { runAgent } from '../agents/runner.js'; import { makeResponse, @@ -47,7 +47,7 @@ import { applyDefaultMocks, } from './engine-test-helpers.js'; -describe('WorkflowEngine: Abort (SIGINT)', () => { +describe('PieceEngine: Abort (SIGINT)', () => { let tmpDir: string; beforeEach(() => { @@ -62,7 +62,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { } }); - function makeSimpleConfig(): WorkflowConfig { + function makeSimpleConfig(): PieceConfig { return { name: 'test', maxIterations: 10, @@ -86,10 +86,10 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { describe('abort() before run loop iteration', () => { it('should abort immediately when abort() called before movement execution', async () => { const config = makeSimpleConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); const abortFn = vi.fn(); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); // Call abort before run engine.abort(); @@ -108,7 +108,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { describe('abort() during movement execution', () => { it('should abort when abort() is called during runAgent', async () => { const config = makeSimpleConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); // Simulate abort during movement execution: runAgent rejects after abort() is called vi.mocked(runAgent).mockImplementation(async () => { @@ -117,7 +117,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { }); const abortFn = vi.fn(); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); @@ -130,7 +130,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { describe('abort() idempotency', () => { it('should remain abort-requested on multiple abort() calls', () => { const config = makeSimpleConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); engine.abort(); engine.abort(); @@ -143,14 +143,14 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { describe('isAbortRequested()', () => { it('should return false initially', () => { const config = makeSimpleConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); expect(engine.isAbortRequested()).toBe(false); }); it('should return true after abort()', () => { const config = makeSimpleConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); engine.abort(); @@ -161,7 +161,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { describe('abort between movements', () => { it('should stop after completing current movement when abort() is called', async () => { const config = makeSimpleConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); // First movement completes normally, but abort is called during it vi.mocked(runAgent).mockImplementation(async () => { @@ -175,7 +175,7 @@ describe('WorkflowEngine: Abort (SIGINT)', () => { ]); const abortFn = vi.fn(); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); diff --git a/src/__tests__/engine-agent-overrides.test.ts b/src/__tests__/engine-agent-overrides.test.ts index 2982d26..9a658f9 100644 --- a/src/__tests__/engine-agent-overrides.test.ts +++ b/src/__tests__/engine-agent-overrides.test.ts @@ -1,7 +1,7 @@ /** - * Tests for WorkflowEngine provider/model overrides. + * Tests for PieceEngine provider/model overrides. * - * Verifies that CLI-specified overrides take precedence over workflow movement defaults, + * Verifies that CLI-specified overrides take precedence over piece movement defaults, * and that movement-specific values are used when no overrides are present. */ @@ -11,11 +11,11 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../core/workflow/evaluation/index.js', () => ({ +vi.mock('../core/piece/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn(), runReportPhase: vi.fn(), runStatusJudgmentPhase: vi.fn(), @@ -26,9 +26,9 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; import { runAgent } from '../agents/runner.js'; -import type { WorkflowConfig } from '../core/models/index.js'; +import type { PieceConfig } from '../core/models/index.js'; import { makeResponse, makeRule, @@ -38,19 +38,19 @@ import { applyDefaultMocks, } from './engine-test-helpers.js'; -describe('WorkflowEngine agent overrides', () => { +describe('PieceEngine agent overrides', () => { beforeEach(() => { vi.resetAllMocks(); applyDefaultMocks(); }); - it('respects workflow movement provider/model even when CLI overrides are provided', async () => { + it('respects piece movement provider/model even when CLI overrides are provided', async () => { const movement = makeMovement('plan', { provider: 'claude', model: 'claude-movement', rules: [makeRule('done', 'COMPLETE')], }); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'override-test', movements: [movement], initialMovement: 'plan', @@ -62,7 +62,7 @@ describe('WorkflowEngine agent overrides', () => { ]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); - const engine = new WorkflowEngine(config, '/tmp/project', 'override task', { + const engine = new PieceEngine(config, '/tmp/project', 'override task', { projectCwd: '/tmp/project', provider: 'codex', model: 'cli-model', @@ -75,11 +75,11 @@ describe('WorkflowEngine agent overrides', () => { expect(options.model).toBe('claude-movement'); }); - it('allows CLI overrides when workflow movement leaves provider/model undefined', async () => { + it('allows CLI overrides when piece movement leaves provider/model undefined', async () => { const movement = makeMovement('plan', { rules: [makeRule('done', 'COMPLETE')], }); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'override-fallback', movements: [movement], initialMovement: 'plan', @@ -91,7 +91,7 @@ describe('WorkflowEngine agent overrides', () => { ]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); - const engine = new WorkflowEngine(config, '/tmp/project', 'override task', { + const engine = new PieceEngine(config, '/tmp/project', 'override task', { projectCwd: '/tmp/project', provider: 'codex', model: 'cli-model', @@ -104,13 +104,13 @@ describe('WorkflowEngine agent overrides', () => { expect(options.model).toBe('cli-model'); }); - it('falls back to workflow movement provider/model when no overrides supplied', async () => { + it('falls back to piece movement provider/model when no overrides supplied', async () => { const movement = makeMovement('plan', { provider: 'claude', model: 'movement-model', rules: [makeRule('done', 'COMPLETE')], }); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'movement-defaults', movements: [movement], initialMovement: 'plan', @@ -122,7 +122,7 @@ describe('WorkflowEngine agent overrides', () => { ]); mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); - const engine = new WorkflowEngine(config, '/tmp/project', 'movement task', { projectCwd: '/tmp/project' }); + const engine = new PieceEngine(config, '/tmp/project', 'movement task', { projectCwd: '/tmp/project' }); await engine.run(); const options = vi.mocked(runAgent).mock.calls[0][2]; diff --git a/src/__tests__/engine-blocked.test.ts b/src/__tests__/engine-blocked.test.ts index fe3f082..49d36be 100644 --- a/src/__tests__/engine-blocked.test.ts +++ b/src/__tests__/engine-blocked.test.ts @@ -1,5 +1,5 @@ /** - * WorkflowEngine integration tests: blocked handling scenarios. + * PieceEngine integration tests: blocked handling scenarios. * * Covers: * - Blocked without onUserInput callback (abort) @@ -16,11 +16,11 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../core/workflow/evaluation/index.js', () => ({ +vi.mock('../core/piece/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -33,17 +33,17 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; import { makeResponse, - buildDefaultWorkflowConfig, + buildDefaultPieceConfig, mockRunAgentSequence, mockDetectMatchedRuleSequence, createTestTmpDir, applyDefaultMocks, } from './engine-test-helpers.js'; -describe('WorkflowEngine Integration: Blocked Handling', () => { +describe('PieceEngine Integration: Blocked Handling', () => { let tmpDir: string; beforeEach(() => { @@ -59,8 +59,8 @@ describe('WorkflowEngine Integration: Blocked Handling', () => { }); it('should abort when blocked and no onUserInput callback', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', status: 'blocked', content: 'Need clarification' }), @@ -73,7 +73,7 @@ describe('WorkflowEngine Integration: Blocked Handling', () => { const blockedFn = vi.fn(); const abortFn = vi.fn(); engine.on('movement:blocked', blockedFn); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); @@ -83,9 +83,9 @@ describe('WorkflowEngine Integration: Blocked Handling', () => { }); it('should abort when blocked and onUserInput returns null', async () => { - const config = buildDefaultWorkflowConfig(); + const config = buildDefaultPieceConfig(); const onUserInput = vi.fn().mockResolvedValue(null); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, onUserInput }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, onUserInput }); mockRunAgentSequence([ makeResponse({ agent: 'plan', status: 'blocked', content: 'Need info' }), @@ -102,9 +102,9 @@ describe('WorkflowEngine Integration: Blocked Handling', () => { }); it('should continue when blocked and onUserInput provides input', async () => { - const config = buildDefaultWorkflowConfig(); + const config = buildDefaultPieceConfig(); const onUserInput = vi.fn().mockResolvedValueOnce('User provided clarification'); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, onUserInput }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, onUserInput }); mockRunAgentSequence([ // First: plan is blocked diff --git a/src/__tests__/engine-error.test.ts b/src/__tests__/engine-error.test.ts index ed027bc..835c0bc 100644 --- a/src/__tests__/engine-error.test.ts +++ b/src/__tests__/engine-error.test.ts @@ -1,5 +1,5 @@ /** - * WorkflowEngine integration tests: error handling scenarios. + * PieceEngine integration tests: error handling scenarios. * * Covers: * - No rule matched (abort) @@ -17,11 +17,11 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../core/workflow/evaluation/index.js', () => ({ +vi.mock('../core/piece/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -34,21 +34,21 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; import { runAgent } from '../agents/runner.js'; -import { detectMatchedRule } from '../core/workflow/index.js'; +import { detectMatchedRule } from '../core/piece/index.js'; import { makeResponse, makeMovement, makeRule, - buildDefaultWorkflowConfig, + buildDefaultPieceConfig, mockRunAgentSequence, mockDetectMatchedRuleSequence, createTestTmpDir, applyDefaultMocks, } from './engine-test-helpers.js'; -describe('WorkflowEngine Integration: Error Handling', () => { +describe('PieceEngine Integration: Error Handling', () => { let tmpDir: string; beforeEach(() => { @@ -68,8 +68,8 @@ describe('WorkflowEngine Integration: Error Handling', () => { // ===================================================== describe('No rule matched', () => { it('should abort when detectMatchedRule returns undefined', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Unclear output' }), @@ -78,7 +78,7 @@ describe('WorkflowEngine Integration: Error Handling', () => { mockDetectMatchedRuleSequence([undefined]); const abortFn = vi.fn(); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); @@ -94,13 +94,13 @@ describe('WorkflowEngine Integration: Error Handling', () => { // ===================================================== describe('runAgent throws', () => { it('should abort when runAgent throws an error', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); vi.mocked(runAgent).mockRejectedValueOnce(new Error('API connection failed')); const abortFn = vi.fn(); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); @@ -116,7 +116,7 @@ describe('WorkflowEngine Integration: Error Handling', () => { // ===================================================== describe('Loop detection', () => { it('should abort when loop detected with action: abort', async () => { - const config = buildDefaultWorkflowConfig({ + const config = buildDefaultPieceConfig({ maxIterations: 100, loopDetection: { maxConsecutiveSameStep: 3, action: 'abort' }, initialMovement: 'loop-step', @@ -127,7 +127,7 @@ describe('WorkflowEngine Integration: Error Handling', () => { ], }); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); for (let i = 0; i < 5; i++) { vi.mocked(runAgent).mockResolvedValueOnce( @@ -139,7 +139,7 @@ describe('WorkflowEngine Integration: Error Handling', () => { } const abortFn = vi.fn(); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); @@ -156,8 +156,8 @@ describe('WorkflowEngine Integration: Error Handling', () => { // ===================================================== describe('Iteration limit', () => { it('should abort when max iterations reached without onIterationLimit callback', async () => { - const config = buildDefaultWorkflowConfig({ maxIterations: 2 }); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig({ maxIterations: 2 }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -174,7 +174,7 @@ describe('WorkflowEngine Integration: Error Handling', () => { const limitFn = vi.fn(); const abortFn = vi.fn(); engine.on('iteration:limit', limitFn); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); @@ -186,11 +186,11 @@ describe('WorkflowEngine Integration: Error Handling', () => { }); it('should extend iterations when onIterationLimit provides additional iterations', async () => { - const config = buildDefaultWorkflowConfig({ maxIterations: 2 }); + const config = buildDefaultPieceConfig({ maxIterations: 2 }); const onIterationLimit = vi.fn().mockResolvedValueOnce(10); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, onIterationLimit, }); diff --git a/src/__tests__/engine-happy-path.test.ts b/src/__tests__/engine-happy-path.test.ts index dd49c36..6290d5f 100644 --- a/src/__tests__/engine-happy-path.test.ts +++ b/src/__tests__/engine-happy-path.test.ts @@ -1,5 +1,5 @@ /** - * WorkflowEngine integration tests: happy path and normal flow scenarios. + * PieceEngine integration tests: happy path and normal flow scenarios. * * Covers: * - Full happy path (plan → implement → ai_review → reviewers → supervise → COMPLETE) @@ -13,7 +13,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { existsSync, rmSync } from 'node:fs'; -import type { WorkflowConfig, WorkflowMovement } from '../core/models/index.js'; +import type { PieceConfig, PieceMovement } from '../core/models/index.js'; // --- Mock setup (must be before imports that use these modules) --- @@ -21,11 +21,11 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../core/workflow/evaluation/index.js', () => ({ +vi.mock('../core/piece/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -38,20 +38,20 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; import { runAgent } from '../agents/runner.js'; import { makeResponse, makeMovement, makeRule, - buildDefaultWorkflowConfig, + buildDefaultPieceConfig, mockRunAgentSequence, mockDetectMatchedRuleSequence, createTestTmpDir, applyDefaultMocks, } from './engine-test-helpers.js'; -describe('WorkflowEngine Integration: Happy Path', () => { +describe('PieceEngine Integration: Happy Path', () => { let tmpDir: string; beforeEach(() => { @@ -71,8 +71,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('Happy path', () => { it('should complete: plan → implement → ai_review → reviewers(all approved) → supervise → COMPLETE', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan complete' }), @@ -94,7 +94,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { ]); const completeFn = vi.fn(); - engine.on('workflow:complete', completeFn); + engine.on('piece:complete', completeFn); const state = await engine.run(); @@ -110,8 +110,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('Review reject and fix loop', () => { it('should handle: reviewers(needs_fix) → fix → reviewers(all approved) → supervise → COMPLETE', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -151,8 +151,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { }); it('should inject latest reviewers output as Previous Response for repeated fix steps', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -220,8 +220,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { }); it('should use the latest movement output across different steps for Previous Response', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -278,8 +278,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('AI review reject and fix', () => { it('should handle: ai_review(issues) → ai_fix → reviewers → supervise → COMPLETE', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -315,8 +315,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('ABORT transition', () => { it('should abort when movement transitions to ABORT', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Requirements unclear' }), @@ -328,7 +328,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { ]); const abortFn = vi.fn(); - engine.on('workflow:abort', abortFn); + engine.on('piece:abort', abortFn); const state = await engine.run(); @@ -342,8 +342,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('Event emissions', () => { it('should emit movement:start and movement:complete for each movement', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan' }), @@ -375,12 +375,12 @@ describe('WorkflowEngine Integration: Happy Path', () => { expect(startFn).toHaveBeenCalledTimes(5); expect(completeFn).toHaveBeenCalledTimes(5); - const startedMovements = startFn.mock.calls.map(call => (call[0] as WorkflowMovement).name); + const startedMovements = startFn.mock.calls.map(call => (call[0] as PieceMovement).name); expect(startedMovements).toEqual(['plan', 'implement', 'ai_review', 'reviewers', 'supervise']); }); it('should pass instruction to movement:start for normal movements', async () => { - const simpleConfig: WorkflowConfig = { + const simpleConfig: PieceConfig = { name: 'test', maxIterations: 10, initialMovement: 'plan', @@ -390,7 +390,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { }), ], }; - const engine = new WorkflowEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -412,8 +412,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { }); it('should pass empty instruction to movement:start for parallel movements', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan' }), @@ -441,7 +441,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { // Find the "reviewers" movement:start call (parallel movement) const reviewersCall = startFn.mock.calls.find( - (call) => (call[0] as WorkflowMovement).name === 'reviewers' + (call) => (call[0] as PieceMovement).name === 'reviewers' ); expect(reviewersCall).toBeDefined(); // Parallel movements emit empty string for instruction @@ -450,8 +450,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { }); it('should emit iteration:limit when max iterations reached', async () => { - const config = buildDefaultWorkflowConfig({ maxIterations: 1 }); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig({ maxIterations: 1 }); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan' }), @@ -474,8 +474,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('Movement output tracking', () => { it('should store outputs for all executed movements', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan output' }), @@ -510,7 +510,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('Phase events', () => { it('should emit phase:start and phase:complete events for Phase 1', async () => { - const simpleConfig: WorkflowConfig = { + const simpleConfig: PieceConfig = { name: 'test', maxIterations: 10, initialMovement: 'plan', @@ -520,7 +520,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { }), ], }; - const engine = new WorkflowEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir }); + const engine = new PieceEngine(simpleConfig, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -547,8 +547,8 @@ describe('WorkflowEngine Integration: Happy Path', () => { }); it('should emit phase events for all movements in happy path', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan' }), @@ -593,15 +593,15 @@ describe('WorkflowEngine Integration: Happy Path', () => { // ===================================================== describe('Config validation', () => { it('should throw when initial movement does not exist', () => { - const config = buildDefaultWorkflowConfig({ initialMovement: 'nonexistent' }); + const config = buildDefaultPieceConfig({ initialMovement: 'nonexistent' }); expect(() => { - new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); }).toThrow('Unknown movement: nonexistent'); }); it('should throw when rule references nonexistent movement', () => { - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'test', maxIterations: 10, initialMovement: 'step1', @@ -613,7 +613,7 @@ describe('WorkflowEngine Integration: Happy Path', () => { }; expect(() => { - new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); }).toThrow('nonexistent_step'); }); }); diff --git a/src/__tests__/engine-parallel.test.ts b/src/__tests__/engine-parallel.test.ts index 151c403..85157b8 100644 --- a/src/__tests__/engine-parallel.test.ts +++ b/src/__tests__/engine-parallel.test.ts @@ -1,5 +1,5 @@ /** - * WorkflowEngine integration tests: parallel movement aggregation. + * PieceEngine integration tests: parallel movement aggregation. * * Covers: * - Aggregated output format (## headers and --- separators) @@ -16,11 +16,11 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../core/workflow/evaluation/index.js', () => ({ +vi.mock('../core/piece/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -33,18 +33,18 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; import { runAgent } from '../agents/runner.js'; import { makeResponse, - buildDefaultWorkflowConfig, + buildDefaultPieceConfig, mockRunAgentSequence, mockDetectMatchedRuleSequence, createTestTmpDir, applyDefaultMocks, } from './engine-test-helpers.js'; -describe('WorkflowEngine Integration: Parallel Movement Aggregation', () => { +describe('PieceEngine Integration: Parallel Movement Aggregation', () => { let tmpDir: string; beforeEach(() => { @@ -60,8 +60,8 @@ describe('WorkflowEngine Integration: Parallel Movement Aggregation', () => { }); it('should aggregate sub-movement outputs with ## headers and --- separators', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan done' }), @@ -97,8 +97,8 @@ describe('WorkflowEngine Integration: Parallel Movement Aggregation', () => { }); it('should store individual sub-movement outputs in movementOutputs', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan' }), @@ -129,8 +129,8 @@ describe('WorkflowEngine Integration: Parallel Movement Aggregation', () => { }); it('should execute sub-movements concurrently (both runAgent calls happen)', async () => { - const config = buildDefaultWorkflowConfig(); - const engine = new WorkflowEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); + const config = buildDefaultPieceConfig(); + const engine = new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir }); mockRunAgentSequence([ makeResponse({ agent: 'plan', content: 'Plan' }), diff --git a/src/__tests__/engine-report.test.ts b/src/__tests__/engine-report.test.ts index 0d5722b..b8f769f 100644 --- a/src/__tests__/engine-report.test.ts +++ b/src/__tests__/engine-report.test.ts @@ -8,8 +8,8 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { EventEmitter } from 'node:events'; import { existsSync } from 'node:fs'; -import { isReportObjectConfig } from '../core/workflow/index.js'; -import type { WorkflowMovement, ReportObjectConfig, ReportConfig } from '../core/models/index.js'; +import { isReportObjectConfig } from '../core/piece/index.js'; +import type { PieceMovement, ReportObjectConfig, ReportConfig } from '../core/models/index.js'; /** * Extracted emitMovementReports logic for unit testing. @@ -19,7 +19,7 @@ import type { WorkflowMovement, ReportObjectConfig, ReportConfig } from '../core */ function emitMovementReports( emitter: EventEmitter, - movement: WorkflowMovement, + movement: PieceMovement, reportDir: string, projectCwd: string, ): void { @@ -39,7 +39,7 @@ function emitMovementReports( function emitIfReportExists( emitter: EventEmitter, - movement: WorkflowMovement, + movement: PieceMovement, baseDir: string, fileName: string, ): void { @@ -49,8 +49,8 @@ function emitIfReportExists( } } -/** Create a minimal WorkflowMovement for testing */ -function createMovement(overrides: Partial = {}): WorkflowMovement { +/** Create a minimal PieceMovement for testing */ +function createMovement(overrides: Partial = {}): PieceMovement { return { name: 'test-movement', agent: 'coder', diff --git a/src/__tests__/engine-test-helpers.ts b/src/__tests__/engine-test-helpers.ts index 470523b..5e3b7c5 100644 --- a/src/__tests__/engine-test-helpers.ts +++ b/src/__tests__/engine-test-helpers.ts @@ -1,7 +1,7 @@ /** - * Shared helpers for WorkflowEngine integration tests. + * Shared helpers for PieceEngine integration tests. * - * Provides mock setup, factory functions, and a default workflow config + * Provides mock setup, factory functions, and a default piece config * matching the parallel reviewers structure (plan → implement → ai_review → reviewers → supervise). */ @@ -10,14 +10,14 @@ import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'node:crypto'; -import type { WorkflowConfig, WorkflowMovement, AgentResponse, WorkflowRule } from '../core/models/index.js'; +import type { PieceConfig, PieceMovement, AgentResponse, PieceRule } from '../core/models/index.js'; // --- Mock imports (consumers must call vi.mock before importing this) --- import { runAgent } from '../agents/runner.js'; -import { detectMatchedRule } from '../core/workflow/index.js'; -import type { RuleMatch } from '../core/workflow/index.js'; -import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/workflow/index.js'; +import { detectMatchedRule } from '../core/piece/index.js'; +import type { RuleMatch } from '../core/piece/index.js'; +import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/piece/index.js'; import { generateReportDir } from '../shared/utils/index.js'; // --- Factory functions --- @@ -33,11 +33,11 @@ export function makeResponse(overrides: Partial = {}): AgentRespo }; } -export function makeRule(condition: string, next: string, extra: Partial = {}): WorkflowRule { +export function makeRule(condition: string, next: string, extra: Partial = {}): PieceRule { return { condition, next, ...extra }; } -export function makeMovement(name: string, overrides: Partial = {}): WorkflowMovement { +export function makeMovement(name: string, overrides: Partial = {}): PieceMovement { return { name, agent: `../agents/${name}.md`, @@ -49,10 +49,10 @@ export function makeMovement(name: string, overrides: Partial } /** - * Build a workflow config matching the default.yaml parallel reviewers structure: + * Build a piece config matching the default.yaml parallel reviewers structure: * plan → implement → ai_review → (ai_fix↔) → reviewers(parallel) → (fix↔) → supervise */ -export function buildDefaultWorkflowConfig(overrides: Partial = {}): WorkflowConfig { +export function buildDefaultPieceConfig(overrides: Partial = {}): PieceConfig { const archReviewSubMovement = makeMovement('arch-review', { rules: [ makeRule('approved', 'COMPLETE'), @@ -69,7 +69,7 @@ export function buildDefaultWorkflowConfig(overrides: Partial = return { name: 'test-default', - description: 'Test workflow', + description: 'Test piece', maxIterations: 30, initialMovement: 'plan', movements: [ diff --git a/src/__tests__/engine-worktree-report.test.ts b/src/__tests__/engine-worktree-report.test.ts index eba2ba2..c189a58 100644 --- a/src/__tests__/engine-worktree-report.test.ts +++ b/src/__tests__/engine-worktree-report.test.ts @@ -17,11 +17,11 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../core/workflow/evaluation/index.js', () => ({ +vi.mock('../core/piece/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -34,8 +34,8 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; -import { runReportPhase } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; +import { runReportPhase } from '../core/piece/index.js'; import { makeResponse, makeMovement, @@ -44,7 +44,7 @@ import { mockDetectMatchedRuleSequence, applyDefaultMocks, } from './engine-test-helpers.js'; -import type { WorkflowConfig } from '../core/models/index.js'; +import type { PieceConfig } from '../core/models/index.js'; function createWorktreeDirs(): { projectCwd: string; cloneCwd: string } { const base = join(tmpdir(), `takt-worktree-test-${randomUUID()}`); @@ -64,10 +64,10 @@ function createWorktreeDirs(): { projectCwd: string; cloneCwd: string } { return { projectCwd, cloneCwd }; } -function buildSimpleConfig(): WorkflowConfig { +function buildSimpleConfig(): PieceConfig { return { name: 'worktree-test', - description: 'Test workflow for worktree', + description: 'Test piece for worktree', maxIterations: 10, initialMovement: 'review', movements: [ @@ -81,7 +81,7 @@ function buildSimpleConfig(): WorkflowConfig { }; } -describe('WorkflowEngine: worktree reportDir resolution', () => { +describe('PieceEngine: worktree reportDir resolution', () => { let projectCwd: string; let cloneCwd: string; let baseDir: string; @@ -104,7 +104,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { it('should pass projectCwd-based reportDir to phase runner context in worktree mode', async () => { // Given: worktree environment where cwd !== projectCwd const config = buildSimpleConfig(); - const engine = new WorkflowEngine(config, cloneCwd, 'test task', { + const engine = new PieceEngine(config, cloneCwd, 'test task', { projectCwd, }); @@ -115,7 +115,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { { index: 0, method: 'tag' as const }, ]); - // When: run the workflow + // When: run the piece await engine.run(); // Then: runReportPhase was called with context containing projectCwd-based reportDir @@ -133,7 +133,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { it('should pass projectCwd-based reportDir to buildInstruction (used by {report_dir} placeholder)', async () => { // Given: worktree environment with a movement that uses {report_dir} in template - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'worktree-test', description: 'Test', maxIterations: 10, @@ -148,7 +148,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { }), ], }; - const engine = new WorkflowEngine(config, cloneCwd, 'test task', { + const engine = new PieceEngine(config, cloneCwd, 'test task', { projectCwd, }); @@ -160,7 +160,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { { index: 0, method: 'tag' as const }, ]); - // When: run the workflow + // When: run the piece await engine.run(); // Then: the instruction should contain projectCwd-based reportDir @@ -178,7 +178,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { // Given: normal environment where cwd === projectCwd const normalDir = projectCwd; const config = buildSimpleConfig(); - const engine = new WorkflowEngine(config, normalDir, 'test task', { + const engine = new PieceEngine(config, normalDir, 'test task', { projectCwd: normalDir, }); @@ -189,7 +189,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { { index: 0, method: 'tag' as const }, ]); - // When: run the workflow + // When: run the piece await engine.run(); // Then: reportDir should be the same (cwd === projectCwd) diff --git a/src/__tests__/exitCodes.test.ts b/src/__tests__/exitCodes.test.ts index 7670463..9281d58 100644 --- a/src/__tests__/exitCodes.test.ts +++ b/src/__tests__/exitCodes.test.ts @@ -7,7 +7,7 @@ import { EXIT_SUCCESS, EXIT_GENERAL_ERROR, EXIT_ISSUE_FETCH_FAILED, - EXIT_WORKFLOW_FAILED, + EXIT_PIECE_FAILED, EXIT_GIT_OPERATION_FAILED, EXIT_PR_CREATION_FAILED, EXIT_SIGINT, @@ -19,7 +19,7 @@ describe('exit codes', () => { EXIT_SUCCESS, EXIT_GENERAL_ERROR, EXIT_ISSUE_FETCH_FAILED, - EXIT_WORKFLOW_FAILED, + EXIT_PIECE_FAILED, EXIT_GIT_OPERATION_FAILED, EXIT_PR_CREATION_FAILED, EXIT_SIGINT, @@ -32,7 +32,7 @@ describe('exit codes', () => { expect(EXIT_SUCCESS).toBe(0); expect(EXIT_GENERAL_ERROR).toBe(1); expect(EXIT_ISSUE_FETCH_FAILED).toBe(2); - expect(EXIT_WORKFLOW_FAILED).toBe(3); + expect(EXIT_PIECE_FAILED).toBe(3); expect(EXIT_GIT_OPERATION_FAILED).toBe(4); expect(EXIT_PR_CREATION_FAILED).toBe(5); expect(EXIT_SIGINT).toBe(130); diff --git a/src/__tests__/github-pr.test.ts b/src/__tests__/github-pr.test.ts index ccd0380..2a128cd 100644 --- a/src/__tests__/github-pr.test.ts +++ b/src/__tests__/github-pr.test.ts @@ -19,12 +19,12 @@ describe('buildPrBody', () => { comments: [], }; - const result = buildPrBody(issue, 'Workflow `default` completed.'); + const result = buildPrBody(issue, 'Piece `default` completed.'); expect(result).toContain('## Summary'); expect(result).toContain('Implement username/password authentication.'); expect(result).toContain('## Execution Report'); - expect(result).toContain('Workflow `default` completed.'); + expect(result).toContain('Piece `default` completed.'); expect(result).toContain('Closes #99'); }); diff --git a/src/__tests__/globalConfig-defaults.test.ts b/src/__tests__/globalConfig-defaults.test.ts index d9b47f0..277a179 100644 --- a/src/__tests__/globalConfig-defaults.test.ts +++ b/src/__tests__/globalConfig-defaults.test.ts @@ -40,7 +40,7 @@ describe('loadGlobalConfig', () => { expect(config.language).toBe('en'); expect(config.trustedDirectories).toEqual([]); - expect(config.defaultWorkflow).toBe('default'); + expect(config.defaultPiece).toBe('default'); expect(config.logLevel).toBe('info'); expect(config.provider).toBe('claude'); expect(config.model).toBeUndefined(); diff --git a/src/__tests__/i18n.test.ts b/src/__tests__/i18n.test.ts index e668e4b..5e37919 100644 --- a/src/__tests__/i18n.test.ts +++ b/src/__tests__/i18n.test.ts @@ -35,7 +35,7 @@ describe('getLabel', () => { describe('template variable substitution', () => { it('replaces {variableName} placeholders with provided values', () => { - const result = getLabel('workflow.iterationLimit.maxReached', undefined, { + const result = getLabel('piece.iterationLimit.maxReached', undefined, { currentIteration: '5', maxIterations: '10', }); @@ -43,14 +43,14 @@ describe('getLabel', () => { }); it('replaces single variable', () => { - const result = getLabel('workflow.notifyComplete', undefined, { + const result = getLabel('piece.notifyComplete', undefined, { iteration: '3', }); expect(result).toContain('3 iterations'); }); it('leaves unmatched placeholders as-is', () => { - const result = getLabel('workflow.notifyAbort', undefined, {}); + const result = getLabel('piece.notifyAbort', undefined, {}); expect(result).toContain('{reason}'); }); }); @@ -100,29 +100,29 @@ describe('label integrity', () => { expect(ui).toHaveProperty('cancelled'); }); - it('contains all expected workflow keys in en', () => { - expect(() => getLabel('workflow.iterationLimit.maxReached')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.currentMovement')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.continueQuestion')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.continueLabel')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.continueDescription')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.stopLabel')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.inputPrompt')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.invalidInput')).not.toThrow(); - expect(() => getLabel('workflow.iterationLimit.userInputPrompt')).not.toThrow(); - expect(() => getLabel('workflow.notifyComplete')).not.toThrow(); - expect(() => getLabel('workflow.notifyAbort')).not.toThrow(); - expect(() => getLabel('workflow.sigintGraceful')).not.toThrow(); - expect(() => getLabel('workflow.sigintForce')).not.toThrow(); + it('contains all expected piece keys in en', () => { + expect(() => getLabel('piece.iterationLimit.maxReached')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.currentMovement')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.continueQuestion')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.continueLabel')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.continueDescription')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.stopLabel')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.inputPrompt')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.invalidInput')).not.toThrow(); + expect(() => getLabel('piece.iterationLimit.userInputPrompt')).not.toThrow(); + expect(() => getLabel('piece.notifyComplete')).not.toThrow(); + expect(() => getLabel('piece.notifyAbort')).not.toThrow(); + expect(() => getLabel('piece.sigintGraceful')).not.toThrow(); + expect(() => getLabel('piece.sigintForce')).not.toThrow(); }); it('en and ja have the same key structure', () => { const stringKeys = [ 'interactive.ui.intro', 'interactive.ui.cancelled', - 'workflow.iterationLimit.maxReached', - 'workflow.notifyComplete', - 'workflow.sigintGraceful', + 'piece.iterationLimit.maxReached', + 'piece.notifyComplete', + 'piece.sigintGraceful', ]; for (const key of stringKeys) { expect(() => getLabel(key, 'en')).not.toThrow(); diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 400c135..f96d98f 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -12,22 +12,22 @@ import { type ReportInstructionContext, type StatusJudgmentContext, type InstructionContext, -} from '../core/workflow/index.js'; +} from '../core/piece/index.js'; // Function wrappers for test readability -function buildInstruction(step: WorkflowMovement, ctx: InstructionContext): string { +function buildInstruction(step: PieceMovement, ctx: InstructionContext): string { return new InstructionBuilder(step, ctx).build(); } -function buildReportInstruction(step: WorkflowMovement, ctx: ReportInstructionContext): string { +function buildReportInstruction(step: PieceMovement, ctx: ReportInstructionContext): string { return new ReportInstructionBuilder(step, ctx).build(); } -function buildStatusJudgmentInstruction(step: WorkflowMovement, ctx: StatusJudgmentContext): string { +function buildStatusJudgmentInstruction(step: PieceMovement, ctx: StatusJudgmentContext): string { return new StatusJudgmentBuilder(step, ctx).build(); } -import type { WorkflowMovement, WorkflowRule } from '../core/models/index.js'; +import type { PieceMovement, PieceRule } from '../core/models/index.js'; -function createMinimalStep(template: string): WorkflowMovement { +function createMinimalStep(template: string): PieceMovement { return { name: 'test-step', agent: 'test-agent', @@ -186,7 +186,7 @@ describe('instruction-builder', () => { }); describe('generateStatusRulesComponents', () => { - const rules: WorkflowRule[] = [ + const rules: PieceRule[] = [ { condition: '要件が明確で実装可能', next: 'implement' }, { condition: 'ユーザーが質問をしている', next: 'COMPLETE' }, { condition: '要件が不明確、情報不足', next: 'ABORT', appendix: '確認事項:\n- {質問1}\n- {質問2}' }, @@ -201,7 +201,7 @@ describe('instruction-builder', () => { }); it('should generate criteria table with numbered tags (en)', () => { - const enRules: WorkflowRule[] = [ + const enRules: PieceRule[] = [ { condition: 'Requirements are clear', next: 'implement' }, { condition: 'User is asking a question', next: 'COMPLETE' }, ]; @@ -229,7 +229,7 @@ describe('instruction-builder', () => { }); it('should not generate appendix when no rules have appendix', () => { - const noAppendixRules: WorkflowRule[] = [ + const noAppendixRules: PieceRule[] = [ { condition: 'Done', next: 'review' }, { condition: 'Blocked', next: 'plan' }, ]; @@ -248,7 +248,7 @@ describe('instruction-builder', () => { }); it('should omit interactive-only rules when interactive is false', () => { - const filteredRules: WorkflowRule[] = [ + const filteredRules: PieceRule[] = [ { condition: 'Clear', next: 'implement' }, { condition: 'User input required', next: 'implement', interactiveOnly: true }, { condition: 'Blocked', next: 'plan' }, @@ -298,7 +298,7 @@ describe('instruction-builder', () => { }); }); - describe('auto-injected Workflow Context section', () => { + describe('auto-injected Piece Context section', () => { it('should include iteration, step iteration, and step name', () => { const step = createMinimalStep('Do work'); step.name = 'implement'; @@ -311,7 +311,7 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('## Workflow Context'); + expect(result).toContain('## Piece Context'); expect(result).toContain('- Iteration: 3/20'); expect(result).toContain('- Movement Iteration: 2'); expect(result).toContain('- Movement: implement'); @@ -328,7 +328,7 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('## Workflow Context'); + expect(result).toContain('## Piece Context'); expect(result).toContain('Report Directory'); expect(result).toContain('Report File'); expect(result).toContain('Phase 1'); @@ -380,12 +380,12 @@ describe('instruction-builder', () => { expect(result).toContain('- Movement Iteration: 3(このムーブメントの実行回数)'); }); - it('should include workflow structure when workflowSteps is provided', () => { + it('should include piece structure when pieceSteps is provided', () => { const step = createMinimalStep('Do work'); step.name = 'implement'; const context = createMinimalContext({ language: 'en', - workflowMovements: [ + pieceMovements: [ { name: 'plan' }, { name: 'implement' }, { name: 'review' }, @@ -395,7 +395,7 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('This workflow consists of 3 movements:'); + expect(result).toContain('This piece consists of 3 movements:'); expect(result).toContain('- Movement 1: plan'); expect(result).toContain('- Movement 2: implement'); expect(result).toContain('← current'); @@ -407,7 +407,7 @@ describe('instruction-builder', () => { step.name = 'plan'; const context = createMinimalContext({ language: 'en', - workflowMovements: [ + pieceMovements: [ { name: 'plan' }, { name: 'implement' }, ], @@ -425,7 +425,7 @@ describe('instruction-builder', () => { step.name = 'plan'; const context = createMinimalContext({ language: 'ja', - workflowMovements: [ + pieceMovements: [ { name: 'plan', description: 'タスクを分析し実装計画を作成する' }, { name: 'implement' }, ], @@ -437,34 +437,34 @@ describe('instruction-builder', () => { expect(result).toContain('- Movement 1: plan(タスクを分析し実装計画を作成する) ← 現在'); }); - it('should skip workflow structure when workflowSteps is not provided', () => { + it('should skip piece structure when pieceSteps is not provided', () => { const step = createMinimalStep('Do work'); const context = createMinimalContext({ language: 'en' }); const result = buildInstruction(step, context); - expect(result).not.toContain('This workflow consists of'); + expect(result).not.toContain('This piece consists of'); }); - it('should skip workflow structure when workflowSteps is empty', () => { + it('should skip piece structure when pieceSteps is empty', () => { const step = createMinimalStep('Do work'); const context = createMinimalContext({ language: 'en', - workflowMovements: [], + pieceMovements: [], currentMovementIndex: -1, }); const result = buildInstruction(step, context); - expect(result).not.toContain('This workflow consists of'); + expect(result).not.toContain('This piece consists of'); }); - it('should render workflow structure in Japanese', () => { + it('should render piece structure in Japanese', () => { const step = createMinimalStep('Do work'); step.name = 'plan'; const context = createMinimalContext({ language: 'ja', - workflowMovements: [ + pieceMovements: [ { name: 'plan' }, { name: 'implement' }, ], @@ -473,7 +473,7 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('このワークフローは2ムーブメントで構成されています:'); + expect(result).toContain('このピースは2ムーブメントで構成されています:'); expect(result).toContain('← 現在'); }); @@ -482,7 +482,7 @@ describe('instruction-builder', () => { step.name = 'sub-step'; const context = createMinimalContext({ language: 'en', - workflowMovements: [ + pieceMovements: [ { name: 'plan' }, { name: 'implement' }, ], @@ -491,7 +491,7 @@ describe('instruction-builder', () => { const result = buildInstruction(step, context); - expect(result).toContain('This workflow consists of 2 movements:'); + expect(result).toContain('This piece consists of 2 movements:'); expect(result).not.toContain('← current'); }); }); diff --git a/src/__tests__/interactive.test.ts b/src/__tests__/interactive.test.ts index 5da7571..66137c4 100644 --- a/src/__tests__/interactive.test.ts +++ b/src/__tests__/interactive.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn(() => ({ provider: 'mock', language: 'en' })), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../infra/providers/index.js', () => ({ diff --git a/src/__tests__/it-error-recovery.test.ts b/src/__tests__/it-error-recovery.test.ts index c553417..27f6611 100644 --- a/src/__tests__/it-error-recovery.test.ts +++ b/src/__tests__/it-error-recovery.test.ts @@ -5,7 +5,7 @@ * loop detection, scenario queue exhaustion, and movement execution exceptions. * * Mocked: UI, session, phase-runner, notifications, config, callAiJudge - * Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator + * Not mocked: PieceEngine, runAgent, detectMatchedRule, rule-evaluator */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -13,7 +13,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { setMockScenario, resetScenario } from '../infra/mock/index.js'; -import type { WorkflowConfig, WorkflowMovement, WorkflowRule } from '../core/models/index.js'; +import type { PieceConfig, PieceMovement, PieceRule } from '../core/models/index.js'; import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js'; // --- Mocks --- @@ -26,7 +26,7 @@ vi.mock('../infra/claude/client.js', async (importOriginal) => { }; }); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -42,7 +42,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../infra/config/project/projectConfig.js', () => ({ @@ -51,15 +51,15 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string): WorkflowRule { +function makeRule(condition: string, next: string): PieceRule { return { condition, next }; } -function makeMovement(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowMovement { +function makeMovement(name: string, agentPath: string, rules: PieceRule[]): PieceMovement { return { name, agent: `./agents/${name}.md`, @@ -98,10 +98,10 @@ function buildEngineOptions(projectCwd: string) { }; } -function buildWorkflow(agentPaths: Record, maxIterations: number): WorkflowConfig { +function buildPiece(agentPaths: Record, maxIterations: number): PieceConfig { return { name: 'it-error', - description: 'IT error recovery workflow', + description: 'IT error recovery piece', maxIterations, initialMovement: 'plan', movements: [ @@ -142,15 +142,15 @@ describe('Error Recovery IT: agent blocked response', () => { { agent: 'plan', status: 'blocked', content: 'Error: Agent is blocked.' }, ]); - const config = buildWorkflow(agentPaths, 10); - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const config = buildPiece(agentPaths, 10); + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); const state = await engine.run(); - // Blocked agent should result in workflow abort + // Blocked agent should result in piece abort expect(state.status).toBe('aborted'); }); @@ -159,8 +159,8 @@ describe('Error Recovery IT: agent blocked response', () => { { agent: 'plan', status: 'done', content: '' }, ]); - const config = buildWorkflow(agentPaths, 10); - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const config = buildPiece(agentPaths, 10); + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -189,15 +189,15 @@ describe('Error Recovery IT: max iterations reached', () => { }); it('should abort when max iterations reached (tight limit)', async () => { - // Only 2 iterations allowed, but workflow needs 3 movements + // Only 2 iterations allowed, but piece needs 3 movements setMockScenario([ { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' }, { agent: 'implement', status: 'done', content: '[IMPLEMENT:1]\n\nDone.' }, { agent: 'review', status: 'done', content: '[REVIEW:1]\n\nPassed.' }, ]); - const config = buildWorkflow(agentPaths, 2); - const engine = new WorkflowEngine(config, testDir, 'Task', { + const config = buildPiece(agentPaths, 2); + const engine = new PieceEngine(config, testDir, 'Task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -216,8 +216,8 @@ describe('Error Recovery IT: max iterations reached', () => { })); setMockScenario(loopScenario); - const config = buildWorkflow(agentPaths, 4); - const engine = new WorkflowEngine(config, testDir, 'Looping task', { + const config = buildPiece(agentPaths, 4); + const engine = new PieceEngine(config, testDir, 'Looping task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -245,14 +245,14 @@ describe('Error Recovery IT: scenario queue exhaustion', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should handle scenario queue exhaustion mid-workflow', async () => { - // Only 1 entry, but workflow needs 3 movements + it('should handle scenario queue exhaustion mid-piece', async () => { + // Only 1 entry, but piece needs 3 movements setMockScenario([ { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' }, ]); - const config = buildWorkflow(agentPaths, 10); - const engine = new WorkflowEngine(config, testDir, 'Task', { + const config = buildPiece(agentPaths, 10); + const engine = new PieceEngine(config, testDir, 'Task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -281,21 +281,21 @@ describe('Error Recovery IT: movement events on error paths', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should emit workflow:abort event with reason on max iterations', async () => { + it('should emit piece:abort event with reason on max iterations', async () => { const loopScenario = Array.from({ length: 6 }, (_, i) => ({ status: 'done' as const, content: i % 2 === 0 ? '[PLAN:1]\n\nClear.' : '[IMPLEMENT:2]\n\nCannot proceed.', })); setMockScenario(loopScenario); - const config = buildWorkflow(agentPaths, 3); - const engine = new WorkflowEngine(config, testDir, 'Task', { + const config = buildPiece(agentPaths, 3); + const engine = new PieceEngine(config, testDir, 'Task', { ...buildEngineOptions(testDir), provider: 'mock', }); let abortReason: string | undefined; - engine.on('workflow:abort', (_state, reason) => { + engine.on('piece:abort', (_state, reason) => { abortReason = reason; }); @@ -309,8 +309,8 @@ describe('Error Recovery IT: movement events on error paths', () => { { agent: 'plan', status: 'done', content: '[PLAN:2]\n\nRequirements unclear.' }, ]); - const config = buildWorkflow(agentPaths, 10); - const engine = new WorkflowEngine(config, testDir, 'Task', { + const config = buildPiece(agentPaths, 10); + const engine = new PieceEngine(config, testDir, 'Task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -348,7 +348,7 @@ describe('Error Recovery IT: programmatic abort', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should support engine.abort() to cancel running workflow', async () => { + it('should support engine.abort() to cancel running piece', async () => { // Provide enough scenarios for 3 steps setMockScenario([ { agent: 'plan', status: 'done', content: '[PLAN:1]\n\nClear.' }, @@ -356,8 +356,8 @@ describe('Error Recovery IT: programmatic abort', () => { { agent: 'review', status: 'done', content: '[REVIEW:1]\n\nPassed.' }, ]); - const config = buildWorkflow(agentPaths, 10); - const engine = new WorkflowEngine(config, testDir, 'Task', { + const config = buildPiece(agentPaths, 10); + const engine = new PieceEngine(config, testDir, 'Task', { ...buildEngineOptions(testDir), provider: 'mock', }); diff --git a/src/__tests__/it-instruction-builder.test.ts b/src/__tests__/it-instruction-builder.test.ts index 1025af1..662d7f6 100644 --- a/src/__tests__/it-instruction-builder.test.ts +++ b/src/__tests__/it-instruction-builder.test.ts @@ -2,43 +2,43 @@ * Instruction builder integration tests. * * Tests template variable expansion and auto-injection in buildInstruction(). - * Uses real workflow movement configs (not mocked) against the buildInstruction function. + * Uses real piece movement configs (not mocked) against the buildInstruction function. * * Not mocked: buildInstruction, buildReportInstruction, buildStatusJudgmentInstruction */ import { describe, it, expect, vi } from 'vitest'; -import type { WorkflowMovement, WorkflowRule, AgentResponse } from '../core/models/index.js'; +import type { PieceMovement, PieceRule, AgentResponse } from '../core/models/index.js'; vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); -import { InstructionBuilder } from '../core/workflow/index.js'; -import { ReportInstructionBuilder, type ReportInstructionContext } from '../core/workflow/index.js'; -import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../core/workflow/index.js'; -import type { InstructionContext } from '../core/workflow/index.js'; +import { InstructionBuilder } from '../core/piece/index.js'; +import { ReportInstructionBuilder, type ReportInstructionContext } from '../core/piece/index.js'; +import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../core/piece/index.js'; +import type { InstructionContext } from '../core/piece/index.js'; // Function wrappers for test readability -function buildInstruction(movement: WorkflowMovement, ctx: InstructionContext): string { +function buildInstruction(movement: PieceMovement, ctx: InstructionContext): string { return new InstructionBuilder(movement, ctx).build(); } -function buildReportInstruction(movement: WorkflowMovement, ctx: ReportInstructionContext): string { +function buildReportInstruction(movement: PieceMovement, ctx: ReportInstructionContext): string { return new ReportInstructionBuilder(movement, ctx).build(); } -function buildStatusJudgmentInstruction(movement: WorkflowMovement, ctx: StatusJudgmentContext): string { +function buildStatusJudgmentInstruction(movement: PieceMovement, ctx: StatusJudgmentContext): string { return new StatusJudgmentBuilder(movement, ctx).build(); } // --- Test helpers --- -function makeRule(condition: string, next: string, extra?: Partial): WorkflowRule { +function makeRule(condition: string, next: string, extra?: Partial): PieceRule { return { condition, next, ...extra }; } -function makeMovement(overrides: Partial = {}): WorkflowMovement { +function makeMovement(overrides: Partial = {}): PieceMovement { return { name: 'test-step', agent: 'test-agent', @@ -187,7 +187,7 @@ describe('Instruction Builder IT: iteration variables', () => { expect(result).toContain('Iter: 5/30, movement iter: 2'); }); - it('should include iteration in Workflow Context section', () => { + it('should include iteration in Piece Context section', () => { const step = makeMovement(); const ctx = makeContext({ iteration: 7, maxIterations: 20, movementIteration: 3 }); diff --git a/src/__tests__/it-workflow-execution.test.ts b/src/__tests__/it-piece-execution.test.ts similarity index 83% rename from src/__tests__/it-workflow-execution.test.ts rename to src/__tests__/it-piece-execution.test.ts index 884f3d6..260400a 100644 --- a/src/__tests__/it-workflow-execution.test.ts +++ b/src/__tests__/it-piece-execution.test.ts @@ -1,12 +1,12 @@ /** - * Workflow execution integration tests. + * Piece execution integration tests. * - * Tests WorkflowEngine with real runAgent + MockProvider + ScenarioQueue. + * Tests PieceEngine with real runAgent + MockProvider + ScenarioQueue. * No vi.mock on runAgent or detectMatchedRule — rules are matched via * [MOVEMENT_NAME:N] tags in scenario content (tag-based detection). * * Mocked: UI, session, phase-runner (report/judgment phases), notifications, config - * Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator + * Not mocked: PieceEngine, runAgent, detectMatchedRule, rule-evaluator */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -14,7 +14,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { setMockScenario, resetScenario } from '../infra/mock/index.js'; -import type { WorkflowConfig, WorkflowMovement, WorkflowRule } from '../core/models/index.js'; +import type { PieceConfig, PieceMovement, PieceRule } from '../core/models/index.js'; import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js'; // --- Mocks (minimal — only infrastructure, not core logic) --- @@ -30,7 +30,7 @@ vi.mock('../infra/claude/client.js', async (importOriginal) => { }; }); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -45,7 +45,7 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../infra/config/project/projectConfig.js', () => ({ @@ -54,15 +54,15 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string): WorkflowRule { +function makeRule(condition: string, next: string): PieceRule { return { condition, next }; } -function makeMovement(name: string, agentPath: string, rules: WorkflowRule[]): WorkflowMovement { +function makeMovement(name: string, agentPath: string, rules: PieceRule[]): PieceMovement { return { name, agent: `./agents/${name}.md`, @@ -100,10 +100,10 @@ function buildEngineOptions(projectCwd: string) { }; } -function buildSimpleWorkflow(agentPaths: Record): WorkflowConfig { +function buildSimplePiece(agentPaths: Record): PieceConfig { return { name: 'it-simple', - description: 'IT simple workflow', + description: 'IT simple piece', maxIterations: 15, initialMovement: 'plan', movements: [ @@ -123,10 +123,10 @@ function buildSimpleWorkflow(agentPaths: Record): WorkflowConfig }; } -function buildLoopWorkflow(agentPaths: Record): WorkflowConfig { +function buildLoopPiece(agentPaths: Record): PieceConfig { return { name: 'it-loop', - description: 'IT workflow with fix loop', + description: 'IT piece with fix loop', maxIterations: 20, initialMovement: 'plan', movements: [ @@ -154,7 +154,7 @@ function buildLoopWorkflow(agentPaths: Record): WorkflowConfig { }; } -describe('Workflow Engine IT: Happy Path', () => { +describe('Piece Engine IT: Happy Path', () => { let testDir: string; let agentPaths: Record; @@ -177,8 +177,8 @@ describe('Workflow Engine IT: Happy Path', () => { { agent: 'review', status: 'done', content: '[REVIEW:1]\n\nAll checks passed.' }, ]); - const config = buildSimpleWorkflow(agentPaths); - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const config = buildSimplePiece(agentPaths); + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -194,8 +194,8 @@ describe('Workflow Engine IT: Happy Path', () => { { agent: 'plan', status: 'done', content: '[PLAN:2]\n\nRequirements unclear.' }, ]); - const config = buildSimpleWorkflow(agentPaths); - const engine = new WorkflowEngine(config, testDir, 'Vague task', { + const config = buildSimplePiece(agentPaths); + const engine = new PieceEngine(config, testDir, 'Vague task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -207,7 +207,7 @@ describe('Workflow Engine IT: Happy Path', () => { }); }); -describe('Workflow Engine IT: Fix Loop', () => { +describe('Piece Engine IT: Fix Loop', () => { let testDir: string; let agentPaths: Record; @@ -237,8 +237,8 @@ describe('Workflow Engine IT: Fix Loop', () => { { agent: 'supervise', status: 'done', content: '[SUPERVISE:1]\n\nAll checks passed.' }, ]); - const config = buildLoopWorkflow(agentPaths); - const engine = new WorkflowEngine(config, testDir, 'Task needing fix', { + const config = buildLoopPiece(agentPaths); + const engine = new PieceEngine(config, testDir, 'Task needing fix', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -257,8 +257,8 @@ describe('Workflow Engine IT: Fix Loop', () => { { agent: 'fix', status: 'done', content: '[FIX:2]\n\nCannot fix.' }, ]); - const config = buildLoopWorkflow(agentPaths); - const engine = new WorkflowEngine(config, testDir, 'Unfixable task', { + const config = buildLoopPiece(agentPaths); + const engine = new PieceEngine(config, testDir, 'Unfixable task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -269,7 +269,7 @@ describe('Workflow Engine IT: Fix Loop', () => { }); }); -describe('Workflow Engine IT: Max Iterations', () => { +describe('Piece Engine IT: Max Iterations', () => { let testDir: string; let agentPaths: Record; @@ -293,10 +293,10 @@ describe('Workflow Engine IT: Max Iterations', () => { })); setMockScenario(infiniteScenario); - const config = buildSimpleWorkflow(agentPaths); + const config = buildSimplePiece(agentPaths); config.maxIterations = 5; - const engine = new WorkflowEngine(config, testDir, 'Looping task', { + const engine = new PieceEngine(config, testDir, 'Looping task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -308,7 +308,7 @@ describe('Workflow Engine IT: Max Iterations', () => { }); }); -describe('Workflow Engine IT: Movement Output Tracking', () => { +describe('Piece Engine IT: Movement Output Tracking', () => { let testDir: string; let agentPaths: Record; @@ -331,8 +331,8 @@ describe('Workflow Engine IT: Movement Output Tracking', () => { { agent: 'review', status: 'done', content: '[REVIEW:1]\n\nReview output.' }, ]); - const config = buildSimpleWorkflow(agentPaths); - const engine = new WorkflowEngine(config, testDir, 'Track outputs', { + const config = buildSimplePiece(agentPaths); + const engine = new PieceEngine(config, testDir, 'Track outputs', { ...buildEngineOptions(testDir), provider: 'mock', }); diff --git a/src/__tests__/it-workflow-loader.test.ts b/src/__tests__/it-piece-loader.test.ts similarity index 72% rename from src/__tests__/it-workflow-loader.test.ts rename to src/__tests__/it-piece-loader.test.ts index ec6fa30..f230dc0 100644 --- a/src/__tests__/it-workflow-loader.test.ts +++ b/src/__tests__/it-piece-loader.test.ts @@ -1,11 +1,11 @@ /** - * Workflow loader integration tests. + * Piece loader integration tests. * - * Tests the 3-tier workflow resolution (project-local → user → builtin) + * Tests the 3-tier piece resolution (project-local → user → builtin) * and YAML parsing including special rule syntax (ai(), all(), any()). * * Mocked: globalConfig (for language/builtins) - * Not mocked: loadWorkflow, parseWorkflow, rule parsing + * Not mocked: loadPiece, parsePiece, rule parsing */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -19,12 +19,12 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); // --- Imports (after mocks) --- -import { loadWorkflow } from '../infra/config/index.js'; +import { loadPiece } from '../infra/config/index.js'; // --- Test helpers --- @@ -34,7 +34,7 @@ function createTestDir(): string { return dir; } -describe('Workflow Loader IT: builtin workflow loading', () => { +describe('Piece Loader IT: builtin piece loading', () => { let testDir: string; beforeEach(() => { @@ -48,8 +48,8 @@ describe('Workflow Loader IT: builtin workflow loading', () => { const builtinNames = ['default', 'minimal', 'expert', 'expert-cqrs', 'research', 'magi', 'review-only', 'review-fix-minimal']; for (const name of builtinNames) { - it(`should load builtin workflow: ${name}`, () => { - const config = loadWorkflow(name, testDir); + it(`should load builtin piece: ${name}`, () => { + const config = loadPiece(name, testDir); expect(config).not.toBeNull(); expect(config!.name).toBe(name); @@ -59,13 +59,13 @@ describe('Workflow Loader IT: builtin workflow loading', () => { }); } - it('should return null for non-existent workflow', () => { - const config = loadWorkflow('non-existent-workflow-xyz', testDir); + it('should return null for non-existent piece', () => { + const config = loadPiece('non-existent-piece-xyz', testDir); expect(config).toBeNull(); }); }); -describe('Workflow Loader IT: project-local workflow override', () => { +describe('Piece Loader IT: project-local piece override', () => { let testDir: string; beforeEach(() => { @@ -76,17 +76,17 @@ describe('Workflow Loader IT: project-local workflow override', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should load project-local workflow from .takt/workflows/', () => { - const workflowsDir = join(testDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); + it('should load project-local piece from .takt/pieces/', () => { + const piecesDir = join(testDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); const agentsDir = join(testDir, 'agents'); mkdirSync(agentsDir, { recursive: true }); writeFileSync(join(agentsDir, 'custom.md'), 'Custom agent'); - writeFileSync(join(workflowsDir, 'custom-wf.yaml'), ` + writeFileSync(join(piecesDir, 'custom-wf.yaml'), ` name: custom-wf -description: Custom project workflow +description: Custom project piece max_iterations: 5 initial_movement: start @@ -99,7 +99,7 @@ movements: instruction: "Do the work" `); - const config = loadWorkflow('custom-wf', testDir); + const config = loadPiece('custom-wf', testDir); expect(config).not.toBeNull(); expect(config!.name).toBe('custom-wf'); @@ -108,7 +108,7 @@ movements: }); }); -describe('Workflow Loader IT: agent path resolution', () => { +describe('Piece Loader IT: agent path resolution', () => { let testDir: string; beforeEach(() => { @@ -119,8 +119,8 @@ describe('Workflow Loader IT: agent path resolution', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should resolve relative agent paths from workflow YAML location', () => { - const config = loadWorkflow('minimal', testDir); + it('should resolve relative agent paths from piece YAML location', () => { + const config = loadPiece('minimal', testDir); expect(config).not.toBeNull(); for (const movement of config!.movements) { @@ -142,7 +142,7 @@ describe('Workflow Loader IT: agent path resolution', () => { }); }); -describe('Workflow Loader IT: rule syntax parsing', () => { +describe('Piece Loader IT: rule syntax parsing', () => { let testDir: string; beforeEach(() => { @@ -153,8 +153,8 @@ describe('Workflow Loader IT: rule syntax parsing', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should parse all() aggregate conditions from default workflow', () => { - const config = loadWorkflow('default', testDir); + it('should parse all() aggregate conditions from default piece', () => { + const config = loadPiece('default', testDir); expect(config).not.toBeNull(); // Find the parallel reviewers movement @@ -171,8 +171,8 @@ describe('Workflow Loader IT: rule syntax parsing', () => { expect(allRule!.aggregateConditionText).toBe('approved'); }); - it('should parse any() aggregate conditions from default workflow', () => { - const config = loadWorkflow('default', testDir); + it('should parse any() aggregate conditions from default piece', () => { + const config = loadPiece('default', testDir); expect(config).not.toBeNull(); const reviewersStep = config!.movements.find( @@ -187,7 +187,7 @@ describe('Workflow Loader IT: rule syntax parsing', () => { }); it('should parse standard rules with next movement', () => { - const config = loadWorkflow('minimal', testDir); + const config = loadPiece('minimal', testDir); expect(config).not.toBeNull(); const implementStep = config!.movements.find((s) => s.name === 'implement'); @@ -203,7 +203,7 @@ describe('Workflow Loader IT: rule syntax parsing', () => { }); }); -describe('Workflow Loader IT: workflow config validation', () => { +describe('Piece Loader IT: piece config validation', () => { let testDir: string; beforeEach(() => { @@ -215,14 +215,14 @@ describe('Workflow Loader IT: workflow config validation', () => { }); it('should set max_iterations from YAML', () => { - const config = loadWorkflow('minimal', testDir); + const config = loadPiece('minimal', testDir); expect(config).not.toBeNull(); expect(typeof config!.maxIterations).toBe('number'); expect(config!.maxIterations).toBeGreaterThan(0); }); it('should set initial_movement from YAML', () => { - const config = loadWorkflow('minimal', testDir); + const config = loadPiece('minimal', testDir); expect(config).not.toBeNull(); expect(typeof config!.initialMovement).toBe('string'); @@ -232,7 +232,7 @@ describe('Workflow Loader IT: workflow config validation', () => { }); it('should preserve edit property on movements (review-only has no edit: true)', () => { - const config = loadWorkflow('review-only', testDir); + const config = loadPiece('review-only', testDir); expect(config).not.toBeNull(); // review-only: no movement should have edit: true @@ -246,7 +246,7 @@ describe('Workflow Loader IT: workflow config validation', () => { } // expert: implement movement should have edit: true - const expertConfig = loadWorkflow('expert', testDir); + const expertConfig = loadPiece('expert', testDir); expect(expertConfig).not.toBeNull(); const implementStep = expertConfig!.movements.find((s) => s.name === 'implement'); expect(implementStep).toBeDefined(); @@ -254,7 +254,7 @@ describe('Workflow Loader IT: workflow config validation', () => { }); it('should set passPreviousResponse from YAML', () => { - const config = loadWorkflow('minimal', testDir); + const config = loadPiece('minimal', testDir); expect(config).not.toBeNull(); // At least some movements should have passPreviousResponse set @@ -263,7 +263,7 @@ describe('Workflow Loader IT: workflow config validation', () => { }); }); -describe('Workflow Loader IT: parallel movement loading', () => { +describe('Piece Loader IT: parallel movement loading', () => { let testDir: string; beforeEach(() => { @@ -274,8 +274,8 @@ describe('Workflow Loader IT: parallel movement loading', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should load parallel sub-movements from default workflow', () => { - const config = loadWorkflow('default', testDir); + it('should load parallel sub-movements from default piece', () => { + const config = loadPiece('default', testDir); expect(config).not.toBeNull(); const parallelStep = config!.movements.find( @@ -292,8 +292,8 @@ describe('Workflow Loader IT: parallel movement loading', () => { } }); - it('should load 4 parallel reviewers from expert workflow', () => { - const config = loadWorkflow('expert', testDir); + it('should load 4 parallel reviewers from expert piece', () => { + const config = loadPiece('expert', testDir); expect(config).not.toBeNull(); const parallelStep = config!.movements.find( @@ -309,7 +309,7 @@ describe('Workflow Loader IT: parallel movement loading', () => { }); }); -describe('Workflow Loader IT: report config loading', () => { +describe('Piece Loader IT: report config loading', () => { let testDir: string; beforeEach(() => { @@ -321,17 +321,17 @@ describe('Workflow Loader IT: report config loading', () => { }); it('should load single report config', () => { - const config = loadWorkflow('default', testDir); + const config = loadPiece('default', testDir); expect(config).not.toBeNull(); - // default workflow: plan movement has a report config + // default piece: plan movement has a report config const planStep = config!.movements.find((s) => s.name === 'plan'); expect(planStep).toBeDefined(); expect(planStep!.report).toBeDefined(); }); - it('should load multi-report config from expert workflow', () => { - const config = loadWorkflow('expert', testDir); + it('should load multi-report config from expert piece', () => { + const config = loadPiece('expert', testDir); expect(config).not.toBeNull(); // implement movement has multi-report: [Scope, Decisions] @@ -343,7 +343,7 @@ describe('Workflow Loader IT: report config loading', () => { }); }); -describe('Workflow Loader IT: invalid YAML handling', () => { +describe('Piece Loader IT: invalid YAML handling', () => { let testDir: string; beforeEach(() => { @@ -354,28 +354,28 @@ describe('Workflow Loader IT: invalid YAML handling', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should throw for workflow file with invalid YAML', () => { - const workflowsDir = join(testDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); + it('should throw for piece file with invalid YAML', () => { + const piecesDir = join(testDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); - writeFileSync(join(workflowsDir, 'broken.yaml'), ` + writeFileSync(join(piecesDir, 'broken.yaml'), ` name: broken this is not: valid yaml: [[[[ - bad: { `); - expect(() => loadWorkflow('broken', testDir)).toThrow(); + expect(() => loadPiece('broken', testDir)).toThrow(); }); - it('should throw for workflow missing required fields', () => { - const workflowsDir = join(testDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); + it('should throw for piece missing required fields', () => { + const piecesDir = join(testDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); - writeFileSync(join(workflowsDir, 'incomplete.yaml'), ` + writeFileSync(join(piecesDir, 'incomplete.yaml'), ` name: incomplete description: Missing movements `); - expect(() => loadWorkflow('incomplete', testDir)).toThrow(); + expect(() => loadPiece('incomplete', testDir)).toThrow(); }); }); diff --git a/src/__tests__/it-workflow-patterns.test.ts b/src/__tests__/it-piece-patterns.test.ts similarity index 87% rename from src/__tests__/it-workflow-patterns.test.ts rename to src/__tests__/it-piece-patterns.test.ts index 944ace2..d09d7ee 100644 --- a/src/__tests__/it-workflow-patterns.test.ts +++ b/src/__tests__/it-piece-patterns.test.ts @@ -1,11 +1,11 @@ /** - * Workflow patterns integration tests. + * Piece patterns integration tests. * - * Tests that all builtin workflow definitions can be loaded and execute - * the expected step transitions using WorkflowEngine + MockProvider + ScenarioQueue. + * Tests that all builtin piece definitions can be loaded and execute + * the expected step transitions using PieceEngine + MockProvider + ScenarioQueue. * * Mocked: UI, session, phase-runner, notifications, config, callAiJudge - * Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator + * Not mocked: PieceEngine, runAgent, detectMatchedRule, rule-evaluator */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -33,7 +33,7 @@ vi.mock('../infra/claude/client.js', async (importOriginal) => { }; }); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -49,7 +49,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../infra/config/project/projectConfig.js', () => ({ @@ -58,9 +58,9 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; -import { loadWorkflow } from '../infra/config/index.js'; -import type { WorkflowConfig } from '../core/models/index.js'; +import { PieceEngine } from '../core/piece/index.js'; +import { loadPiece } from '../infra/config/index.js'; +import type { PieceConfig } from '../core/models/index.js'; // --- Test helpers --- @@ -70,8 +70,8 @@ function createTestDir(): string { return dir; } -function createEngine(config: WorkflowConfig, dir: string, task: string): WorkflowEngine { - return new WorkflowEngine(config, dir, task, { +function createEngine(config: PieceConfig, dir: string, task: string): PieceEngine { + return new PieceEngine(config, dir, task, { projectCwd: dir, provider: 'mock', detectRuleIndex, @@ -79,7 +79,7 @@ function createEngine(config: WorkflowConfig, dir: string, task: string): Workfl }); } -describe('Workflow Patterns IT: minimal workflow', () => { +describe('Piece Patterns IT: minimal piece', () => { let testDir: string; beforeEach(() => { @@ -93,7 +93,7 @@ describe('Workflow Patterns IT: minimal workflow', () => { }); it('should complete: implement → reviewers (parallel: ai_review + supervise) → COMPLETE', async () => { - const config = loadWorkflow('minimal', testDir); + const config = loadPiece('minimal', testDir); expect(config).not.toBeNull(); setMockScenario([ @@ -110,7 +110,7 @@ describe('Workflow Patterns IT: minimal workflow', () => { }); it('should ABORT when implement cannot proceed', async () => { - const config = loadWorkflow('minimal', testDir); + const config = loadPiece('minimal', testDir); setMockScenario([ { agent: 'coder', status: 'done', content: 'Cannot proceed, insufficient info.' }, @@ -125,7 +125,7 @@ describe('Workflow Patterns IT: minimal workflow', () => { }); -describe('Workflow Patterns IT: default workflow (parallel reviewers)', () => { +describe('Piece Patterns IT: default piece (parallel reviewers)', () => { let testDir: string; beforeEach(() => { @@ -139,7 +139,7 @@ describe('Workflow Patterns IT: default workflow (parallel reviewers)', () => { }); it('should complete with all("approved") in parallel review step', async () => { - const config = loadWorkflow('default', testDir); + const config = loadPiece('default', testDir); expect(config).not.toBeNull(); setMockScenario([ @@ -161,7 +161,7 @@ describe('Workflow Patterns IT: default workflow (parallel reviewers)', () => { }); it('should route to fix when any("needs_fix") in parallel review step', async () => { - const config = loadWorkflow('default', testDir); + const config = loadPiece('default', testDir); setMockScenario([ { agent: 'planner', status: 'done', content: 'Requirements are clear and implementable' }, @@ -189,7 +189,7 @@ describe('Workflow Patterns IT: default workflow (parallel reviewers)', () => { }); }); -describe('Workflow Patterns IT: research workflow', () => { +describe('Piece Patterns IT: research piece', () => { let testDir: string; beforeEach(() => { @@ -203,7 +203,7 @@ describe('Workflow Patterns IT: research workflow', () => { }); it('should complete: plan → dig → supervise → COMPLETE', async () => { - const config = loadWorkflow('research', testDir); + const config = loadPiece('research', testDir); expect(config).not.toBeNull(); setMockScenario([ @@ -220,7 +220,7 @@ describe('Workflow Patterns IT: research workflow', () => { }); it('should loop: plan → dig → supervise (insufficient) → plan → dig → supervise → COMPLETE', async () => { - const config = loadWorkflow('research', testDir); + const config = loadPiece('research', testDir); setMockScenario([ { agent: 'research/planner', status: 'done', content: '[PLAN:1]\n\nPlanning is complete.' }, @@ -240,7 +240,7 @@ describe('Workflow Patterns IT: research workflow', () => { }); }); -describe('Workflow Patterns IT: magi workflow', () => { +describe('Piece Patterns IT: magi piece', () => { let testDir: string; beforeEach(() => { @@ -254,7 +254,7 @@ describe('Workflow Patterns IT: magi workflow', () => { }); it('should complete: melchior → balthasar → casper → COMPLETE', async () => { - const config = loadWorkflow('magi', testDir); + const config = loadPiece('magi', testDir); expect(config).not.toBeNull(); setMockScenario([ @@ -271,7 +271,7 @@ describe('Workflow Patterns IT: magi workflow', () => { }); }); -describe('Workflow Patterns IT: review-only workflow', () => { +describe('Piece Patterns IT: review-only piece', () => { let testDir: string; beforeEach(() => { @@ -285,7 +285,7 @@ describe('Workflow Patterns IT: review-only workflow', () => { }); it('should complete: plan → reviewers (all approved) → supervise → COMPLETE', async () => { - const config = loadWorkflow('review-only', testDir); + const config = loadPiece('review-only', testDir); expect(config).not.toBeNull(); setMockScenario([ @@ -305,7 +305,7 @@ describe('Workflow Patterns IT: review-only workflow', () => { }); it('should verify no movements have edit: true', () => { - const config = loadWorkflow('review-only', testDir); + const config = loadPiece('review-only', testDir); expect(config).not.toBeNull(); for (const movement of config!.movements) { @@ -319,7 +319,7 @@ describe('Workflow Patterns IT: review-only workflow', () => { }); }); -describe('Workflow Patterns IT: expert workflow (4 parallel reviewers)', () => { +describe('Piece Patterns IT: expert piece (4 parallel reviewers)', () => { let testDir: string; beforeEach(() => { @@ -333,7 +333,7 @@ describe('Workflow Patterns IT: expert workflow (4 parallel reviewers)', () => { }); it('should complete with all("approved") in 4-parallel review', async () => { - const config = loadWorkflow('expert', testDir); + const config = loadPiece('expert', testDir); expect(config).not.toBeNull(); setMockScenario([ diff --git a/src/__tests__/it-pipeline-modes.test.ts b/src/__tests__/it-pipeline-modes.test.ts index 2da43f8..e1c18b0 100644 --- a/src/__tests__/it-pipeline-modes.test.ts +++ b/src/__tests__/it-pipeline-modes.test.ts @@ -2,11 +2,11 @@ * Pipeline execution mode integration tests. * * Tests various --pipeline mode option combinations including: - * - --task, --issue, --skip-git, --auto-pr, --workflow (name/path), --provider, --model + * - --task, --issue, --skip-git, --auto-pr, --piece (name/path), --provider, --model * - Exit codes for different failure scenarios * * Mocked: git (child_process), GitHub API, UI, notifications, session, phase-runner, config - * Not mocked: executePipeline, executeTask, WorkflowEngine, runAgent, rule evaluation + * Not mocked: executePipeline, executeTask, PieceEngine, runAgent, rule evaluation */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -109,7 +109,7 @@ vi.mock('../infra/config/paths.js', async (importOriginal) => { updateAgentSession: vi.fn(), loadWorktreeSessions: vi.fn().mockReturnValue({}), updateWorktreeSession: vi.fn(), - getCurrentWorkflow: vi.fn().mockReturnValue('default'), + getCurrentPiece: vi.fn().mockReturnValue('default'), getProjectConfigDir: vi.fn().mockImplementation((cwd: string) => join(cwd, '.takt')), }; }); @@ -141,7 +141,7 @@ vi.mock('../shared/prompt/index.js', () => ({ promptInput: vi.fn().mockResolvedValue(null), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -152,13 +152,13 @@ vi.mock('../core/workflow/phase-runner.js', () => ({ import { executePipeline } from '../features/pipeline/index.js'; import { EXIT_ISSUE_FETCH_FAILED, - EXIT_WORKFLOW_FAILED, + EXIT_PIECE_FAILED, EXIT_PR_CREATION_FAILED, } from '../shared/exitCodes.js'; // --- Test helpers --- -function createTestWorkflowDir(): { dir: string; workflowPath: string } { +function createTestPieceDir(): { dir: string; piecePath: string } { const dir = mkdtempSync(join(tmpdir(), 'takt-it-pm-')); mkdirSync(join(dir, '.takt', 'reports', 'test-report-dir'), { recursive: true }); @@ -168,9 +168,9 @@ function createTestWorkflowDir(): { dir: string; workflowPath: string } { writeFileSync(join(agentsDir, 'coder.md'), 'You are a coder.'); writeFileSync(join(agentsDir, 'reviewer.md'), 'You are a reviewer.'); - const workflowYaml = ` + const pieceYaml = ` name: it-pipeline -description: Pipeline test workflow +description: Pipeline test piece max_iterations: 10 initial_movement: plan @@ -203,10 +203,10 @@ movements: instruction: "{task}" `; - const workflowPath = join(dir, 'workflow.yaml'); - writeFileSync(workflowPath, workflowYaml); + const piecePath = join(dir, 'piece.yaml'); + writeFileSync(piecePath, pieceYaml); - return { dir, workflowPath }; + return { dir, piecePath }; } function happyScenario(): void { @@ -217,15 +217,15 @@ function happyScenario(): void { ]); } -describe('Pipeline Modes IT: --task + --workflow path', () => { +describe('Pipeline Modes IT: --task + --piece path', () => { let testDir: string; - let workflowPath: string; + let piecePath: string; beforeEach(() => { vi.clearAllMocks(); - const setup = createTestWorkflowDir(); + const setup = createTestPieceDir(); testDir = setup.dir; - workflowPath = setup.workflowPath; + piecePath = setup.piecePath; }); afterEach(() => { @@ -238,7 +238,7 @@ describe('Pipeline Modes IT: --task + --workflow path', () => { const exitCode = await executePipeline({ task: 'Add a feature', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -248,30 +248,30 @@ describe('Pipeline Modes IT: --task + --workflow path', () => { expect(exitCode).toBe(0); }); - it('should return EXIT_WORKFLOW_FAILED (3) on ABORT', async () => { + it('should return EXIT_PIECE_FAILED (3) on ABORT', async () => { setMockScenario([ { agent: 'planner', status: 'done', content: '[PLAN:2]\n\nRequirements unclear.' }, ]); const exitCode = await executePipeline({ task: 'Vague task', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, provider: 'mock', }); - expect(exitCode).toBe(EXIT_WORKFLOW_FAILED); + expect(exitCode).toBe(EXIT_PIECE_FAILED); }); }); -describe('Pipeline Modes IT: --task + --workflow name (builtin)', () => { +describe('Pipeline Modes IT: --task + --piece name (builtin)', () => { let testDir: string; beforeEach(() => { vi.clearAllMocks(); - const setup = createTestWorkflowDir(); + const setup = createTestPieceDir(); testDir = setup.dir; }); @@ -280,7 +280,7 @@ describe('Pipeline Modes IT: --task + --workflow name (builtin)', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should load and execute builtin minimal workflow by name', async () => { + it('should load and execute builtin minimal piece by name', async () => { setMockScenario([ { agent: 'coder', status: 'done', content: 'Implementation complete' }, { agent: 'ai-antipattern-reviewer', status: 'done', content: 'No AI-specific issues' }, @@ -289,7 +289,7 @@ describe('Pipeline Modes IT: --task + --workflow name (builtin)', () => { const exitCode = await executePipeline({ task: 'Add a feature', - workflow: 'minimal', + piece: 'minimal', autoPr: false, skipGit: true, cwd: testDir, @@ -299,29 +299,29 @@ describe('Pipeline Modes IT: --task + --workflow name (builtin)', () => { expect(exitCode).toBe(0); }); - it('should return EXIT_WORKFLOW_FAILED for non-existent workflow name', async () => { + it('should return EXIT_PIECE_FAILED for non-existent piece name', async () => { const exitCode = await executePipeline({ task: 'Test task', - workflow: 'non-existent-workflow-xyz', + piece: 'non-existent-piece-xyz', autoPr: false, skipGit: true, cwd: testDir, provider: 'mock', }); - expect(exitCode).toBe(EXIT_WORKFLOW_FAILED); + expect(exitCode).toBe(EXIT_PIECE_FAILED); }); }); describe('Pipeline Modes IT: --issue', () => { let testDir: string; - let workflowPath: string; + let piecePath: string; beforeEach(() => { vi.clearAllMocks(); - const setup = createTestWorkflowDir(); + const setup = createTestPieceDir(); testDir = setup.dir; - workflowPath = setup.workflowPath; + piecePath = setup.piecePath; }); afterEach(() => { @@ -329,7 +329,7 @@ describe('Pipeline Modes IT: --issue', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should fetch issue and execute workflow', async () => { + it('should fetch issue and execute piece', async () => { mockCheckGhCli.mockReturnValue({ available: true }); mockFetchIssue.mockReturnValue({ number: 42, @@ -341,7 +341,7 @@ describe('Pipeline Modes IT: --issue', () => { const exitCode = await executePipeline({ issueNumber: 42, - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -357,7 +357,7 @@ describe('Pipeline Modes IT: --issue', () => { const exitCode = await executePipeline({ issueNumber: 42, - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -375,7 +375,7 @@ describe('Pipeline Modes IT: --issue', () => { const exitCode = await executePipeline({ issueNumber: 999, - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -387,7 +387,7 @@ describe('Pipeline Modes IT: --issue', () => { it('should return EXIT_ISSUE_FETCH_FAILED when neither --issue nor --task specified', async () => { const exitCode = await executePipeline({ - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -400,13 +400,13 @@ describe('Pipeline Modes IT: --issue', () => { describe('Pipeline Modes IT: --auto-pr', () => { let testDir: string; - let workflowPath: string; + let piecePath: string; beforeEach(() => { vi.clearAllMocks(); - const setup = createTestWorkflowDir(); + const setup = createTestPieceDir(); testDir = setup.dir; - workflowPath = setup.workflowPath; + piecePath = setup.piecePath; }); afterEach(() => { @@ -420,7 +420,7 @@ describe('Pipeline Modes IT: --auto-pr', () => { const exitCode = await executePipeline({ task: 'Add a feature', - workflow: workflowPath, + piece: piecePath, autoPr: true, skipGit: false, cwd: testDir, @@ -437,7 +437,7 @@ describe('Pipeline Modes IT: --auto-pr', () => { const exitCode = await executePipeline({ task: 'Add a feature', - workflow: workflowPath, + piece: piecePath, autoPr: true, skipGit: false, cwd: testDir, @@ -452,7 +452,7 @@ describe('Pipeline Modes IT: --auto-pr', () => { const exitCode = await executePipeline({ task: 'Add a feature', - workflow: workflowPath, + piece: piecePath, autoPr: true, skipGit: true, cwd: testDir, @@ -466,13 +466,13 @@ describe('Pipeline Modes IT: --auto-pr', () => { describe('Pipeline Modes IT: --provider and --model overrides', () => { let testDir: string; - let workflowPath: string; + let piecePath: string; beforeEach(() => { vi.clearAllMocks(); - const setup = createTestWorkflowDir(); + const setup = createTestPieceDir(); testDir = setup.dir; - workflowPath = setup.workflowPath; + piecePath = setup.piecePath; }); afterEach(() => { @@ -480,12 +480,12 @@ describe('Pipeline Modes IT: --provider and --model overrides', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should pass provider override to workflow execution', async () => { + it('should pass provider override to piece execution', async () => { happyScenario(); const exitCode = await executePipeline({ task: 'Test task', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -495,12 +495,12 @@ describe('Pipeline Modes IT: --provider and --model overrides', () => { expect(exitCode).toBe(0); }); - it('should pass model override to workflow execution', async () => { + it('should pass model override to piece execution', async () => { happyScenario(); const exitCode = await executePipeline({ task: 'Test task', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -514,13 +514,13 @@ describe('Pipeline Modes IT: --provider and --model overrides', () => { describe('Pipeline Modes IT: review → fix loop', () => { let testDir: string; - let workflowPath: string; + let piecePath: string; beforeEach(() => { vi.clearAllMocks(); - const setup = createTestWorkflowDir(); + const setup = createTestPieceDir(); testDir = setup.dir; - workflowPath = setup.workflowPath; + piecePath = setup.piecePath; }); afterEach(() => { @@ -542,7 +542,7 @@ describe('Pipeline Modes IT: review → fix loop', () => { const exitCode = await executePipeline({ task: 'Task with fix loop', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, diff --git a/src/__tests__/it-pipeline.test.ts b/src/__tests__/it-pipeline.test.ts index f67669a..62fe482 100644 --- a/src/__tests__/it-pipeline.test.ts +++ b/src/__tests__/it-pipeline.test.ts @@ -5,7 +5,7 @@ * of the pipeline execution flow. Git operations are skipped via --skip-git. * * Mocked: git operations (child_process), GitHub API, UI output, notifications, session - * Not mocked: executeTask, executeWorkflow, WorkflowEngine, runAgent, rule evaluation + * Not mocked: executeTask, executePiece, PieceEngine, runAgent, rule evaluation */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -92,7 +92,7 @@ vi.mock('../infra/config/paths.js', async (importOriginal) => { updateAgentSession: vi.fn(), loadWorktreeSessions: vi.fn().mockReturnValue({}), updateWorktreeSession: vi.fn(), - getCurrentWorkflow: vi.fn().mockReturnValue('default'), + getCurrentPiece: vi.fn().mockReturnValue('default'), getProjectConfigDir: vi.fn().mockImplementation((cwd: string) => join(cwd, '.takt')), }; }); @@ -123,7 +123,7 @@ vi.mock('../shared/prompt/index.js', () => ({ promptInput: vi.fn().mockResolvedValue(null), })); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -135,8 +135,8 @@ import { executePipeline } from '../features/pipeline/index.js'; // --- Test helpers --- -/** Create a minimal test workflow YAML + agent files in a temp directory */ -function createTestWorkflowDir(): { dir: string; workflowPath: string } { +/** Create a minimal test piece YAML + agent files in a temp directory */ +function createTestPieceDir(): { dir: string; piecePath: string } { const dir = mkdtempSync(join(tmpdir(), 'takt-it-pipeline-')); // Create .takt/reports structure @@ -149,10 +149,10 @@ function createTestWorkflowDir(): { dir: string; workflowPath: string } { writeFileSync(join(agentsDir, 'coder.md'), 'You are a coder. Implement the task.'); writeFileSync(join(agentsDir, 'reviewer.md'), 'You are a reviewer. Review the code.'); - // Create a simple workflow YAML - const workflowYaml = ` + // Create a simple piece YAML + const pieceYaml = ` name: it-simple -description: Integration test workflow +description: Integration test piece max_iterations: 10 initial_movement: plan @@ -185,21 +185,21 @@ movements: instruction: "{task}" `; - const workflowPath = join(dir, 'workflow.yaml'); - writeFileSync(workflowPath, workflowYaml); + const piecePath = join(dir, 'piece.yaml'); + writeFileSync(piecePath, pieceYaml); - return { dir, workflowPath }; + return { dir, piecePath }; } describe('Pipeline Integration Tests', () => { let testDir: string; - let workflowPath: string; + let piecePath: string; beforeEach(() => { vi.clearAllMocks(); - const setup = createTestWorkflowDir(); + const setup = createTestPieceDir(); testDir = setup.dir; - workflowPath = setup.workflowPath; + piecePath = setup.piecePath; }); afterEach(() => { @@ -207,7 +207,7 @@ describe('Pipeline Integration Tests', () => { rmSync(testDir, { recursive: true, force: true }); }); - it('should complete pipeline with workflow path + skip-git + mock scenario', async () => { + it('should complete pipeline with piece path + skip-git + mock scenario', async () => { // Scenario: plan -> implement -> review -> COMPLETE // agent field must match extractAgentName(movement.agent), i.e., the .md filename without extension setMockScenario([ @@ -218,7 +218,7 @@ describe('Pipeline Integration Tests', () => { const exitCode = await executePipeline({ task: 'Add a hello world function', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, @@ -228,8 +228,8 @@ describe('Pipeline Integration Tests', () => { expect(exitCode).toBe(0); }); - it('should complete pipeline with workflow name + skip-git + mock scenario', async () => { - // Use builtin 'minimal' workflow + it('should complete pipeline with piece name + skip-git + mock scenario', async () => { + // Use builtin 'minimal' piece // agent field: extractAgentName result (from .md filename) // tag in content: [MOVEMENT_NAME:N] where MOVEMENT_NAME is the movement name uppercased setMockScenario([ @@ -240,7 +240,7 @@ describe('Pipeline Integration Tests', () => { const exitCode = await executePipeline({ task: 'Add a hello world function', - workflow: 'minimal', + piece: 'minimal', autoPr: false, skipGit: true, cwd: testDir, @@ -250,21 +250,21 @@ describe('Pipeline Integration Tests', () => { expect(exitCode).toBe(0); }); - it('should return EXIT_WORKFLOW_FAILED for non-existent workflow', async () => { + it('should return EXIT_PIECE_FAILED for non-existent piece', async () => { const exitCode = await executePipeline({ task: 'Test task', - workflow: 'non-existent-workflow-xyz', + piece: 'non-existent-piece-xyz', autoPr: false, skipGit: true, cwd: testDir, provider: 'mock', }); - // executeTask returns false when workflow not found → executePipeline returns EXIT_WORKFLOW_FAILED (3) + // executeTask returns false when piece not found → executePipeline returns EXIT_PIECE_FAILED (3) expect(exitCode).toBe(3); }); - it('should handle ABORT transition from workflow', async () => { + it('should handle ABORT transition from piece', async () => { // Scenario: plan returns second rule -> ABORT setMockScenario([ { agent: 'planner', status: 'done', content: '[PLAN:2]\n\nRequirements unclear, insufficient info.' }, @@ -272,14 +272,14 @@ describe('Pipeline Integration Tests', () => { const exitCode = await executePipeline({ task: 'Vague task with no details', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, provider: 'mock', }); - // ABORT means workflow failed -> EXIT_WORKFLOW_FAILED (3) + // ABORT means piece failed -> EXIT_PIECE_FAILED (3) expect(exitCode).toBe(3); }); @@ -296,7 +296,7 @@ describe('Pipeline Integration Tests', () => { const exitCode = await executePipeline({ task: 'Task needing a fix', - workflow: workflowPath, + piece: piecePath, autoPr: false, skipGit: true, cwd: testDir, diff --git a/src/__tests__/it-rule-evaluation.test.ts b/src/__tests__/it-rule-evaluation.test.ts index d7f11f3..b1c4956 100644 --- a/src/__tests__/it-rule-evaluation.test.ts +++ b/src/__tests__/it-rule-evaluation.test.ts @@ -15,7 +15,7 @@ */ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import type { WorkflowMovement, WorkflowState, WorkflowRule, AgentResponse } from '../core/models/index.js'; +import type { PieceMovement, PieceState, PieceRule, AgentResponse } from '../core/models/index.js'; // --- Mocks --- @@ -24,7 +24,7 @@ const mockCallAiJudge = vi.fn(); vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../infra/config/project/projectConfig.js', () => ({ @@ -33,21 +33,21 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({ // --- Imports (after mocks) --- -import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js'; +import { detectMatchedRule, evaluateAggregateConditions } from '../core/piece/index.js'; import { detectRuleIndex } from '../infra/claude/index.js'; -import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js'; +import type { RuleMatch, RuleEvaluatorContext } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string, extra?: Partial): WorkflowRule { +function makeRule(condition: string, next: string, extra?: Partial): PieceRule { return { condition, next, ...extra }; } function makeMovement( name: string, - rules: WorkflowRule[], - parallel?: WorkflowMovement[], -): WorkflowMovement { + rules: PieceRule[], + parallel?: PieceMovement[], +): PieceMovement { return { name, agent: 'test-agent', @@ -59,9 +59,9 @@ function makeMovement( }; } -function makeState(movementOutputs?: Map): WorkflowState { +function makeState(movementOutputs?: Map): PieceState { return { - workflowName: 'it-test', + pieceName: 'it-test', currentMovement: 'test', iteration: 1, status: 'running', @@ -399,7 +399,7 @@ describe('Rule Evaluation IT: movements without rules', () => { }); it('should return undefined for movement with no rules', async () => { - const step: WorkflowMovement = { + const step: PieceMovement = { name: 'step', agent: 'agent', agentDisplayName: 'step', diff --git a/src/__tests__/it-three-phase-execution.test.ts b/src/__tests__/it-three-phase-execution.test.ts index bbc5dfe..31d517f 100644 --- a/src/__tests__/it-three-phase-execution.test.ts +++ b/src/__tests__/it-three-phase-execution.test.ts @@ -6,7 +6,7 @@ * * Mocked: UI, session, config, callAiJudge * Selectively mocked: phase-runner (to inspect call patterns) - * Not mocked: WorkflowEngine, runAgent, detectMatchedRule, rule-evaluator + * Not mocked: PieceEngine, runAgent, detectMatchedRule, rule-evaluator */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -14,7 +14,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { setMockScenario, resetScenario } from '../infra/mock/index.js'; -import type { WorkflowConfig, WorkflowMovement, WorkflowRule } from '../core/models/index.js'; +import type { PieceConfig, PieceMovement, PieceRule } from '../core/models/index.js'; import { callAiJudge, detectRuleIndex } from '../infra/claude/index.js'; // --- Mocks --- @@ -31,7 +31,7 @@ const mockNeedsStatusJudgmentPhase = vi.fn(); const mockRunReportPhase = vi.fn(); const mockRunStatusJudgmentPhase = vi.fn(); -vi.mock('../core/workflow/phase-runner.js', () => ({ +vi.mock('../core/piece/phase-runner.js', () => ({ needsStatusJudgmentPhase: (...args: unknown[]) => mockNeedsStatusJudgmentPhase(...args), runReportPhase: (...args: unknown[]) => mockRunReportPhase(...args), runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...args), @@ -47,7 +47,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../infra/config/project/projectConfig.js', () => ({ @@ -56,11 +56,11 @@ vi.mock('../infra/config/project/projectConfig.js', () => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../core/workflow/index.js'; +import { PieceEngine } from '../core/piece/index.js'; // --- Test helpers --- -function makeRule(condition: string, next: string): WorkflowRule { +function makeRule(condition: string, next: string): PieceRule { return { condition, next }; } @@ -87,9 +87,9 @@ function buildEngineOptions(projectCwd: string) { function makeMovement( name: string, agentPath: string, - rules: WorkflowRule[], + rules: PieceRule[], options: { report?: string | { label: string; path: string }[]; edit?: boolean } = {}, -): WorkflowMovement { +): PieceMovement { return { name, agent: './agents/agent.md', @@ -129,7 +129,7 @@ describe('Three-Phase Execution IT: phase1 only (no report, no tag rules)', () = { status: 'done', content: '[STEP:1]\n\nDone.' }, ]); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'it-phase1-only', description: 'Test', maxIterations: 5, @@ -142,7 +142,7 @@ describe('Three-Phase Execution IT: phase1 only (no report, no tag rules)', () = ], }; - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -181,7 +181,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { { status: 'done', content: '[STEP:1]\n\nDone.' }, ]); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'it-phase1-2', description: 'Test', maxIterations: 5, @@ -194,7 +194,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { ], }; - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -211,7 +211,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { { status: 'done', content: '[STEP:1]\n\nDone.' }, ]); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'it-phase1-2-multi', description: 'Test', maxIterations: 5, @@ -223,7 +223,7 @@ describe('Three-Phase Execution IT: phase1 + phase2 (report defined)', () => { ], }; - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -262,7 +262,7 @@ describe('Three-Phase Execution IT: phase1 + phase3 (tag rules defined)', () => { status: 'done', content: 'Agent completed the work.' }, ]); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'it-phase1-3', description: 'Test', maxIterations: 5, @@ -275,7 +275,7 @@ describe('Three-Phase Execution IT: phase1 + phase3 (tag rules defined)', () => ], }; - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -313,7 +313,7 @@ describe('Three-Phase Execution IT: all three phases', () => { { status: 'done', content: 'Agent completed the work.' }, ]); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'it-all-phases', description: 'Test', maxIterations: 5, @@ -326,7 +326,7 @@ describe('Three-Phase Execution IT: all three phases', () => { ], }; - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); @@ -373,7 +373,7 @@ describe('Three-Phase Execution IT: phase3 tag → rule match', () => { // Phase 3 returns rule 2 (ABORT) mockRunStatusJudgmentPhase.mockResolvedValue('[STEP1:2]'); - const config: WorkflowConfig = { + const config: PieceConfig = { name: 'it-phase3-tag', description: 'Test', maxIterations: 5, @@ -389,7 +389,7 @@ describe('Three-Phase Execution IT: phase3 tag → rule match', () => { ], }; - const engine = new WorkflowEngine(config, testDir, 'Test task', { + const engine = new PieceEngine(config, testDir, 'Test task', { ...buildEngineOptions(testDir), provider: 'mock', }); diff --git a/src/__tests__/models.test.ts b/src/__tests__/models.test.ts index 400d879..8583cce 100644 --- a/src/__tests__/models.test.ts +++ b/src/__tests__/models.test.ts @@ -7,7 +7,7 @@ import { AgentTypeSchema, StatusSchema, PermissionModeSchema, - WorkflowConfigRawSchema, + PieceConfigRawSchema, CustomAgentConfigSchema, GlobalConfigSchema, } from '../core/models/index.js'; @@ -57,11 +57,11 @@ describe('PermissionModeSchema', () => { }); }); -describe('WorkflowConfigRawSchema', () => { - it('should parse valid workflow config', () => { +describe('PieceConfigRawSchema', () => { + it('should parse valid piece config', () => { const config = { - name: 'test-workflow', - description: 'A test workflow', + name: 'test-piece', + description: 'A test piece', movements: [ { name: 'step1', @@ -75,8 +75,8 @@ describe('WorkflowConfigRawSchema', () => { ], }; - const result = WorkflowConfigRawSchema.parse(config); - expect(result.name).toBe('test-workflow'); + const result = PieceConfigRawSchema.parse(config); + expect(result.name).toBe('test-piece'); expect(result.movements).toHaveLength(1); expect(result.movements![0]?.allowed_tools).toEqual(['Read', 'Grep']); expect(result.max_iterations).toBe(10); @@ -84,7 +84,7 @@ describe('WorkflowConfigRawSchema', () => { it('should parse movement with permission_mode', () => { const config = { - name: 'test-workflow', + name: 'test-piece', movements: [ { name: 'implement', @@ -99,13 +99,13 @@ describe('WorkflowConfigRawSchema', () => { ], }; - const result = WorkflowConfigRawSchema.parse(config); + const result = PieceConfigRawSchema.parse(config); expect(result.movements![0]?.permission_mode).toBe('edit'); }); it('should allow omitting permission_mode', () => { const config = { - name: 'test-workflow', + name: 'test-piece', movements: [ { name: 'plan', @@ -115,13 +115,13 @@ describe('WorkflowConfigRawSchema', () => { ], }; - const result = WorkflowConfigRawSchema.parse(config); + const result = PieceConfigRawSchema.parse(config); expect(result.movements![0]?.permission_mode).toBeUndefined(); }); it('should reject invalid permission_mode', () => { const config = { - name: 'test-workflow', + name: 'test-piece', movements: [ { name: 'step1', @@ -132,16 +132,16 @@ describe('WorkflowConfigRawSchema', () => { ], }; - expect(() => WorkflowConfigRawSchema.parse(config)).toThrow(); + expect(() => PieceConfigRawSchema.parse(config)).toThrow(); }); it('should require at least one movement', () => { const config = { - name: 'empty-workflow', + name: 'empty-piece', movements: [], }; - expect(() => WorkflowConfigRawSchema.parse(config)).toThrow(); + expect(() => PieceConfigRawSchema.parse(config)).toThrow(); }); }); @@ -202,7 +202,7 @@ describe('GlobalConfigSchema', () => { const result = GlobalConfigSchema.parse(config); expect(result.trusted_directories).toEqual([]); - expect(result.default_workflow).toBe('default'); + expect(result.default_piece).toBe('default'); expect(result.log_level).toBe('info'); expect(result.provider).toBe('claude'); }); @@ -210,7 +210,7 @@ describe('GlobalConfigSchema', () => { it('should accept valid config', () => { const config = { trusted_directories: ['/home/user/projects'], - default_workflow: 'custom', + default_piece: 'custom', log_level: 'debug' as const, }; diff --git a/src/__tests__/parallel-and-loader.test.ts b/src/__tests__/parallel-and-loader.test.ts index 4466d1d..0ce49aa 100644 --- a/src/__tests__/parallel-and-loader.test.ts +++ b/src/__tests__/parallel-and-loader.test.ts @@ -3,12 +3,12 @@ * * Covers: * - Schema validation for parallel sub-movements - * - Workflow loader normalization of ai() conditions and parallel movements + * - Piece loader normalization of ai() conditions and parallel movements * - Engine parallel movement aggregation logic */ import { describe, it, expect } from 'vitest'; -import { WorkflowConfigRawSchema, ParallelSubMovementRawSchema, WorkflowMovementRawSchema } from '../core/models/index.js'; +import { PieceConfigRawSchema, ParallelSubMovementRawSchema, PieceMovementRawSchema } from '../core/models/index.js'; describe('ParallelSubMovementRawSchema', () => { it('should validate a valid parallel sub-movement', () => { @@ -73,7 +73,7 @@ describe('ParallelSubMovementRawSchema', () => { }); }); -describe('WorkflowMovementRawSchema with parallel', () => { +describe('PieceMovementRawSchema with parallel', () => { it('should accept a movement with parallel sub-movements (no agent)', () => { const raw = { name: 'parallel-review', @@ -86,7 +86,7 @@ describe('WorkflowMovementRawSchema with parallel', () => { ], }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); }); @@ -96,7 +96,7 @@ describe('WorkflowMovementRawSchema with parallel', () => { instruction_template: 'Do something', }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); }); @@ -107,7 +107,7 @@ describe('WorkflowMovementRawSchema with parallel', () => { instruction_template: 'Code something', }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); }); @@ -117,15 +117,15 @@ describe('WorkflowMovementRawSchema with parallel', () => { parallel: [], }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); }); }); -describe('WorkflowConfigRawSchema with parallel movements', () => { - it('should validate a workflow with parallel movement', () => { +describe('PieceConfigRawSchema with parallel movements', () => { + it('should validate a piece with parallel movement', () => { const raw = { - name: 'test-parallel-workflow', + name: 'test-parallel-piece', movements: [ { name: 'plan', @@ -148,7 +148,7 @@ describe('WorkflowConfigRawSchema with parallel movements', () => { max_iterations: 10, }; - const result = WorkflowConfigRawSchema.safeParse(raw); + const result = PieceConfigRawSchema.safeParse(raw); expect(result.success).toBe(true); if (result.success) { expect(result.data.movements).toHaveLength(2); @@ -156,9 +156,9 @@ describe('WorkflowConfigRawSchema with parallel movements', () => { } }); - it('should validate a workflow mixing normal and parallel movements', () => { + it('should validate a piece mixing normal and parallel movements', () => { const raw = { - name: 'mixed-workflow', + name: 'mixed-piece', movements: [ { name: 'plan', agent: 'planner.md', rules: [{ condition: 'Done', next: 'implement' }] }, { name: 'implement', agent: 'coder.md', rules: [{ condition: 'Done', next: 'review' }] }, @@ -174,7 +174,7 @@ describe('WorkflowConfigRawSchema with parallel movements', () => { initial_movement: 'plan', }; - const result = WorkflowConfigRawSchema.safeParse(raw); + const result = PieceConfigRawSchema.safeParse(raw); expect(result.success).toBe(true); if (result.success) { expect(result.data.movements[0].agent).toBe('planner.md'); @@ -183,7 +183,7 @@ describe('WorkflowConfigRawSchema with parallel movements', () => { }); }); -describe('ai() condition in WorkflowRuleSchema', () => { +describe('ai() condition in PieceRuleSchema', () => { it('should accept ai() condition as a string', () => { const raw = { name: 'test-step', @@ -194,7 +194,7 @@ describe('ai() condition in WorkflowRuleSchema', () => { ], }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); if (result.success) { expect(result.data.rules?.[0].condition).toBe('ai("All reviews approved")'); @@ -212,13 +212,13 @@ describe('ai() condition in WorkflowRuleSchema', () => { ], }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); }); }); describe('ai() condition regex parsing', () => { - // Test the regex pattern used in workflowLoader.ts + // Test the regex pattern used in pieceLoader.ts const AI_CONDITION_REGEX = /^ai\("(.+)"\)$/; it('should match simple ai() condition', () => { @@ -299,7 +299,7 @@ describe('all()/any() aggregate condition regex parsing', () => { }); }); -describe('all()/any() condition in WorkflowMovementRawSchema', () => { +describe('all()/any() condition in PieceMovementRawSchema', () => { it('should accept all() condition as a string', () => { const raw = { name: 'parallel-review', @@ -312,7 +312,7 @@ describe('all()/any() condition in WorkflowMovementRawSchema', () => { ], }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); if (result.success) { expect(result.data.rules?.[0].condition).toBe('all("approved")'); @@ -333,7 +333,7 @@ describe('all()/any() condition in WorkflowMovementRawSchema', () => { ], }; - const result = WorkflowMovementRawSchema.safeParse(raw); + const result = PieceMovementRawSchema.safeParse(raw); expect(result.success).toBe(true); }); }); diff --git a/src/__tests__/parallel-logger.test.ts b/src/__tests__/parallel-logger.test.ts index e401ef4..1d15c30 100644 --- a/src/__tests__/parallel-logger.test.ts +++ b/src/__tests__/parallel-logger.test.ts @@ -3,8 +3,8 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { ParallelLogger } from '../core/workflow/index.js'; -import type { StreamEvent } from '../core/workflow/index.js'; +import { ParallelLogger } from '../core/piece/index.js'; +import type { StreamEvent } from '../core/piece/index.js'; describe('ParallelLogger', () => { let output: string[]; diff --git a/src/__tests__/workflow-builtin-toggle.test.ts b/src/__tests__/piece-builtin-toggle.test.ts similarity index 52% rename from src/__tests__/workflow-builtin-toggle.test.ts rename to src/__tests__/piece-builtin-toggle.test.ts index 581d70d..219bfa1 100644 --- a/src/__tests__/workflow-builtin-toggle.test.ts +++ b/src/__tests__/piece-builtin-toggle.test.ts @@ -1,5 +1,5 @@ /** - * Tests for builtin workflow enable/disable flag + * Tests for builtin piece enable/disable flag */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -13,20 +13,20 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => { ...original, getLanguage: () => 'en', getDisabledBuiltins: () => [], - getBuiltinWorkflowsEnabled: () => false, + getBuiltinPiecesEnabled: () => false, }; }); -const { listWorkflows } = await import('../infra/config/loaders/workflowLoader.js'); +const { listPieces } = await import('../infra/config/loaders/pieceLoader.js'); -const SAMPLE_WORKFLOW = `name: test-workflow +const SAMPLE_PIECE = `name: test-piece movements: - name: step1 agent: coder instruction: "{task}" `; -describe('builtin workflow toggle', () => { +describe('builtin piece toggle', () => { let tempDir: string; beforeEach(() => { @@ -37,13 +37,13 @@ describe('builtin workflow toggle', () => { rmSync(tempDir, { recursive: true, force: true }); }); - it('should exclude builtin workflows when disabled', () => { - const projectWorkflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(projectWorkflowsDir, { recursive: true }); - writeFileSync(join(projectWorkflowsDir, 'project-custom.yaml'), SAMPLE_WORKFLOW); + it('should exclude builtin pieces when disabled', () => { + const projectPiecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(projectPiecesDir, { recursive: true }); + writeFileSync(join(projectPiecesDir, 'project-custom.yaml'), SAMPLE_PIECE); - const workflows = listWorkflows(tempDir); - expect(workflows).toContain('project-custom'); - expect(workflows).not.toContain('default'); + const pieces = listPieces(tempDir); + expect(pieces).toContain('project-custom'); + expect(pieces).not.toContain('default'); }); }); diff --git a/src/__tests__/piece-categories.test.ts b/src/__tests__/piece-categories.test.ts new file mode 100644 index 0000000..7525a09 --- /dev/null +++ b/src/__tests__/piece-categories.test.ts @@ -0,0 +1,303 @@ +/** + * Tests for piece category (subdirectory) support — Issue #85 + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { + listPieces, + listPieceEntries, + loadAllPieces, + loadPiece, +} from '../infra/config/loaders/pieceLoader.js'; +import type { PieceDirEntry } from '../infra/config/loaders/pieceLoader.js'; +import { + buildPieceSelectionItems, + buildTopLevelSelectOptions, + parseCategorySelection, + buildCategoryPieceOptions, + type PieceSelectionItem, +} from '../features/pieceSelection/index.js'; + +const SAMPLE_PIECE = `name: test-piece +description: Test piece +initial_movement: step1 +max_iterations: 1 + +movements: + - name: step1 + agent: coder + instruction: "{task}" +`; + +function createPiece(dir: string, name: string, content?: string): void { + writeFileSync(join(dir, `${name}.yaml`), content ?? SAMPLE_PIECE); +} + +describe('piece categories - directory scanning', () => { + let tempDir: string; + let piecesDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + piecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should discover root-level pieces', () => { + createPiece(piecesDir, 'simple'); + createPiece(piecesDir, 'advanced'); + + const pieces = listPieces(tempDir); + expect(pieces).toContain('simple'); + expect(pieces).toContain('advanced'); + }); + + it('should discover pieces in subdirectories with category prefix', () => { + const frontendDir = join(piecesDir, 'frontend'); + mkdirSync(frontendDir); + createPiece(frontendDir, 'react'); + createPiece(frontendDir, 'vue'); + + const pieces = listPieces(tempDir); + expect(pieces).toContain('frontend/react'); + expect(pieces).toContain('frontend/vue'); + }); + + it('should discover both root-level and categorized pieces', () => { + createPiece(piecesDir, 'simple'); + + const frontendDir = join(piecesDir, 'frontend'); + mkdirSync(frontendDir); + createPiece(frontendDir, 'react'); + + const backendDir = join(piecesDir, 'backend'); + mkdirSync(backendDir); + createPiece(backendDir, 'api'); + + const pieces = listPieces(tempDir); + expect(pieces).toContain('simple'); + expect(pieces).toContain('frontend/react'); + expect(pieces).toContain('backend/api'); + }); + + it('should not scan deeper than 1 level', () => { + const deepDir = join(piecesDir, 'category', 'subcategory'); + mkdirSync(deepDir, { recursive: true }); + createPiece(deepDir, 'deep'); + + const pieces = listPieces(tempDir); + // category/subcategory should be treated as a directory entry, not scanned further + expect(pieces).not.toContain('category/subcategory/deep'); + // Only 1-level: category/deep would not exist since deep.yaml is in subcategory + expect(pieces).not.toContain('deep'); + }); +}); + +describe('piece categories - listPieceEntries', () => { + let tempDir: string; + let piecesDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + piecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should return entries with category information', () => { + createPiece(piecesDir, 'simple'); + + const frontendDir = join(piecesDir, 'frontend'); + mkdirSync(frontendDir); + createPiece(frontendDir, 'react'); + + const entries = listPieceEntries(tempDir); + const simpleEntry = entries.find((e) => e.name === 'simple'); + const reactEntry = entries.find((e) => e.name === 'frontend/react'); + + expect(simpleEntry).toBeDefined(); + expect(simpleEntry!.category).toBeUndefined(); + expect(simpleEntry!.source).toBe('project'); + + expect(reactEntry).toBeDefined(); + expect(reactEntry!.category).toBe('frontend'); + expect(reactEntry!.source).toBe('project'); + }); +}); + +describe('piece categories - loadAllPieces', () => { + let tempDir: string; + let piecesDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + piecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should load categorized pieces with qualified names as keys', () => { + const frontendDir = join(piecesDir, 'frontend'); + mkdirSync(frontendDir); + createPiece(frontendDir, 'react'); + + const pieces = loadAllPieces(tempDir); + expect(pieces.has('frontend/react')).toBe(true); + }); +}); + +describe('piece categories - loadPiece', () => { + let tempDir: string; + let piecesDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); + piecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(piecesDir, { recursive: true }); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should load piece by category/name identifier', () => { + const frontendDir = join(piecesDir, 'frontend'); + mkdirSync(frontendDir); + createPiece(frontendDir, 'react'); + + const piece = loadPiece('frontend/react', tempDir); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('test-piece'); + }); + + it('should return null for non-existent category/name', () => { + const piece = loadPiece('nonexistent/piece', tempDir); + expect(piece).toBeNull(); + }); + + it('should support .yml extension in subdirectories', () => { + const backendDir = join(piecesDir, 'backend'); + mkdirSync(backendDir); + writeFileSync(join(backendDir, 'api.yml'), SAMPLE_PIECE); + + const piece = loadPiece('backend/api', tempDir); + expect(piece).not.toBeNull(); + }); +}); + +describe('buildPieceSelectionItems', () => { + it('should separate root pieces and categories', () => { + const entries: PieceDirEntry[] = [ + { name: 'simple', path: '/tmp/simple.yaml', source: 'project' }, + { name: 'frontend/react', path: '/tmp/frontend/react.yaml', category: 'frontend', source: 'project' }, + { name: 'frontend/vue', path: '/tmp/frontend/vue.yaml', category: 'frontend', source: 'project' }, + { name: 'backend/api', path: '/tmp/backend/api.yaml', category: 'backend', source: 'project' }, + ]; + + const items = buildPieceSelectionItems(entries); + + const pieces = items.filter((i) => i.type === 'piece'); + const categories = items.filter((i) => i.type === 'category'); + + expect(pieces).toHaveLength(1); + expect(pieces[0]!.name).toBe('simple'); + + expect(categories).toHaveLength(2); + const frontend = categories.find((c) => c.name === 'frontend'); + expect(frontend).toBeDefined(); + expect(frontend!.type === 'category' && frontend!.pieces).toEqual(['frontend/react', 'frontend/vue']); + + const backend = categories.find((c) => c.name === 'backend'); + expect(backend).toBeDefined(); + expect(backend!.type === 'category' && backend!.pieces).toEqual(['backend/api']); + }); + + it('should sort items alphabetically', () => { + const entries: PieceDirEntry[] = [ + { name: 'zebra', path: '/tmp/zebra.yaml', source: 'project' }, + { name: 'alpha', path: '/tmp/alpha.yaml', source: 'project' }, + { name: 'misc/playground', path: '/tmp/misc/playground.yaml', category: 'misc', source: 'project' }, + ]; + + const items = buildPieceSelectionItems(entries); + const names = items.map((i) => i.name); + expect(names).toEqual(['alpha', 'misc', 'zebra']); + }); + + it('should return empty array for empty input', () => { + const items = buildPieceSelectionItems([]); + expect(items).toEqual([]); + }); +}); + +describe('2-stage category selection helpers', () => { + const items: PieceSelectionItem[] = [ + { type: 'piece', name: 'simple' }, + { type: 'category', name: 'frontend', pieces: ['frontend/react', 'frontend/vue'] }, + { type: 'category', name: 'backend', pieces: ['backend/api'] }, + ]; + + describe('buildTopLevelSelectOptions', () => { + it('should encode categories with prefix in value', () => { + const options = buildTopLevelSelectOptions(items, ''); + const categoryOption = options.find((o) => o.label.includes('frontend')); + expect(categoryOption).toBeDefined(); + expect(categoryOption!.value).toBe('__category__:frontend'); + }); + + it('should mark current piece', () => { + const options = buildTopLevelSelectOptions(items, 'simple'); + const simpleOption = options.find((o) => o.value === 'simple'); + expect(simpleOption!.label).toContain('(current)'); + }); + + it('should mark category containing current piece', () => { + const options = buildTopLevelSelectOptions(items, 'frontend/react'); + const frontendOption = options.find((o) => o.value === '__category__:frontend'); + expect(frontendOption!.label).toContain('(current)'); + }); + }); + + describe('parseCategorySelection', () => { + it('should return category name for category selection', () => { + expect(parseCategorySelection('__category__:frontend')).toBe('frontend'); + }); + + it('should return null for direct piece selection', () => { + expect(parseCategorySelection('simple')).toBeNull(); + }); + }); + + describe('buildCategoryPieceOptions', () => { + it('should return options for pieces in a category', () => { + const options = buildCategoryPieceOptions(items, 'frontend', ''); + expect(options).not.toBeNull(); + expect(options).toHaveLength(2); + expect(options![0]!.value).toBe('frontend/react'); + expect(options![0]!.label).toBe('react'); + }); + + it('should mark current piece in category', () => { + const options = buildCategoryPieceOptions(items, 'frontend', 'frontend/vue'); + const vueOption = options!.find((o) => o.value === 'frontend/vue'); + expect(vueOption!.label).toContain('(current)'); + }); + + it('should return null for non-existent category', () => { + expect(buildCategoryPieceOptions(items, 'nonexistent', '')).toBeNull(); + }); + }); +}); diff --git a/src/__tests__/workflow-category-config.test.ts b/src/__tests__/piece-category-config.test.ts similarity index 55% rename from src/__tests__/workflow-category-config.test.ts rename to src/__tests__/piece-category-config.test.ts index db08ce3..6d4deb5 100644 --- a/src/__tests__/workflow-category-config.test.ts +++ b/src/__tests__/piece-category-config.test.ts @@ -1,5 +1,5 @@ /** - * Tests for workflow category configuration loading and building + * Tests for piece category configuration loading and building */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -7,7 +7,7 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'node:crypto'; -import type { WorkflowWithSource } from '../infra/config/index.js'; +import type { PieceWithSource } from '../infra/config/index.js'; const pathsState = vi.hoisted(() => ({ globalConfigPath: '', @@ -32,7 +32,7 @@ vi.mock('../infra/resources/index.js', async (importOriginal) => { }; }); -const workflowCategoriesState = vi.hoisted(() => ({ +const pieceCategoriesState = vi.hoisted(() => ({ categories: undefined as any, showOthersCategory: undefined as boolean | undefined, othersCategoryName: undefined as string | undefined, @@ -46,32 +46,32 @@ vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => { }; }); -vi.mock('../infra/config/global/workflowCategories.js', async (importOriginal) => { +vi.mock('../infra/config/global/pieceCategories.js', async (importOriginal) => { const original = await importOriginal() as Record; return { ...original, - getWorkflowCategoriesConfig: () => workflowCategoriesState.categories, - getShowOthersCategory: () => workflowCategoriesState.showOthersCategory, - getOthersCategoryName: () => workflowCategoriesState.othersCategoryName, + getPieceCategoriesConfig: () => pieceCategoriesState.categories, + getShowOthersCategory: () => pieceCategoriesState.showOthersCategory, + getOthersCategoryName: () => pieceCategoriesState.othersCategoryName, }; }); const { - getWorkflowCategories, + getPieceCategories, loadDefaultCategories, - buildCategorizedWorkflows, - findWorkflowCategories, -} = await import('../infra/config/loaders/workflowCategories.js'); + buildCategorizedPieces, + findPieceCategories, +} = await import('../infra/config/loaders/pieceCategories.js'); function writeYaml(path: string, content: string): void { writeFileSync(path, content.trim() + '\n', 'utf-8'); } -function createWorkflowMap(entries: { name: string; source: 'builtin' | 'user' | 'project' }[]): - Map { - const workflows = new Map(); +function createPieceMap(entries: { name: string; source: 'builtin' | 'user' | 'project' }[]): + Map { + const pieces = new Map(); for (const entry of entries) { - workflows.set(entry.name, { + pieces.set(entry.name, { source: entry.source, config: { name: entry.name, @@ -81,10 +81,10 @@ function createWorkflowMap(entries: { name: string; source: 'builtin' | 'user' | }, }); } - return workflows; + return pieces; } -describe('workflow category config loading', () => { +describe('piece category config loading', () => { let testDir: string; let resourcesDir: string; let globalConfigPath: string; @@ -101,91 +101,91 @@ describe('workflow category config loading', () => { pathsState.projectConfigPath = projectConfigPath; pathsState.resourcesDir = resourcesDir; - // Reset workflow categories state - workflowCategoriesState.categories = undefined; - workflowCategoriesState.showOthersCategory = undefined; - workflowCategoriesState.othersCategoryName = undefined; + // 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 workflow_categories', () => { + it('should load default categories when no configs define piece_categories', () => { writeYaml(join(resourcesDir, 'default-categories.yaml'), ` -workflow_categories: +piece_categories: Default: - workflows: + pieces: - simple show_others_category: true others_category_name: "Others" `); - const config = getWorkflowCategories(testDir); + const config = getPieceCategories(testDir); expect(config).not.toBeNull(); - expect(config!.workflowCategories).toEqual([ - { name: 'Default', workflows: ['simple'], children: [] }, + expect(config!.pieceCategories).toEqual([ + { name: 'Default', pieces: ['simple'], children: [] }, ]); }); - it('should prefer project config over default when workflow_categories is defined', () => { + it('should prefer project config over default when piece_categories is defined', () => { writeYaml(join(resourcesDir, 'default-categories.yaml'), ` -workflow_categories: +piece_categories: Default: - workflows: + pieces: - simple `); writeYaml(projectConfigPath, ` -workflow_categories: +piece_categories: Project: - workflows: + pieces: - custom show_others_category: false `); - const config = getWorkflowCategories(testDir); + const config = getPieceCategories(testDir); expect(config).not.toBeNull(); - expect(config!.workflowCategories).toEqual([ - { name: 'Project', workflows: ['custom'], children: [] }, + expect(config!.pieceCategories).toEqual([ + { name: 'Project', pieces: ['custom'], children: [] }, ]); expect(config!.showOthersCategory).toBe(false); }); - it('should prefer user config over project config when workflow_categories is defined', () => { + it('should prefer user config over project config when piece_categories is defined', () => { writeYaml(join(resourcesDir, 'default-categories.yaml'), ` -workflow_categories: +piece_categories: Default: - workflows: + pieces: - simple `); writeYaml(projectConfigPath, ` -workflow_categories: +piece_categories: Project: - workflows: + pieces: - custom `); // Simulate user config from separate file - workflowCategoriesState.categories = { + pieceCategoriesState.categories = { User: { - workflows: ['preferred'], + pieces: ['preferred'], }, }; - const config = getWorkflowCategories(testDir); + const config = getPieceCategories(testDir); expect(config).not.toBeNull(); - expect(config!.workflowCategories).toEqual([ - { name: 'User', workflows: ['preferred'], children: [] }, + expect(config!.pieceCategories).toEqual([ + { name: 'User', pieces: ['preferred'], children: [] }, ]); }); - it('should ignore configs without workflow_categories and fall back to default', () => { + it('should ignore configs without piece_categories and fall back to default', () => { writeYaml(join(resourcesDir, 'default-categories.yaml'), ` -workflow_categories: +piece_categories: Default: - workflows: + pieces: - simple `); @@ -193,10 +193,10 @@ workflow_categories: show_others_category: false `); - const config = getWorkflowCategories(testDir); + const config = getPieceCategories(testDir); expect(config).not.toBeNull(); - expect(config!.workflowCategories).toEqual([ - { name: 'Default', workflows: ['simple'], children: [] }, + expect(config!.pieceCategories).toEqual([ + { name: 'Default', pieces: ['simple'], children: [] }, ]); }); @@ -206,18 +206,18 @@ show_others_category: false }); }); -describe('buildCategorizedWorkflows', () => { - it('should warn for missing workflows and generate Others', () => { - const allWorkflows = createWorkflowMap([ +describe('buildCategorizedPieces', () => { + it('should warn for missing pieces and generate Others', () => { + const allPieces = createPieceMap([ { name: 'a', source: 'user' }, { name: 'b', source: 'user' }, { name: 'c', source: 'builtin' }, ]); const config = { - workflowCategories: [ + pieceCategories: [ { name: 'Cat', - workflows: ['a', 'missing', 'c'], + pieces: ['a', 'missing', 'c'], children: [], }, ], @@ -225,43 +225,43 @@ describe('buildCategorizedWorkflows', () => { othersCategoryName: 'Others', }; - const categorized = buildCategorizedWorkflows(allWorkflows, config); + const categorized = buildCategorizedPieces(allPieces, config); expect(categorized.categories).toEqual([ - { name: 'Cat', workflows: ['a'], children: [] }, - { name: 'Others', workflows: ['b'], children: [] }, + { name: 'Cat', pieces: ['a'], children: [] }, + { name: 'Others', pieces: ['b'], children: [] }, ]); expect(categorized.builtinCategories).toEqual([ - { name: 'Cat', workflows: ['c'], children: [] }, + { name: 'Cat', pieces: ['c'], children: [] }, ]); - expect(categorized.missingWorkflows).toEqual([ - { categoryPath: ['Cat'], workflowName: 'missing' }, + expect(categorized.missingPieces).toEqual([ + { categoryPath: ['Cat'], pieceName: 'missing' }, ]); }); it('should skip empty categories', () => { - const allWorkflows = createWorkflowMap([ + const allPieces = createPieceMap([ { name: 'a', source: 'user' }, ]); const config = { - workflowCategories: [ - { name: 'Empty', workflows: [], children: [] }, + pieceCategories: [ + { name: 'Empty', pieces: [], children: [] }, ], showOthersCategory: false, othersCategoryName: 'Others', }; - const categorized = buildCategorizedWorkflows(allWorkflows, config); + const categorized = buildCategorizedPieces(allPieces, config); expect(categorized.categories).toEqual([]); expect(categorized.builtinCategories).toEqual([]); }); - it('should find categories containing a workflow', () => { + it('should find categories containing a piece', () => { const categories = [ - { name: 'A', workflows: ['shared'], children: [] }, - { name: 'B', workflows: ['shared'], children: [] }, + { name: 'A', pieces: ['shared'], children: [] }, + { name: 'B', pieces: ['shared'], children: [] }, ]; - const paths = findWorkflowCategories('shared', categories).sort(); + const paths = findPieceCategories('shared', categories).sort(); expect(paths).toEqual(['A', 'B']); }); @@ -269,14 +269,14 @@ describe('buildCategorizedWorkflows', () => { const categories = [ { name: 'Parent', - workflows: [], + pieces: [], children: [ - { name: 'Child', workflows: ['nested'], children: [] }, + { name: 'Child', pieces: ['nested'], children: [] }, ], }, ]; - const paths = findWorkflowCategories('nested', categories); + const paths = findPieceCategories('nested', categories); expect(paths).toEqual(['Parent / Child']); }); }); diff --git a/src/__tests__/workflow-expert-parallel.test.ts b/src/__tests__/piece-expert-parallel.test.ts similarity index 74% rename from src/__tests__/workflow-expert-parallel.test.ts rename to src/__tests__/piece-expert-parallel.test.ts index 6514cae..01c7b68 100644 --- a/src/__tests__/workflow-expert-parallel.test.ts +++ b/src/__tests__/piece-expert-parallel.test.ts @@ -1,8 +1,8 @@ /** - * Tests for expert/expert-cqrs workflow parallel review structure. + * Tests for expert/expert-cqrs piece parallel review structure. * * Validates that: - * - expert and expert-cqrs workflows load successfully via loadWorkflow + * - expert and expert-cqrs pieces load successfully via loadPiece * - The reviewers movement is a parallel movement with expected sub-movements * - ai_review routes to reviewers (not individual review movements) * - fix movement routes back to reviewers @@ -11,25 +11,25 @@ */ import { describe, it, expect } from 'vitest'; -import { loadWorkflow } from '../infra/config/index.js'; +import { loadPiece } from '../infra/config/index.js'; -describe('expert workflow parallel structure', () => { - const workflow = loadWorkflow('expert', process.cwd()); +describe('expert piece parallel structure', () => { + const piece = loadPiece('expert', process.cwd()); it('should load successfully', () => { - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('expert'); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('expert'); }); it('should have a reviewers parallel movement', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); expect(reviewers).toBeDefined(); expect(reviewers!.parallel).toBeDefined(); expect(reviewers!.parallel!.length).toBe(4); }); it('should have arch-review, frontend-review, security-review, qa-review as sub-movements', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); const subNames = reviewers!.parallel!.map((s) => s.name); expect(subNames).toContain('arch-review'); expect(subNames).toContain('frontend-review'); @@ -38,7 +38,7 @@ describe('expert workflow parallel structure', () => { }); it('should have aggregate rules on reviewers movement', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); expect(reviewers!.rules).toBeDefined(); const conditions = reviewers!.rules!.map((r) => r.condition); expect(conditions).toContain('all("approved")'); @@ -46,7 +46,7 @@ describe('expert workflow parallel structure', () => { }); it('should have simple approved/needs_fix rules on each sub-movement', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); for (const sub of reviewers!.parallel!) { expect(sub.rules).toBeDefined(); const conditions = sub.rules!.map((r) => r.condition); @@ -56,21 +56,21 @@ describe('expert workflow parallel structure', () => { }); it('should route ai_review to reviewers', () => { - const aiReview = workflow!.movements.find((s) => s.name === 'ai_review'); + const aiReview = piece!.movements.find((s) => s.name === 'ai_review'); expect(aiReview).toBeDefined(); const approvedRule = aiReview!.rules!.find((r) => r.next === 'reviewers'); expect(approvedRule).toBeDefined(); }); it('should have a unified fix movement routing back to reviewers', () => { - const fix = workflow!.movements.find((s) => s.name === 'fix'); + const fix = piece!.movements.find((s) => s.name === 'fix'); expect(fix).toBeDefined(); const fixComplete = fix!.rules!.find((r) => r.next === 'reviewers'); expect(fixComplete).toBeDefined(); }); it('should not have individual review/fix movements', () => { - const movementNames = workflow!.movements.map((s) => s.name); + const movementNames = piece!.movements.map((s) => s.name); expect(movementNames).not.toContain('architect_review'); expect(movementNames).not.toContain('fix_architect'); expect(movementNames).not.toContain('frontend_review'); @@ -82,35 +82,35 @@ describe('expert workflow parallel structure', () => { }); it('should route reviewers all("approved") to supervise', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); const approvedRule = reviewers!.rules!.find((r) => r.condition === 'all("approved")'); expect(approvedRule!.next).toBe('supervise'); }); it('should route reviewers any("needs_fix") to fix', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); const needsFixRule = reviewers!.rules!.find((r) => r.condition === 'any("needs_fix")'); expect(needsFixRule!.next).toBe('fix'); }); }); -describe('expert-cqrs workflow parallel structure', () => { - const workflow = loadWorkflow('expert-cqrs', process.cwd()); +describe('expert-cqrs piece parallel structure', () => { + const piece = loadPiece('expert-cqrs', process.cwd()); it('should load successfully', () => { - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('expert-cqrs'); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('expert-cqrs'); }); it('should have a reviewers parallel movement', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); expect(reviewers).toBeDefined(); expect(reviewers!.parallel).toBeDefined(); expect(reviewers!.parallel!.length).toBe(4); }); it('should have cqrs-es-review instead of arch-review', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); const subNames = reviewers!.parallel!.map((s) => s.name); expect(subNames).toContain('cqrs-es-review'); expect(subNames).not.toContain('arch-review'); @@ -120,7 +120,7 @@ describe('expert-cqrs workflow parallel structure', () => { }); it('should have aggregate rules on reviewers movement', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); expect(reviewers!.rules).toBeDefined(); const conditions = reviewers!.rules!.map((r) => r.condition); expect(conditions).toContain('all("approved")'); @@ -128,7 +128,7 @@ describe('expert-cqrs workflow parallel structure', () => { }); it('should have simple approved/needs_fix rules on each sub-movement', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); for (const sub of reviewers!.parallel!) { expect(sub.rules).toBeDefined(); const conditions = sub.rules!.map((r) => r.condition); @@ -138,21 +138,21 @@ describe('expert-cqrs workflow parallel structure', () => { }); it('should route ai_review to reviewers', () => { - const aiReview = workflow!.movements.find((s) => s.name === 'ai_review'); + const aiReview = piece!.movements.find((s) => s.name === 'ai_review'); expect(aiReview).toBeDefined(); const approvedRule = aiReview!.rules!.find((r) => r.next === 'reviewers'); expect(approvedRule).toBeDefined(); }); it('should have a unified fix movement routing back to reviewers', () => { - const fix = workflow!.movements.find((s) => s.name === 'fix'); + const fix = piece!.movements.find((s) => s.name === 'fix'); expect(fix).toBeDefined(); const fixComplete = fix!.rules!.find((r) => r.next === 'reviewers'); expect(fixComplete).toBeDefined(); }); it('should not have individual review/fix movements', () => { - const movementNames = workflow!.movements.map((s) => s.name); + const movementNames = piece!.movements.map((s) => s.name); expect(movementNames).not.toContain('cqrs_es_review'); expect(movementNames).not.toContain('fix_cqrs_es'); expect(movementNames).not.toContain('frontend_review'); @@ -164,7 +164,7 @@ describe('expert-cqrs workflow parallel structure', () => { }); it('should use cqrs-es-reviewer agent for the first sub-movement', () => { - const reviewers = workflow!.movements.find((s) => s.name === 'reviewers'); + const reviewers = piece!.movements.find((s) => s.name === 'reviewers'); const cqrsReview = reviewers!.parallel!.find((s) => s.name === 'cqrs-es-review'); expect(cqrsReview!.agent).toContain('cqrs-es-reviewer'); }); diff --git a/src/__tests__/workflow-selection.test.ts b/src/__tests__/piece-selection.test.ts similarity index 58% rename from src/__tests__/workflow-selection.test.ts rename to src/__tests__/piece-selection.test.ts index 1cd1ea8..70269bb 100644 --- a/src/__tests__/workflow-selection.test.ts +++ b/src/__tests__/piece-selection.test.ts @@ -1,9 +1,9 @@ /** - * Tests for workflow selection helpers + * Tests for piece selection helpers */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { WorkflowDirEntry } from '../infra/config/loaders/workflowLoader.js'; +import type { PieceDirEntry } from '../infra/config/loaders/pieceLoader.js'; const selectOptionMock = vi.fn(); @@ -12,19 +12,19 @@ vi.mock('../shared/prompt/index.js', () => ({ })); vi.mock('../infra/config/global/index.js', () => ({ - getBookmarkedWorkflows: () => [], + getBookmarkedPieces: () => [], toggleBookmark: vi.fn(), })); -const { selectWorkflowFromEntries } = await import('../features/workflowSelection/index.js'); +const { selectPieceFromEntries } = await import('../features/pieceSelection/index.js'); -describe('selectWorkflowFromEntries', () => { +describe('selectPieceFromEntries', () => { beforeEach(() => { selectOptionMock.mockReset(); }); - it('should select from custom workflows when source is chosen', async () => { - const entries: WorkflowDirEntry[] = [ + it('should select from custom pieces when source is chosen', async () => { + const entries: PieceDirEntry[] = [ { name: 'custom-flow', path: '/tmp/custom.yaml', source: 'user' }, { name: 'builtin-flow', path: '/tmp/builtin.yaml', source: 'builtin' }, ]; @@ -33,19 +33,19 @@ describe('selectWorkflowFromEntries', () => { .mockResolvedValueOnce('custom') .mockResolvedValueOnce('custom-flow'); - const selected = await selectWorkflowFromEntries(entries, ''); + const selected = await selectPieceFromEntries(entries, ''); expect(selected).toBe('custom-flow'); expect(selectOptionMock).toHaveBeenCalledTimes(2); }); - it('should skip source selection when only builtin workflows exist', async () => { - const entries: WorkflowDirEntry[] = [ + it('should skip source selection when only builtin pieces exist', async () => { + const entries: PieceDirEntry[] = [ { name: 'builtin-flow', path: '/tmp/builtin.yaml', source: 'builtin' }, ]; selectOptionMock.mockResolvedValueOnce('builtin-flow'); - const selected = await selectWorkflowFromEntries(entries, ''); + const selected = await selectPieceFromEntries(entries, ''); expect(selected).toBe('builtin-flow'); expect(selectOptionMock).toHaveBeenCalledTimes(1); }); diff --git a/src/__tests__/pieceLoader.test.ts b/src/__tests__/pieceLoader.test.ts new file mode 100644 index 0000000..f85cd08 --- /dev/null +++ b/src/__tests__/pieceLoader.test.ts @@ -0,0 +1,189 @@ +/** + * Tests for isPiecePath and loadPieceByIdentifier + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { + isPiecePath, + loadPieceByIdentifier, + listPieces, + loadAllPieces, +} from '../infra/config/loaders/pieceLoader.js'; + +const SAMPLE_PIECE = `name: test-piece +description: Test piece +initial_movement: step1 +max_iterations: 1 + +movements: + - name: step1 + agent: coder + instruction: "{task}" +`; + +describe('isPiecePath', () => { + it('should return true for absolute paths', () => { + expect(isPiecePath('/path/to/piece.yaml')).toBe(true); + expect(isPiecePath('/piece')).toBe(true); + }); + + it('should return true for home directory paths', () => { + expect(isPiecePath('~/piece.yaml')).toBe(true); + expect(isPiecePath('~/.takt/pieces/custom.yaml')).toBe(true); + }); + + it('should return true for relative paths starting with ./', () => { + expect(isPiecePath('./piece.yaml')).toBe(true); + expect(isPiecePath('./subdir/piece.yaml')).toBe(true); + }); + + it('should return true for relative paths starting with ../', () => { + expect(isPiecePath('../piece.yaml')).toBe(true); + expect(isPiecePath('../subdir/piece.yaml')).toBe(true); + }); + + it('should return true for paths ending with .yaml', () => { + expect(isPiecePath('custom.yaml')).toBe(true); + expect(isPiecePath('my-piece.yaml')).toBe(true); + }); + + it('should return true for paths ending with .yml', () => { + expect(isPiecePath('custom.yml')).toBe(true); + expect(isPiecePath('my-piece.yml')).toBe(true); + }); + + it('should return false for plain piece names', () => { + expect(isPiecePath('default')).toBe(false); + expect(isPiecePath('simple')).toBe(false); + expect(isPiecePath('magi')).toBe(false); + expect(isPiecePath('my-custom-piece')).toBe(false); + }); +}); + +describe('loadPieceByIdentifier', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-test-')); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should load piece by name (builtin)', () => { + const piece = loadPieceByIdentifier('default', process.cwd()); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('default'); + }); + + it('should load piece by absolute path', () => { + const filePath = join(tempDir, 'test.yaml'); + writeFileSync(filePath, SAMPLE_PIECE); + + const piece = loadPieceByIdentifier(filePath, tempDir); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('test-piece'); + }); + + it('should load piece by relative path', () => { + const filePath = join(tempDir, 'test.yaml'); + writeFileSync(filePath, SAMPLE_PIECE); + + const piece = loadPieceByIdentifier('./test.yaml', tempDir); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('test-piece'); + }); + + it('should load piece by filename with .yaml extension', () => { + const filePath = join(tempDir, 'test.yaml'); + writeFileSync(filePath, SAMPLE_PIECE); + + const piece = loadPieceByIdentifier('test.yaml', tempDir); + expect(piece).not.toBeNull(); + expect(piece!.name).toBe('test-piece'); + }); + + it('should return null for non-existent name', () => { + const piece = loadPieceByIdentifier('non-existent-piece-xyz', process.cwd()); + expect(piece).toBeNull(); + }); + + it('should return null for non-existent path', () => { + const piece = loadPieceByIdentifier('./non-existent.yaml', tempDir); + expect(piece).toBeNull(); + }); +}); + +describe('listPieces with project-local', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-test-')); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should include project-local pieces when cwd is provided', () => { + const projectPiecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(projectPiecesDir, { recursive: true }); + writeFileSync(join(projectPiecesDir, 'project-custom.yaml'), SAMPLE_PIECE); + + const pieces = listPieces(tempDir); + expect(pieces).toContain('project-custom'); + }); + + it('should include builtin pieces regardless of cwd', () => { + const pieces = listPieces(tempDir); + expect(pieces).toContain('default'); + }); + +}); + +describe('loadAllPieces with project-local', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'takt-test-')); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should include project-local pieces when cwd is provided', () => { + const projectPiecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(projectPiecesDir, { recursive: true }); + writeFileSync(join(projectPiecesDir, 'project-custom.yaml'), SAMPLE_PIECE); + + const pieces = loadAllPieces(tempDir); + expect(pieces.has('project-custom')).toBe(true); + expect(pieces.get('project-custom')!.name).toBe('test-piece'); + }); + + it('should have project-local override builtin when same name', () => { + const projectPiecesDir = join(tempDir, '.takt', 'pieces'); + mkdirSync(projectPiecesDir, { recursive: true }); + + const overridePiece = `name: project-override +description: Project override +initial_movement: step1 +max_iterations: 1 + +movements: + - name: step1 + agent: coder + instruction: "{task}" +`; + writeFileSync(join(projectPiecesDir, 'default.yaml'), overridePiece); + + const pieces = loadAllPieces(tempDir); + expect(pieces.get('default')!.name).toBe('project-override'); + }); + +}); diff --git a/src/__tests__/pipelineExecution.test.ts b/src/__tests__/pipelineExecution.test.ts index cf74503..efc7c38 100644 --- a/src/__tests__/pipelineExecution.test.ts +++ b/src/__tests__/pipelineExecution.test.ts @@ -76,7 +76,7 @@ describe('executePipeline', () => { mockLoadGlobalConfig.mockReturnValue({ language: 'en', trustedDirectories: [], - defaultWorkflow: 'default', + defaultPiece: 'default', logLevel: 'info', provider: 'claude', }); @@ -84,7 +84,7 @@ describe('executePipeline', () => { it('should return exit code 2 when neither --issue nor --task is specified', async () => { const exitCode = await executePipeline({ - workflow: 'default', + piece: 'default', autoPr: false, cwd: '/tmp/test', }); @@ -97,7 +97,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ issueNumber: 99, - workflow: 'default', + piece: 'default', autoPr: false, cwd: '/tmp/test', }); @@ -112,7 +112,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ issueNumber: 999, - workflow: 'default', + piece: 'default', autoPr: false, cwd: '/tmp/test', }); @@ -120,7 +120,7 @@ describe('executePipeline', () => { expect(exitCode).toBe(2); }); - it('should return exit code 3 when workflow fails', async () => { + it('should return exit code 3 when piece fails', async () => { mockFetchIssue.mockReturnValueOnce({ number: 99, title: 'Test issue', @@ -132,7 +132,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ issueNumber: 99, - workflow: 'default', + piece: 'default', autoPr: false, cwd: '/tmp/test', }); @@ -145,7 +145,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ task: 'Fix the bug', - workflow: 'default', + piece: 'default', autoPr: false, cwd: '/tmp/test', }); @@ -154,7 +154,7 @@ describe('executePipeline', () => { expect(mockExecuteTask).toHaveBeenCalledWith({ task: 'Fix the bug', cwd: '/tmp/test', - workflowIdentifier: 'default', + pieceIdentifier: 'default', projectCwd: '/tmp/test', agentOverrides: undefined, }); @@ -165,7 +165,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ task: 'Fix the bug', - workflow: 'default', + piece: 'default', autoPr: false, cwd: '/tmp/test', provider: 'codex', @@ -176,7 +176,7 @@ describe('executePipeline', () => { expect(mockExecuteTask).toHaveBeenCalledWith({ task: 'Fix the bug', cwd: '/tmp/test', - workflowIdentifier: 'default', + pieceIdentifier: 'default', projectCwd: '/tmp/test', agentOverrides: { provider: 'codex', model: 'codex-model' }, }); @@ -188,7 +188,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ task: 'Fix the bug', - workflow: 'default', + piece: 'default', autoPr: true, cwd: '/tmp/test', }); @@ -202,7 +202,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ task: 'Fix the bug', - workflow: 'default', + piece: 'default', branch: 'fix/my-branch', autoPr: true, repo: 'owner/repo', @@ -224,7 +224,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ task: 'From --task flag', - workflow: 'magi', + piece: 'magi', autoPr: false, cwd: '/tmp/test', }); @@ -233,7 +233,7 @@ describe('executePipeline', () => { expect(mockExecuteTask).toHaveBeenCalledWith({ task: 'From --task flag', cwd: '/tmp/test', - workflowIdentifier: 'magi', + pieceIdentifier: 'magi', projectCwd: '/tmp/test', agentOverrides: undefined, }); @@ -244,7 +244,7 @@ describe('executePipeline', () => { mockLoadGlobalConfig.mockReturnValue({ language: 'en', trustedDirectories: [], - defaultWorkflow: 'default', + defaultPiece: 'default', logLevel: 'info', provider: 'claude', pipeline: { @@ -263,7 +263,7 @@ describe('executePipeline', () => { await executePipeline({ issueNumber: 42, - workflow: 'default', + piece: 'default', branch: 'test-branch', autoPr: false, cwd: '/tmp/test', @@ -281,7 +281,7 @@ describe('executePipeline', () => { mockLoadGlobalConfig.mockReturnValue({ language: 'en', trustedDirectories: [], - defaultWorkflow: 'default', + defaultPiece: 'default', logLevel: 'info', provider: 'claude', pipeline: { @@ -300,7 +300,7 @@ describe('executePipeline', () => { await executePipeline({ issueNumber: 10, - workflow: 'default', + piece: 'default', autoPr: false, cwd: '/tmp/test', }); @@ -318,7 +318,7 @@ describe('executePipeline', () => { mockLoadGlobalConfig.mockReturnValue({ language: 'en', trustedDirectories: [], - defaultWorkflow: 'default', + defaultPiece: 'default', logLevel: 'info', provider: 'claude', pipeline: { @@ -338,7 +338,7 @@ describe('executePipeline', () => { await executePipeline({ issueNumber: 50, - workflow: 'default', + piece: 'default', branch: 'fix-auth', autoPr: true, cwd: '/tmp/test', @@ -360,7 +360,7 @@ describe('executePipeline', () => { await executePipeline({ task: 'Fix bug', - workflow: 'default', + piece: 'default', branch: 'fix-branch', autoPr: true, cwd: '/tmp/test', @@ -383,7 +383,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ task: 'Fix the bug', - workflow: 'default', + piece: 'default', autoPr: false, skipGit: true, cwd: '/tmp/test', @@ -393,7 +393,7 @@ describe('executePipeline', () => { expect(mockExecuteTask).toHaveBeenCalledWith({ task: 'Fix the bug', cwd: '/tmp/test', - workflowIdentifier: 'default', + pieceIdentifier: 'default', projectCwd: '/tmp/test', agentOverrides: undefined, }); @@ -411,7 +411,7 @@ describe('executePipeline', () => { const exitCode = await executePipeline({ task: 'Fix the bug', - workflow: 'default', + piece: 'default', autoPr: true, skipGit: true, cwd: '/tmp/test', @@ -421,12 +421,12 @@ describe('executePipeline', () => { expect(mockCreatePullRequest).not.toHaveBeenCalled(); }); - it('should still return workflow failure exit code when skipGit is true', async () => { + it('should still return piece failure exit code when skipGit is true', async () => { mockExecuteTask.mockResolvedValueOnce(false); const exitCode = await executePipeline({ task: 'Fix the bug', - workflow: 'default', + piece: 'default', autoPr: false, skipGit: true, cwd: '/tmp/test', diff --git a/src/__tests__/prompts.test.ts b/src/__tests__/prompts.test.ts index 0b8ba65..22328be 100644 --- a/src/__tests__/prompts.test.ts +++ b/src/__tests__/prompts.test.ts @@ -39,7 +39,7 @@ describe('variable substitution', () => { it('replaces {{variableName}} placeholders with provided values', () => { const result = loadTemplate('perform_builtin_agent_system_prompt', 'en', { agentName: 'test-agent' }); expect(result).toContain('You are the test-agent agent'); - expect(result).toContain('Follow the standard test-agent workflow'); + expect(result).toContain('Follow the standard test-agent piece'); }); it('replaces undefined variables with empty string', () => { @@ -57,13 +57,13 @@ describe('variable substitution', () => { expect(result).toContain('| 1 | Success |'); }); - it('replaces workflow info variables in interactive prompt', () => { + it('replaces piece info variables in interactive prompt', () => { const result = loadTemplate('score_interactive_system_prompt', 'en', { - workflowInfo: true, - workflowName: 'my-workflow', - workflowDescription: 'Test description', + pieceInfo: true, + pieceName: 'my-piece', + pieceDescription: 'Test description', }); - expect(result).toContain('"my-workflow"'); + expect(result).toContain('"my-piece"'); expect(result).toContain('Test description'); }); }); @@ -189,11 +189,11 @@ describe('template content integrity', () => { expect(en).toContain('## Execution Rules'); expect(en).toContain('Do NOT run git commit'); expect(en).toContain('Do NOT use `cd`'); - expect(en).toContain('## Workflow Context'); + expect(en).toContain('## Piece Context'); expect(en).toContain('## Instructions'); }); - it('perform_phase1_message contains workflow context variables', () => { + it('perform_phase1_message contains piece context variables', () => { const en = loadTemplate('perform_phase1_message', 'en'); expect(en).toContain('{{iteration}}'); expect(en).toContain('{{movement}}'); diff --git a/src/__tests__/review-only-workflow.test.ts b/src/__tests__/review-only-piece.test.ts similarity index 92% rename from src/__tests__/review-only-workflow.test.ts rename to src/__tests__/review-only-piece.test.ts index 9e08b9b..e37ad73 100644 --- a/src/__tests__/review-only-workflow.test.ts +++ b/src/__tests__/review-only-piece.test.ts @@ -1,9 +1,9 @@ /** - * Tests for review-only workflow + * Tests for review-only piece * * Covers: - * - Workflow YAML files (EN/JA) load and pass schema validation - * - Workflow structure: plan -> reviewers (parallel) -> supervise -> pr-comment + * - Piece YAML files (EN/JA) load and pass schema validation + * - Piece structure: plan -> reviewers (parallel) -> supervise -> pr-comment * - All movements have edit: false * - pr-commenter agent has Bash in allowed_tools * - Routing rules for local vs PR comment flows @@ -13,7 +13,7 @@ import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { parse as parseYaml } from 'yaml'; -import { WorkflowConfigRawSchema } from '../core/models/index.js'; +import { PieceConfigRawSchema } from '../core/models/index.js'; const RESOURCES_DIR = join(import.meta.dirname, '../../resources/global'); @@ -23,11 +23,11 @@ function loadReviewOnlyYaml(lang: 'en' | 'ja') { return parseYaml(content); } -describe('review-only workflow (EN)', () => { +describe('review-only piece (EN)', () => { const raw = loadReviewOnlyYaml('en'); it('should pass schema validation', () => { - const result = WorkflowConfigRawSchema.safeParse(raw); + const result = PieceConfigRawSchema.safeParse(raw); expect(result.success).toBe(true); }); @@ -137,11 +137,11 @@ describe('review-only workflow (EN)', () => { }); }); -describe('review-only workflow (JA)', () => { +describe('review-only piece (JA)', () => { const raw = loadReviewOnlyYaml('ja'); it('should pass schema validation', () => { - const result = WorkflowConfigRawSchema.safeParse(raw); + const result = PieceConfigRawSchema.safeParse(raw); expect(result.success).toBe(true); }); @@ -203,21 +203,21 @@ describe('pr-commenter agent files', () => { expect(content).toContain('gh pr comment'); }); - it('should NOT contain workflow-specific report names (EN)', () => { + it('should NOT contain piece-specific report names (EN)', () => { const filePath = join(RESOURCES_DIR, 'en', 'agents', 'review', 'pr-commenter.md'); const content = readFileSync(filePath, 'utf-8'); - // Agent should not reference specific review-only workflow report files + // Agent should not reference specific review-only piece report files expect(content).not.toContain('01-architect-review.md'); expect(content).not.toContain('02-security-review.md'); expect(content).not.toContain('03-ai-review.md'); expect(content).not.toContain('04-review-summary.md'); - // Agent should not reference specific reviewer names from review-only workflow + // Agent should not reference specific reviewer names from review-only piece expect(content).not.toContain('Architecture review report'); expect(content).not.toContain('Security review report'); expect(content).not.toContain('AI antipattern review report'); }); - it('should NOT contain workflow-specific report names (JA)', () => { + it('should NOT contain piece-specific report names (JA)', () => { const filePath = join(RESOURCES_DIR, 'ja', 'agents', 'review', 'pr-commenter.md'); const content = readFileSync(filePath, 'utf-8'); expect(content).not.toContain('01-architect-review.md'); @@ -227,7 +227,7 @@ describe('pr-commenter agent files', () => { }); }); -describe('pr-comment instruction_template contains workflow-specific procedures', () => { +describe('pr-comment instruction_template contains piece-specific procedures', () => { it('EN: should reference specific report files', () => { const raw = loadReviewOnlyYaml('en'); const prComment = raw.movements.find((s: { name: string }) => s.name === 'pr-comment'); diff --git a/src/__tests__/session.test.ts b/src/__tests__/session.test.ts index 100ebb5..fd0bc00 100644 --- a/src/__tests__/session.test.ts +++ b/src/__tests__/session.test.ts @@ -17,8 +17,8 @@ import { type SessionLog, type NdjsonRecord, type NdjsonStepComplete, - type NdjsonWorkflowComplete, - type NdjsonWorkflowAbort, + type NdjsonPieceComplete, + type NdjsonPieceAbort, type NdjsonPhaseStart, type NdjsonPhaseComplete, type NdjsonInteractiveStart, @@ -56,7 +56,7 @@ describe('updateLatestPointer', () => { expect(pointer.sessionId).toBe('abc-123'); expect(pointer.logFile).toBe('abc-123.jsonl'); expect(pointer.task).toBe('my task'); - expect(pointer.workflowName).toBe('default'); + expect(pointer.pieceName).toBe('default'); expect(pointer.status).toBe('running'); expect(pointer.iterations).toBe(0); expect(pointer.startTime).toBeDefined(); @@ -84,7 +84,7 @@ describe('updateLatestPointer', () => { const log1 = createSessionLog('first task', projectDir, 'wf1'); updateLatestPointer(log1, 'sid-first', projectDir); - // Simulate a second workflow starting + // Simulate a second piece starting const log2 = createSessionLog('second task', projectDir, 'wf2'); updateLatestPointer(log2, 'sid-second', projectDir, { copyToPrevious: true }); @@ -102,11 +102,11 @@ describe('updateLatestPointer', () => { }); it('should not update previous.json on step-complete calls (no copyToPrevious)', () => { - // Workflow 1 creates latest + // Piece 1 creates latest const log1 = createSessionLog('first', projectDir, 'wf'); updateLatestPointer(log1, 'sid-1', projectDir); - // Workflow 2 starts → copies latest to previous + // Piece 2 starts → copies latest to previous const log2 = createSessionLog('second', projectDir, 'wf'); updateLatestPointer(log2, 'sid-2', projectDir, { copyToPrevious: true }); @@ -129,7 +129,7 @@ describe('updateLatestPointer', () => { log.iterations = 2; updateLatestPointer(log, 'sid-1', projectDir); - // Simulate workflow completion + // Simulate piece completion log.status = 'completed'; log.iterations = 3; updateLatestPointer(log, 'sid-1', projectDir); @@ -153,7 +153,7 @@ describe('NDJSON log', () => { }); describe('initNdjsonLog', () => { - it('should create a .jsonl file with workflow_start record', () => { + it('should create a .jsonl file with piece_start record', () => { const filepath = initNdjsonLog('sess-001', 'my task', 'default', projectDir); expect(filepath).toContain('sess-001.jsonl'); @@ -164,10 +164,10 @@ describe('NDJSON log', () => { expect(lines).toHaveLength(1); const record = JSON.parse(lines[0]!) as NdjsonRecord; - expect(record.type).toBe('workflow_start'); - if (record.type === 'workflow_start') { + expect(record.type).toBe('piece_start'); + if (record.type === 'piece_start') { expect(record.task).toBe('my task'); - expect(record.workflowName).toBe('default'); + expect(record.pieceName).toBe('default'); expect(record.startTime).toBeDefined(); } }); @@ -199,10 +199,10 @@ describe('NDJSON log', () => { const content = readFileSync(filepath, 'utf-8'); const lines = content.trim().split('\n'); - expect(lines).toHaveLength(3); // workflow_start + step_start + step_complete + expect(lines).toHaveLength(3); // piece_start + step_start + step_complete const parsed0 = JSON.parse(lines[0]!) as NdjsonRecord; - expect(parsed0.type).toBe('workflow_start'); + expect(parsed0.type).toBe('piece_start'); const parsed1 = JSON.parse(lines[1]!) as NdjsonRecord; expect(parsed1.type).toBe('step_start'); @@ -247,8 +247,8 @@ describe('NDJSON log', () => { }; appendNdjsonLine(filepath, stepComplete); - const complete: NdjsonWorkflowComplete = { - type: 'workflow_complete', + const complete: NdjsonPieceComplete = { + type: 'piece_complete', iterations: 1, endTime: '2025-01-01T00:00:03.000Z', }; @@ -257,7 +257,7 @@ describe('NDJSON log', () => { const log = loadNdjsonLog(filepath); expect(log).not.toBeNull(); expect(log!.task).toBe('build app'); - expect(log!.workflowName).toBe('default'); + expect(log!.pieceName).toBe('default'); expect(log!.status).toBe('completed'); expect(log!.iterations).toBe(1); expect(log!.endTime).toBe('2025-01-01T00:00:03.000Z'); @@ -268,7 +268,7 @@ describe('NDJSON log', () => { expect(log!.history[0]!.matchedRuleMethod).toBe('phase3_tag'); }); - it('should handle aborted workflow', () => { + it('should handle aborted piece', () => { const filepath = initNdjsonLog('sess-004', 'failing task', 'wf', projectDir); appendNdjsonLine(filepath, { @@ -290,8 +290,8 @@ describe('NDJSON log', () => { timestamp: '2025-01-01T00:00:02.000Z', } satisfies NdjsonStepComplete); - const abort: NdjsonWorkflowAbort = { - type: 'workflow_abort', + const abort: NdjsonPieceAbort = { + type: 'piece_abort', iterations: 1, reason: 'Max iterations reached', endTime: '2025-01-01T00:00:03.000Z', @@ -342,7 +342,7 @@ describe('NDJSON log', () => { } satisfies NdjsonStepComplete); appendNdjsonLine(filepath, { - type: 'workflow_complete', + type: 'piece_complete', iterations: 1, endTime: '2025-01-01T00:00:03.000Z', }); @@ -370,7 +370,7 @@ describe('NDJSON log', () => { } satisfies NdjsonStepComplete); appendNdjsonLine(filepath, { - type: 'workflow_complete', + type: 'piece_complete', iterations: 1, endTime: '2025-01-01T00:00:03.000Z', }); @@ -389,7 +389,7 @@ describe('NDJSON log', () => { const legacyLog: SessionLog = { task: 'legacy task', projectDir, - workflowName: 'wf', + pieceName: 'wf', iterations: 0, startTime: new Date().toISOString(), status: 'running', @@ -422,8 +422,8 @@ describe('NDJSON log', () => { const after2 = readFileSync(filepath, 'utf-8').trim().split('\n'); expect(after2).toHaveLength(2); - // First line should still be workflow_start - expect(JSON.parse(after2[0]!).type).toBe('workflow_start'); + // First line should still be piece_start + expect(JSON.parse(after2[0]!).type).toBe('piece_start'); }); it('should produce valid JSON on each line', () => { @@ -466,7 +466,7 @@ describe('NDJSON log', () => { const content = readFileSync(filepath, 'utf-8'); const lines = content.trim().split('\n'); - expect(lines).toHaveLength(2); // workflow_start + phase_start + expect(lines).toHaveLength(2); // piece_start + phase_start const parsed = JSON.parse(lines[1]!) as NdjsonRecord; expect(parsed.type).toBe('phase_start'); diff --git a/src/__tests__/summarize.test.ts b/src/__tests__/summarize.test.ts index 52940c0..95367f5 100644 --- a/src/__tests__/summarize.test.ts +++ b/src/__tests__/summarize.test.ts @@ -10,7 +10,7 @@ vi.mock('../infra/providers/index.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn(), - getBuiltinWorkflowsEnabled: vi.fn().mockReturnValue(true), + getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true), })); vi.mock('../shared/utils/index.js', async (importOriginal) => ({ @@ -41,7 +41,7 @@ beforeEach(() => { mockLoadGlobalConfig.mockReturnValue({ language: 'ja', trustedDirectories: [], - defaultWorkflow: 'default', + defaultPiece: 'default', logLevel: 'info', provider: 'claude', model: 'haiku', @@ -168,7 +168,7 @@ describe('summarizeTaskName', () => { mockLoadGlobalConfig.mockReturnValue({ language: 'ja', trustedDirectories: [], - defaultWorkflow: 'default', + defaultPiece: 'default', logLevel: 'info', provider: 'codex', model: 'gpt-4', diff --git a/src/__tests__/taskExecution.test.ts b/src/__tests__/taskExecution.test.ts index bf3bd5b..735cb38 100644 --- a/src/__tests__/taskExecution.test.ts +++ b/src/__tests__/taskExecution.test.ts @@ -6,8 +6,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock dependencies before importing the module under test vi.mock('../infra/config/index.js', () => ({ - loadWorkflowByIdentifier: vi.fn(), - isWorkflowPath: vi.fn(() => false), + loadPieceByIdentifier: vi.fn(), + isPiecePath: vi.fn(() => false), loadGlobalConfig: vi.fn(() => ({})), })); @@ -51,8 +51,8 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ getErrorMessage: vi.fn((e) => e.message), })); -vi.mock('../features/tasks/execute/workflowExecution.js', () => ({ - executeWorkflow: vi.fn(), +vi.mock('../features/tasks/execute/pieceExecution.js', () => ({ + executePiece: vi.fn(), })); vi.mock('../shared/context.js', () => ({ @@ -60,7 +60,7 @@ vi.mock('../shared/context.js', () => ({ })); vi.mock('../shared/constants.js', () => ({ - DEFAULT_WORKFLOW_NAME: 'default', + DEFAULT_PIECE_NAME: 'default', DEFAULT_LANGUAGE: 'en', })); @@ -93,7 +93,7 @@ describe('resolveTaskExecution', () => { // Then expect(result).toEqual({ execCwd: '/project', - execWorkflow: 'default', + execPiece: 'default', isWorktree: false, }); expect(mockSummarizeTaskName).not.toHaveBeenCalled(); @@ -149,7 +149,7 @@ describe('resolveTaskExecution', () => { }); expect(result).toEqual({ execCwd: '/project/../20260128T0504-add-auth', - execWorkflow: 'default', + execPiece: 'default', isWorktree: true, branch: 'takt/20260128T0504-add-auth', }); @@ -205,15 +205,15 @@ describe('resolveTaskExecution', () => { expect(mockSummarizeTaskName).toHaveBeenCalledWith('New feature implementation details', { cwd: '/project' }); }); - it('should use workflow override from task data', async () => { - // Given: Task with workflow override + it('should use piece override from task data', async () => { + // Given: Task with piece override const task: TaskInfo = { - name: 'task-with-workflow', + name: 'task-with-piece', content: 'Task content', filePath: '/tasks/task.yaml', data: { task: 'Task content', - workflow: 'custom-workflow', + piece: 'custom-piece', }, }; @@ -221,7 +221,7 @@ describe('resolveTaskExecution', () => { const result = await resolveTaskExecution(task, '/project', 'default'); // Then - expect(result.execWorkflow).toBe('custom-workflow'); + expect(result.execPiece).toBe('custom-piece'); }); it('should pass branch option to createSharedClone when specified', async () => { diff --git a/src/__tests__/transitions.test.ts b/src/__tests__/transitions.test.ts index 6503c82..f1fd351 100644 --- a/src/__tests__/transitions.test.ts +++ b/src/__tests__/transitions.test.ts @@ -1,12 +1,12 @@ /** - * Tests for workflow transitions module (movement-based) + * Tests for piece transitions module (movement-based) */ import { describe, it, expect } from 'vitest'; -import { determineNextMovementByRules } from '../core/workflow/index.js'; -import type { WorkflowMovement } from '../core/models/index.js'; +import { determineNextMovementByRules } from '../core/piece/index.js'; +import type { PieceMovement } from '../core/models/index.js'; -function createMovementWithRules(rules: { condition: string; next: string }[]): WorkflowMovement { +function createMovementWithRules(rules: { condition: string; next: string }[]): PieceMovement { return { name: 'test-step', agent: 'test-agent', @@ -42,7 +42,7 @@ describe('determineNextMovementByRules', () => { }); it('should return null when movement has no rules', () => { - const step: WorkflowMovement = { + const step: PieceMovement = { name: 'test-step', agent: 'test-agent', agentDisplayName: 'Test Agent', @@ -63,7 +63,7 @@ describe('determineNextMovementByRules', () => { it('should return null when rule exists but next is undefined', () => { // Parallel sub-movement rules may omit `next` (optional field) - const step: WorkflowMovement = { + const step: PieceMovement = { name: 'sub-step', agent: 'test-agent', agentDisplayName: 'Test Agent', diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 65080b9..16d2f80 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -65,7 +65,7 @@ describe('createSessionLog', () => { expect(log.task).toBe('test task'); expect(log.projectDir).toBe('/project'); - expect(log.workflowName).toBe('default'); + expect(log.pieceName).toBe('default'); expect(log.iterations).toBe(0); expect(log.status).toBe('running'); expect(log.history).toEqual([]); diff --git a/src/__tests__/workflow-categories.test.ts b/src/__tests__/workflow-categories.test.ts deleted file mode 100644 index 1634cdf..0000000 --- a/src/__tests__/workflow-categories.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Tests for workflow category (subdirectory) support — Issue #85 - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { - listWorkflows, - listWorkflowEntries, - loadAllWorkflows, - loadWorkflow, -} from '../infra/config/loaders/workflowLoader.js'; -import type { WorkflowDirEntry } from '../infra/config/loaders/workflowLoader.js'; -import { - buildWorkflowSelectionItems, - buildTopLevelSelectOptions, - parseCategorySelection, - buildCategoryWorkflowOptions, - type WorkflowSelectionItem, -} from '../features/workflowSelection/index.js'; - -const SAMPLE_WORKFLOW = `name: test-workflow -description: Test workflow -initial_movement: step1 -max_iterations: 1 - -movements: - - name: step1 - agent: coder - instruction: "{task}" -`; - -function createWorkflow(dir: string, name: string, content?: string): void { - writeFileSync(join(dir, `${name}.yaml`), content ?? SAMPLE_WORKFLOW); -} - -describe('workflow categories - directory scanning', () => { - let tempDir: string; - let workflowsDir: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); - workflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); - }); - - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - it('should discover root-level workflows', () => { - createWorkflow(workflowsDir, 'simple'); - createWorkflow(workflowsDir, 'advanced'); - - const workflows = listWorkflows(tempDir); - expect(workflows).toContain('simple'); - expect(workflows).toContain('advanced'); - }); - - it('should discover workflows in subdirectories with category prefix', () => { - const frontendDir = join(workflowsDir, 'frontend'); - mkdirSync(frontendDir); - createWorkflow(frontendDir, 'react'); - createWorkflow(frontendDir, 'vue'); - - const workflows = listWorkflows(tempDir); - expect(workflows).toContain('frontend/react'); - expect(workflows).toContain('frontend/vue'); - }); - - it('should discover both root-level and categorized workflows', () => { - createWorkflow(workflowsDir, 'simple'); - - const frontendDir = join(workflowsDir, 'frontend'); - mkdirSync(frontendDir); - createWorkflow(frontendDir, 'react'); - - const backendDir = join(workflowsDir, 'backend'); - mkdirSync(backendDir); - createWorkflow(backendDir, 'api'); - - const workflows = listWorkflows(tempDir); - expect(workflows).toContain('simple'); - expect(workflows).toContain('frontend/react'); - expect(workflows).toContain('backend/api'); - }); - - it('should not scan deeper than 1 level', () => { - const deepDir = join(workflowsDir, 'category', 'subcategory'); - mkdirSync(deepDir, { recursive: true }); - createWorkflow(deepDir, 'deep'); - - const workflows = listWorkflows(tempDir); - // category/subcategory should be treated as a directory entry, not scanned further - expect(workflows).not.toContain('category/subcategory/deep'); - // Only 1-level: category/deep would not exist since deep.yaml is in subcategory - expect(workflows).not.toContain('deep'); - }); -}); - -describe('workflow categories - listWorkflowEntries', () => { - let tempDir: string; - let workflowsDir: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); - workflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); - }); - - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - it('should return entries with category information', () => { - createWorkflow(workflowsDir, 'simple'); - - const frontendDir = join(workflowsDir, 'frontend'); - mkdirSync(frontendDir); - createWorkflow(frontendDir, 'react'); - - const entries = listWorkflowEntries(tempDir); - const simpleEntry = entries.find((e) => e.name === 'simple'); - const reactEntry = entries.find((e) => e.name === 'frontend/react'); - - expect(simpleEntry).toBeDefined(); - expect(simpleEntry!.category).toBeUndefined(); - expect(simpleEntry!.source).toBe('project'); - - expect(reactEntry).toBeDefined(); - expect(reactEntry!.category).toBe('frontend'); - expect(reactEntry!.source).toBe('project'); - }); -}); - -describe('workflow categories - loadAllWorkflows', () => { - let tempDir: string; - let workflowsDir: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); - workflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); - }); - - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - it('should load categorized workflows with qualified names as keys', () => { - const frontendDir = join(workflowsDir, 'frontend'); - mkdirSync(frontendDir); - createWorkflow(frontendDir, 'react'); - - const workflows = loadAllWorkflows(tempDir); - expect(workflows.has('frontend/react')).toBe(true); - }); -}); - -describe('workflow categories - loadWorkflow', () => { - let tempDir: string; - let workflowsDir: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'takt-cat-test-')); - workflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(workflowsDir, { recursive: true }); - }); - - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - it('should load workflow by category/name identifier', () => { - const frontendDir = join(workflowsDir, 'frontend'); - mkdirSync(frontendDir); - createWorkflow(frontendDir, 'react'); - - const workflow = loadWorkflow('frontend/react', tempDir); - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('test-workflow'); - }); - - it('should return null for non-existent category/name', () => { - const workflow = loadWorkflow('nonexistent/workflow', tempDir); - expect(workflow).toBeNull(); - }); - - it('should support .yml extension in subdirectories', () => { - const backendDir = join(workflowsDir, 'backend'); - mkdirSync(backendDir); - writeFileSync(join(backendDir, 'api.yml'), SAMPLE_WORKFLOW); - - const workflow = loadWorkflow('backend/api', tempDir); - expect(workflow).not.toBeNull(); - }); -}); - -describe('buildWorkflowSelectionItems', () => { - it('should separate root workflows and categories', () => { - const entries: WorkflowDirEntry[] = [ - { name: 'simple', path: '/tmp/simple.yaml', source: 'project' }, - { name: 'frontend/react', path: '/tmp/frontend/react.yaml', category: 'frontend', source: 'project' }, - { name: 'frontend/vue', path: '/tmp/frontend/vue.yaml', category: 'frontend', source: 'project' }, - { name: 'backend/api', path: '/tmp/backend/api.yaml', category: 'backend', source: 'project' }, - ]; - - const items = buildWorkflowSelectionItems(entries); - - const workflows = items.filter((i) => i.type === 'workflow'); - const categories = items.filter((i) => i.type === 'category'); - - expect(workflows).toHaveLength(1); - expect(workflows[0]!.name).toBe('simple'); - - expect(categories).toHaveLength(2); - const frontend = categories.find((c) => c.name === 'frontend'); - expect(frontend).toBeDefined(); - expect(frontend!.type === 'category' && frontend!.workflows).toEqual(['frontend/react', 'frontend/vue']); - - const backend = categories.find((c) => c.name === 'backend'); - expect(backend).toBeDefined(); - expect(backend!.type === 'category' && backend!.workflows).toEqual(['backend/api']); - }); - - it('should sort items alphabetically', () => { - const entries: WorkflowDirEntry[] = [ - { name: 'zebra', path: '/tmp/zebra.yaml', source: 'project' }, - { name: 'alpha', path: '/tmp/alpha.yaml', source: 'project' }, - { name: 'misc/playground', path: '/tmp/misc/playground.yaml', category: 'misc', source: 'project' }, - ]; - - const items = buildWorkflowSelectionItems(entries); - const names = items.map((i) => i.name); - expect(names).toEqual(['alpha', 'misc', 'zebra']); - }); - - it('should return empty array for empty input', () => { - const items = buildWorkflowSelectionItems([]); - expect(items).toEqual([]); - }); -}); - -describe('2-stage category selection helpers', () => { - const items: WorkflowSelectionItem[] = [ - { type: 'workflow', name: 'simple' }, - { type: 'category', name: 'frontend', workflows: ['frontend/react', 'frontend/vue'] }, - { type: 'category', name: 'backend', workflows: ['backend/api'] }, - ]; - - describe('buildTopLevelSelectOptions', () => { - it('should encode categories with prefix in value', () => { - const options = buildTopLevelSelectOptions(items, ''); - const categoryOption = options.find((o) => o.label.includes('frontend')); - expect(categoryOption).toBeDefined(); - expect(categoryOption!.value).toBe('__category__:frontend'); - }); - - it('should mark current workflow', () => { - const options = buildTopLevelSelectOptions(items, 'simple'); - const simpleOption = options.find((o) => o.value === 'simple'); - expect(simpleOption!.label).toContain('(current)'); - }); - - it('should mark category containing current workflow', () => { - const options = buildTopLevelSelectOptions(items, 'frontend/react'); - const frontendOption = options.find((o) => o.value === '__category__:frontend'); - expect(frontendOption!.label).toContain('(current)'); - }); - }); - - describe('parseCategorySelection', () => { - it('should return category name for category selection', () => { - expect(parseCategorySelection('__category__:frontend')).toBe('frontend'); - }); - - it('should return null for direct workflow selection', () => { - expect(parseCategorySelection('simple')).toBeNull(); - }); - }); - - describe('buildCategoryWorkflowOptions', () => { - it('should return options for workflows in a category', () => { - const options = buildCategoryWorkflowOptions(items, 'frontend', ''); - expect(options).not.toBeNull(); - expect(options).toHaveLength(2); - expect(options![0]!.value).toBe('frontend/react'); - expect(options![0]!.label).toBe('react'); - }); - - it('should mark current workflow in category', () => { - const options = buildCategoryWorkflowOptions(items, 'frontend', 'frontend/vue'); - const vueOption = options!.find((o) => o.value === 'frontend/vue'); - expect(vueOption!.label).toContain('(current)'); - }); - - it('should return null for non-existent category', () => { - expect(buildCategoryWorkflowOptions(items, 'nonexistent', '')).toBeNull(); - }); - }); -}); diff --git a/src/__tests__/workflowLoader.test.ts b/src/__tests__/workflowLoader.test.ts deleted file mode 100644 index 1991d35..0000000 --- a/src/__tests__/workflowLoader.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Tests for isWorkflowPath and loadWorkflowByIdentifier - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { - isWorkflowPath, - loadWorkflowByIdentifier, - listWorkflows, - loadAllWorkflows, -} from '../infra/config/loaders/workflowLoader.js'; - -const SAMPLE_WORKFLOW = `name: test-workflow -description: Test workflow -initial_movement: step1 -max_iterations: 1 - -movements: - - name: step1 - agent: coder - instruction: "{task}" -`; - -describe('isWorkflowPath', () => { - it('should return true for absolute paths', () => { - expect(isWorkflowPath('/path/to/workflow.yaml')).toBe(true); - expect(isWorkflowPath('/workflow')).toBe(true); - }); - - it('should return true for home directory paths', () => { - expect(isWorkflowPath('~/workflow.yaml')).toBe(true); - expect(isWorkflowPath('~/.takt/pieces/custom.yaml')).toBe(true); - }); - - it('should return true for relative paths starting with ./', () => { - expect(isWorkflowPath('./workflow.yaml')).toBe(true); - expect(isWorkflowPath('./subdir/workflow.yaml')).toBe(true); - }); - - it('should return true for relative paths starting with ../', () => { - expect(isWorkflowPath('../workflow.yaml')).toBe(true); - expect(isWorkflowPath('../subdir/workflow.yaml')).toBe(true); - }); - - it('should return true for paths ending with .yaml', () => { - expect(isWorkflowPath('custom.yaml')).toBe(true); - expect(isWorkflowPath('my-workflow.yaml')).toBe(true); - }); - - it('should return true for paths ending with .yml', () => { - expect(isWorkflowPath('custom.yml')).toBe(true); - expect(isWorkflowPath('my-workflow.yml')).toBe(true); - }); - - it('should return false for plain workflow names', () => { - expect(isWorkflowPath('default')).toBe(false); - expect(isWorkflowPath('simple')).toBe(false); - expect(isWorkflowPath('magi')).toBe(false); - expect(isWorkflowPath('my-custom-workflow')).toBe(false); - }); -}); - -describe('loadWorkflowByIdentifier', () => { - let tempDir: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'takt-test-')); - }); - - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - it('should load workflow by name (builtin)', () => { - const workflow = loadWorkflowByIdentifier('default', process.cwd()); - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('default'); - }); - - it('should load workflow by absolute path', () => { - const filePath = join(tempDir, 'test.yaml'); - writeFileSync(filePath, SAMPLE_WORKFLOW); - - const workflow = loadWorkflowByIdentifier(filePath, tempDir); - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('test-workflow'); - }); - - it('should load workflow by relative path', () => { - const filePath = join(tempDir, 'test.yaml'); - writeFileSync(filePath, SAMPLE_WORKFLOW); - - const workflow = loadWorkflowByIdentifier('./test.yaml', tempDir); - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('test-workflow'); - }); - - it('should load workflow by filename with .yaml extension', () => { - const filePath = join(tempDir, 'test.yaml'); - writeFileSync(filePath, SAMPLE_WORKFLOW); - - const workflow = loadWorkflowByIdentifier('test.yaml', tempDir); - expect(workflow).not.toBeNull(); - expect(workflow!.name).toBe('test-workflow'); - }); - - it('should return null for non-existent name', () => { - const workflow = loadWorkflowByIdentifier('non-existent-workflow-xyz', process.cwd()); - expect(workflow).toBeNull(); - }); - - it('should return null for non-existent path', () => { - const workflow = loadWorkflowByIdentifier('./non-existent.yaml', tempDir); - expect(workflow).toBeNull(); - }); -}); - -describe('listWorkflows with project-local', () => { - let tempDir: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'takt-test-')); - }); - - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - it('should include project-local workflows when cwd is provided', () => { - const projectWorkflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(projectWorkflowsDir, { recursive: true }); - writeFileSync(join(projectWorkflowsDir, 'project-custom.yaml'), SAMPLE_WORKFLOW); - - const workflows = listWorkflows(tempDir); - expect(workflows).toContain('project-custom'); - }); - - it('should include builtin workflows regardless of cwd', () => { - const workflows = listWorkflows(tempDir); - expect(workflows).toContain('default'); - }); - -}); - -describe('loadAllWorkflows with project-local', () => { - let tempDir: string; - - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'takt-test-')); - }); - - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); - }); - - it('should include project-local workflows when cwd is provided', () => { - const projectWorkflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(projectWorkflowsDir, { recursive: true }); - writeFileSync(join(projectWorkflowsDir, 'project-custom.yaml'), SAMPLE_WORKFLOW); - - const workflows = loadAllWorkflows(tempDir); - expect(workflows.has('project-custom')).toBe(true); - expect(workflows.get('project-custom')!.name).toBe('test-workflow'); - }); - - it('should have project-local override builtin when same name', () => { - const projectWorkflowsDir = join(tempDir, '.takt', 'workflows'); - mkdirSync(projectWorkflowsDir, { recursive: true }); - - const overrideWorkflow = `name: project-override -description: Project override -initial_movement: step1 -max_iterations: 1 - -movements: - - name: step1 - agent: coder - instruction: "{task}" -`; - writeFileSync(join(projectWorkflowsDir, 'default.yaml'), overrideWorkflow); - - const workflows = loadAllWorkflows(tempDir); - expect(workflows.get('default')!.name).toBe('project-override'); - }); - -}); diff --git a/src/agents/types.ts b/src/agents/types.ts index 62c2c12..8bf7933 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -19,7 +19,7 @@ export interface RunAgentOptions { allowedTools?: string[]; /** Maximum number of agentic turns */ maxTurns?: number; - /** Permission mode for tool execution (from workflow step) */ + /** Permission mode for tool execution (from piece step) */ permissionMode?: PermissionMode; onStream?: StreamCallback; onPermissionRequest?: PermissionHandler; diff --git a/src/app/cli/commands.ts b/src/app/cli/commands.ts index a16b9af..8ac7951 100644 --- a/src/app/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -4,10 +4,10 @@ * Registers all named subcommands (run, watch, add, list, switch, clear, eject, config, prompt). */ -import { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/index.js'; +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 { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/index.js'; +import { switchPiece, switchConfig, ejectBuiltin } from '../../features/config/index.js'; import { previewPrompts } from '../../features/prompt/index.js'; import { program, resolvedCwd } from './program.js'; import { resolveAgentOverrides } from './helpers.js'; @@ -16,8 +16,8 @@ program .command('run') .description('Run all pending tasks from .takt/tasks/') .action(async () => { - const workflow = getCurrentWorkflow(resolvedCwd); - await runAllTasks(resolvedCwd, workflow, resolveAgentOverrides(program)); + const piece = getCurrentPiece(resolvedCwd); + await runAllTasks(resolvedCwd, piece, resolveAgentOverrides(program)); }); program @@ -44,10 +44,10 @@ program program .command('switch') - .description('Switch workflow interactively') - .argument('[workflow]', 'Workflow name') - .action(async (workflow?: string) => { - await switchWorkflow(resolvedCwd, workflow); + .description('Switch piece interactively') + .argument('[piece]', 'Piece name') + .action(async (piece?: string) => { + await switchPiece(resolvedCwd, piece); }); program @@ -60,7 +60,7 @@ program program .command('eject') - .description('Copy builtin workflow/agents to ~/.takt/ for customization') + .description('Copy builtin piece/agents to ~/.takt/ for customization') .argument('[name]', 'Specific builtin to eject') .action(async (name?: string) => { await ejectBuiltin(name); @@ -77,7 +77,7 @@ program program .command('prompt') .description('Preview assembled prompts for each movement and phase') - .argument('[workflow]', 'Workflow name or path (defaults to current)') - .action(async (workflow?: string) => { - await previewPrompts(resolvedCwd, workflow); + .argument('[piece]', 'Piece name or path (defaults to current)') + .action(async (piece?: string) => { + await previewPrompts(resolvedCwd, piece); }); diff --git a/src/app/cli/program.ts b/src/app/cli/program.ts index 1821324..aedf1b8 100644 --- a/src/app/cli/program.ts +++ b/src/app/cli/program.ts @@ -42,7 +42,7 @@ program // --- Global options --- program .option('-i, --issue ', 'GitHub issue number (equivalent to #N)', (val: string) => parseInt(val, 10)) - .option('-w, --workflow ', 'Workflow name or path to workflow file') + .option('-w, --piece ', 'Piece name or path to piece file') .option('-b, --branch ', 'Branch name (auto-generated if omitted)') .option('--auto-pr', 'Create PR after successful execution') .option('--repo ', 'Repository (defaults to current)') diff --git a/src/app/cli/routing.ts b/src/app/cli/routing.ts index 957fe16..146ced3 100644 --- a/src/app/cli/routing.ts +++ b/src/app/cli/routing.ts @@ -8,11 +8,11 @@ import { info, error } from '../../shared/ui/index.js'; import { getErrorMessage } from '../../shared/utils/index.js'; import { resolveIssueTask, isIssueReference } from '../../infra/github/index.js'; -import { selectAndExecuteTask, determineWorkflow, type SelectAndExecuteOptions } from '../../features/tasks/index.js'; +import { selectAndExecuteTask, determinePiece, type SelectAndExecuteOptions } from '../../features/tasks/index.js'; import { executePipeline } from '../../features/pipeline/index.js'; import { interactiveMode } from '../../features/interactive/index.js'; -import { getWorkflowDescription } from '../../infra/config/index.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../shared/constants.js'; +import { getPieceDescription } from '../../infra/config/index.js'; +import { DEFAULT_PIECE_NAME } from '../../shared/constants.js'; import { program, resolvedCwd, pipelineMode } from './program.js'; import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js'; @@ -25,7 +25,7 @@ program const selectOptions: SelectAndExecuteOptions = { autoPr: opts.autoPr === true, repo: opts.repo as string | undefined, - workflow: opts.workflow as string | undefined, + piece: opts.piece as string | undefined, createWorktree: createWorktreeOverride, }; @@ -34,7 +34,7 @@ program const exitCode = await executePipeline({ issueNumber: opts.issue as number | undefined, task: opts.task as string | undefined, - workflow: (opts.workflow as string | undefined) ?? DEFAULT_WORKFLOW_NAME, + piece: (opts.piece as string | undefined) ?? DEFAULT_PIECE_NAME, branch: opts.branch as string | undefined, autoPr: opts.autoPr === true, repo: opts.repo as string | undefined, @@ -89,21 +89,21 @@ program } // Short single word or no task → interactive mode (with optional initial input) - const workflowId = await determineWorkflow(resolvedCwd, selectOptions.workflow); - if (workflowId === null) { + const pieceId = await determinePiece(resolvedCwd, selectOptions.piece); + if (pieceId === null) { info('Cancelled'); return; } - const workflowContext = getWorkflowDescription(workflowId, resolvedCwd); - const result = await interactiveMode(resolvedCwd, task, workflowContext); + const pieceContext = getPieceDescription(pieceId, resolvedCwd); + const result = await interactiveMode(resolvedCwd, task, pieceContext); if (!result.confirmed) { return; } selectOptions.interactiveUserInput = true; - selectOptions.workflow = workflowId; + selectOptions.piece = pieceId; selectOptions.interactiveMetadata = { confirmed: result.confirmed, task: result.task }; await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides); }); diff --git a/src/core/models/config.ts b/src/core/models/config.ts index 479c91b..e9f9373 100644 --- a/src/core/models/config.ts +++ b/src/core/models/config.ts @@ -7,9 +7,9 @@ export type TaktConfig = z.infer; export const DEFAULT_CONFIG: TaktConfig = { defaultModel: 'sonnet', - defaultWorkflow: 'default', + defaultPiece: 'default', agentDirs: [], - workflowDirs: [], + pieceDirs: [], claude: { command: 'claude', timeout: 300000, diff --git a/src/core/models/global-config.ts b/src/core/models/global-config.ts index 6435ce6..1e4a20b 100644 --- a/src/core/models/global-config.ts +++ b/src/core/models/global-config.ts @@ -37,17 +37,17 @@ export interface PipelineConfig { export interface GlobalConfig { language: Language; trustedDirectories: string[]; - defaultWorkflow: string; + defaultPiece: string; logLevel: 'debug' | 'info' | 'warn' | 'error'; provider?: 'claude' | 'codex' | 'mock'; model?: string; debug?: DebugConfig; /** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */ worktreeDir?: string; - /** List of builtin workflow/agent names to exclude from fallback loading */ + /** List of builtin piece/agent names to exclude from fallback loading */ disabledBuiltins?: string[]; - /** Enable builtin workflows from resources/global/{lang}/workflows */ - enableBuiltinWorkflows?: boolean; + /** Enable builtin pieces from resources/global/{lang}/pieces */ + enableBuiltinPieces?: boolean; /** Anthropic API key for Claude Code SDK (overridden by TAKT_ANTHROPIC_API_KEY env var) */ anthropicApiKey?: string; /** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */ @@ -58,13 +58,13 @@ export interface GlobalConfig { minimalOutput?: boolean; /** Path to bookmarks file (default: ~/.takt/preferences/bookmarks.yaml) */ bookmarksFile?: string; - /** Path to workflow categories file (default: ~/.takt/preferences/workflow-categories.yaml) */ - workflowCategoriesFile?: string; + /** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */ + pieceCategoriesFile?: string; } /** Project-level configuration */ export interface ProjectConfig { - workflow?: string; + piece?: string; agents?: CustomAgentConfig[]; provider?: 'claude' | 'codex' | 'mock'; } diff --git a/src/core/models/index.ts b/src/core/models/index.ts index 8f92e8e..263d888 100644 --- a/src/core/models/index.ts +++ b/src/core/models/index.ts @@ -8,11 +8,11 @@ export type { ReportObjectConfig, AgentResponse, SessionState, - WorkflowRule, - WorkflowMovement, + PieceRule, + PieceMovement, LoopDetectionConfig, - WorkflowConfig, - WorkflowState, + PieceConfig, + PieceState, CustomAgentConfig, DebugConfig, Language, diff --git a/src/core/models/workflow-types.ts b/src/core/models/piece-types.ts similarity index 86% rename from src/core/models/workflow-types.ts rename to src/core/models/piece-types.ts index b033044..c62b575 100644 --- a/src/core/models/workflow-types.ts +++ b/src/core/models/piece-types.ts @@ -1,12 +1,12 @@ /** - * Workflow configuration and runtime state types + * Piece configuration and runtime state types */ import type { PermissionMode } from './status.js'; import type { AgentResponse } from './response.js'; /** Rule-based transition configuration (unified format) */ -export interface WorkflowRule { +export interface PieceRule { /** Human-readable condition text */ condition: string; /** Next movement name (e.g., implement, COMPLETE, ABORT). Optional for parallel sub-movements. */ @@ -32,7 +32,7 @@ export interface WorkflowRule { aggregateConditionText?: string | string[]; } -/** Report file configuration for a workflow movement (label: path pair) */ +/** Report file configuration for a piece movement (label: path pair) */ export interface ReportConfig { /** Display label (e.g., "Scope", "Decisions") */ label: string; @@ -50,12 +50,12 @@ export interface ReportObjectConfig { format?: string; } -/** Single movement in a workflow */ -export interface WorkflowMovement { +/** Single movement in a piece */ +export interface PieceMovement { name: string; - /** Brief description of this movement's role in the workflow */ + /** Brief description of this movement's role in the piece */ description?: string; - /** Agent name, path, or inline prompt as specified in workflow YAML. Undefined when movement runs without an agent. */ + /** Agent name, path, or inline prompt as specified in piece YAML. Undefined when movement runs without an agent. */ agent?: string; /** Session handling for this movement */ session?: 'continue' | 'refresh'; @@ -75,12 +75,12 @@ export interface WorkflowMovement { edit?: boolean; instructionTemplate: string; /** Rules for movement routing */ - rules?: WorkflowRule[]; + rules?: PieceRule[]; /** Report file configuration. Single string, array of label:path, or object with order/format. */ report?: string | ReportConfig[] | ReportObjectConfig; passPreviousResponse: boolean; /** Sub-movements to execute in parallel. When set, this movement runs all sub-movements concurrently. */ - parallel?: WorkflowMovement[]; + parallel?: PieceMovement[]; } /** Loop detection configuration */ @@ -91,11 +91,11 @@ export interface LoopDetectionConfig { action?: 'abort' | 'warn' | 'ignore'; } -/** Workflow configuration */ -export interface WorkflowConfig { +/** Piece configuration */ +export interface PieceConfig { name: string; description?: string; - movements: WorkflowMovement[]; + movements: PieceMovement[]; initialMovement: string; maxIterations: number; /** Loop detection settings */ @@ -108,9 +108,9 @@ export interface WorkflowConfig { answerAgent?: string; } -/** Runtime state of a workflow execution */ -export interface WorkflowState { - workflowName: string; +/** Runtime state of a piece execution */ +export interface PieceState { + pieceName: string; currentMovement: string; iteration: number; movementOutputs: Map; diff --git a/src/core/models/schemas.ts b/src/core/models/schemas.ts index 97a4204..cd48f74 100644 --- a/src/core/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -29,9 +29,9 @@ export const ClaudeConfigSchema = z.object({ /** TAKT global tool configuration schema */ export const TaktConfigSchema = z.object({ defaultModel: AgentModelSchema, - defaultWorkflow: z.string().default('default'), + defaultPiece: z.string().default('default'), agentDirs: z.array(z.string()).default([]), - workflowDirs: z.array(z.string()).default([]), + pieceDirs: z.array(z.string()).default([]), sessionDir: z.string().optional(), claude: ClaudeConfigSchema.default({ command: 'claude', timeout: 300000 }), }); @@ -100,7 +100,7 @@ export const ReportFieldSchema = z.union([ ]); /** Rule-based transition schema (new unified format) */ -export const WorkflowRuleSchema = z.object({ +export const PieceRuleSchema = z.object({ /** Human-readable condition text */ condition: z.string().min(1), /** Next movement name (e.g., implement, COMPLETE, ABORT). Optional for parallel sub-movements (parent handles routing). */ @@ -125,13 +125,13 @@ export const ParallelSubMovementRawSchema = z.object({ edit: z.boolean().optional(), instruction: z.string().optional(), instruction_template: z.string().optional(), - rules: z.array(WorkflowRuleSchema).optional(), + rules: z.array(PieceRuleSchema).optional(), report: ReportFieldSchema.optional(), pass_previous_response: z.boolean().optional().default(true), }); -/** Workflow movement schema - raw YAML format */ -export const WorkflowMovementRawSchema = z.object({ +/** Piece movement schema - raw YAML format */ +export const PieceMovementRawSchema = z.object({ name: z.string().min(1), description: z.string().optional(), /** Agent is required for normal movements, optional for parallel container movements */ @@ -150,7 +150,7 @@ export const WorkflowMovementRawSchema = z.object({ instruction: z.string().optional(), instruction_template: z.string().optional(), /** Rules for movement routing */ - rules: z.array(WorkflowRuleSchema).optional(), + rules: z.array(PieceRuleSchema).optional(), /** Report file(s) for this movement */ report: ReportFieldSchema.optional(), pass_previous_response: z.boolean().optional().default(true), @@ -158,11 +158,11 @@ export const WorkflowMovementRawSchema = z.object({ parallel: z.array(ParallelSubMovementRawSchema).optional(), }); -/** Workflow configuration schema - raw YAML format */ -export const WorkflowConfigRawSchema = z.object({ +/** Piece configuration schema - raw YAML format */ +export const PieceConfigRawSchema = z.object({ name: z.string().min(1), description: z.string().optional(), - movements: z.array(WorkflowMovementRawSchema).min(1), + movements: z.array(PieceMovementRawSchema).min(1), initial_movement: z.string().optional(), max_iterations: z.number().int().positive().optional().default(10), answer_agent: z.string().optional(), @@ -199,35 +199,35 @@ export const PipelineConfigSchema = z.object({ pr_body_template: z.string().optional(), }); -/** Workflow category config schema (recursive) */ -export type WorkflowCategoryConfigNode = { - workflows?: string[]; - [key: string]: WorkflowCategoryConfigNode | string[] | undefined; +/** Piece category config schema (recursive) */ +export type PieceCategoryConfigNode = { + pieces?: string[]; + [key: string]: PieceCategoryConfigNode | string[] | undefined; }; -export const WorkflowCategoryConfigNodeSchema: z.ZodType = z.lazy(() => +export const PieceCategoryConfigNodeSchema: z.ZodType = z.lazy(() => z.object({ - workflows: z.array(z.string()).optional(), - }).catchall(WorkflowCategoryConfigNodeSchema) + pieces: z.array(z.string()).optional(), + }).catchall(PieceCategoryConfigNodeSchema) ); -export const WorkflowCategoryConfigSchema = z.record(z.string(), WorkflowCategoryConfigNodeSchema); +export const PieceCategoryConfigSchema = z.record(z.string(), PieceCategoryConfigNodeSchema); /** Global config schema */ export const GlobalConfigSchema = z.object({ language: LanguageSchema.optional().default(DEFAULT_LANGUAGE), trusted_directories: z.array(z.string()).optional().default([]), - default_workflow: z.string().optional().default('default'), + default_piece: z.string().optional().default('default'), log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'), provider: z.enum(['claude', 'codex', 'mock']).optional().default('claude'), model: z.string().optional(), debug: DebugConfigSchema.optional(), /** Directory for shared clones (worktree_dir in config). If empty, uses ../{clone-name} relative to project */ worktree_dir: z.string().optional(), - /** List of builtin workflow/agent names to exclude from fallback loading */ + /** List of builtin piece/agent names to exclude from fallback loading */ disabled_builtins: z.array(z.string()).optional().default([]), - /** Enable builtin workflows from resources/global/{lang}/workflows */ - enable_builtin_workflows: z.boolean().optional(), + /** Enable builtin pieces from resources/global/{lang}/pieces */ + enable_builtin_pieces: z.boolean().optional(), /** Anthropic API key for Claude Code SDK (overridden by TAKT_ANTHROPIC_API_KEY env var) */ anthropic_api_key: z.string().optional(), /** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */ @@ -238,13 +238,13 @@ export const GlobalConfigSchema = z.object({ minimal_output: z.boolean().optional().default(false), /** Path to bookmarks file (default: ~/.takt/preferences/bookmarks.yaml) */ bookmarks_file: z.string().optional(), - /** Path to workflow categories file (default: ~/.takt/preferences/workflow-categories.yaml) */ - workflow_categories_file: z.string().optional(), + /** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */ + piece_categories_file: z.string().optional(), }); /** Project config schema */ export const ProjectConfigSchema = z.object({ - workflow: z.string().optional(), + piece: z.string().optional(), agents: z.array(CustomAgentConfigSchema).optional(), provider: z.enum(['claude', 'codex', 'mock']).optional(), }); diff --git a/src/core/models/session.ts b/src/core/models/session.ts index 63e1a6c..5bf2401 100644 --- a/src/core/models/session.ts +++ b/src/core/models/session.ts @@ -6,7 +6,7 @@ import type { AgentResponse } from './response.js'; import type { Status } from './status.js'; /** - * Session state for workflow execution + * Session state for piece execution */ export interface SessionState { task: string; @@ -74,7 +74,7 @@ export interface InteractiveSession { sessionId: string | null; messages: ConversationMessage[]; userApprovedTools: string[]; - currentWorkflow: string; + currentPiece: string; } /** @@ -90,7 +90,7 @@ export function createInteractiveSession( sessionId: null, messages: [], userApprovedTools: [], - currentWorkflow: 'default', + currentPiece: 'default', ...options, }; } diff --git a/src/core/models/status.ts b/src/core/models/status.ts index e43b101..adb0a64 100644 --- a/src/core/models/status.ts +++ b/src/core/models/status.ts @@ -5,7 +5,7 @@ /** Built-in agent types */ export type AgentType = 'coder' | 'architect' | 'supervisor' | 'custom'; -/** Execution status for agents and workflows */ +/** Execution status for agents and pieces */ export type Status = | 'pending' | 'done' diff --git a/src/core/models/types.ts b/src/core/models/types.ts index 2dcd0c4..f0dca5f 100644 --- a/src/core/models/types.ts +++ b/src/core/models/types.ts @@ -23,16 +23,16 @@ export type { SessionState, } from './session.js'; -// Workflow configuration and runtime state +// Piece configuration and runtime state export type { - WorkflowRule, + PieceRule, ReportConfig, ReportObjectConfig, - WorkflowMovement, + PieceMovement, LoopDetectionConfig, - WorkflowConfig, - WorkflowState, -} from './workflow-types.js'; + PieceConfig, + PieceState, +} from './piece-types.js'; // Configuration types (global and project) export type { diff --git a/src/core/workflow/constants.ts b/src/core/piece/constants.ts similarity index 82% rename from src/core/workflow/constants.ts rename to src/core/piece/constants.ts index 3c1f1c8..4874e25 100644 --- a/src/core/workflow/constants.ts +++ b/src/core/piece/constants.ts @@ -1,11 +1,11 @@ /** - * Workflow engine constants + * Piece engine constants * - * Contains all constants used by the workflow engine including + * Contains all constants used by the piece engine including * special movement names, limits, and error messages. */ -/** Special movement names for workflow termination */ +/** Special movement names for piece termination */ export const COMPLETE_MOVEMENT = 'COMPLETE'; export const ABORT_MOVEMENT = 'ABORT'; diff --git a/src/core/workflow/engine/MovementExecutor.ts b/src/core/piece/engine/MovementExecutor.ts similarity index 82% rename from src/core/workflow/engine/MovementExecutor.ts rename to src/core/piece/engine/MovementExecutor.ts index 71310dd..29f731b 100644 --- a/src/core/workflow/engine/MovementExecutor.ts +++ b/src/core/piece/engine/MovementExecutor.ts @@ -1,5 +1,5 @@ /** - * Executes a single workflow movement through the 3-phase model. + * Executes a single piece movement through the 3-phase model. * * Phase 1: Main agent execution (with tools) * Phase 2: Report output (Write-only, optional) @@ -9,8 +9,8 @@ import { existsSync } from 'node:fs'; import { join } from 'node:path'; import type { - WorkflowMovement, - WorkflowState, + PieceMovement, + PieceState, AgentResponse, Language, } from '../../models/types.js'; @@ -32,15 +32,15 @@ export interface MovementExecutorDeps { readonly getReportDir: () => string; readonly getLanguage: () => Language | undefined; readonly getInteractive: () => boolean; - readonly getWorkflowMovements: () => ReadonlyArray<{ name: string; description?: string }>; + readonly getPieceMovements: () => ReadonlyArray<{ name: string; description?: string }>; readonly detectRuleIndex: (content: string, movementName: string) => number; readonly callAiJudge: ( agentOutput: string, conditions: Array<{ index: number; text: string }>, options: { cwd: string } ) => Promise; - readonly onPhaseStart?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; - readonly onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + readonly onPhaseStart?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; + readonly onPhaseComplete?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; } export class MovementExecutor { @@ -50,13 +50,13 @@ export class MovementExecutor { /** Build Phase 1 instruction from template */ buildInstruction( - step: WorkflowMovement, + step: PieceMovement, movementIteration: number, - state: WorkflowState, + state: PieceState, task: string, maxIterations: number, ): string { - const workflowMovements = this.deps.getWorkflowMovements(); + const pieceMovements = this.deps.getPieceMovements(); return new InstructionBuilder(step, { task, iteration: state.iteration, @@ -69,8 +69,8 @@ export class MovementExecutor { reportDir: join(this.deps.getProjectCwd(), this.deps.getReportDir()), language: this.deps.getLanguage(), interactive: this.deps.getInteractive(), - workflowMovements: workflowMovements, - currentMovementIndex: workflowMovements.findIndex(s => s.name === step.name), + pieceMovements: pieceMovements, + currentMovementIndex: pieceMovements.findIndex(s => s.name === step.name), }).build(); } @@ -81,8 +81,8 @@ export class MovementExecutor { * and the instruction used for Phase 1. */ async runNormalMovement( - step: WorkflowMovement, - state: WorkflowState, + step: PieceMovement, + state: PieceState, task: string, maxIterations: number, updateAgentSession: (agent: string, sessionId: string | undefined) => void, @@ -140,7 +140,7 @@ export class MovementExecutor { } /** Collect movement:report events for each report file that exists */ - emitMovementReports(step: WorkflowMovement): void { + emitMovementReports(step: PieceMovement): void { if (!step.report) return; const baseDir = join(this.deps.getProjectCwd(), this.deps.getReportDir()); @@ -156,11 +156,11 @@ export class MovementExecutor { } } - // Collects report file paths that exist (used by WorkflowEngine to emit events) - private reportFiles: Array<{ step: WorkflowMovement; filePath: string; fileName: string }> = []; + // Collects report file paths that exist (used by PieceEngine to emit events) + private reportFiles: Array<{ step: PieceMovement; filePath: string; fileName: string }> = []; /** Check if report file exists and collect for emission */ - private checkReportFile(step: WorkflowMovement, baseDir: string, fileName: string): void { + private checkReportFile(step: PieceMovement, baseDir: string, fileName: string): void { const filePath = join(baseDir, fileName); if (existsSync(filePath)) { this.reportFiles.push({ step, filePath, fileName }); @@ -168,7 +168,7 @@ export class MovementExecutor { } /** Drain collected report files (called by engine after movement execution) */ - drainReportFiles(): Array<{ step: WorkflowMovement; filePath: string; fileName: string }> { + drainReportFiles(): Array<{ step: PieceMovement; filePath: string; fileName: string }> { const files = this.reportFiles; this.reportFiles = []; return files; diff --git a/src/core/workflow/engine/OptionsBuilder.ts b/src/core/piece/engine/OptionsBuilder.ts similarity index 81% rename from src/core/workflow/engine/OptionsBuilder.ts rename to src/core/piece/engine/OptionsBuilder.ts index d688539..d01ecfc 100644 --- a/src/core/workflow/engine/OptionsBuilder.ts +++ b/src/core/piece/engine/OptionsBuilder.ts @@ -2,18 +2,18 @@ * Builds RunAgentOptions for different execution phases. * * Centralizes the option construction logic that was previously - * scattered across WorkflowEngine methods. + * scattered across PieceEngine methods. */ import { join } from 'node:path'; -import type { WorkflowMovement, WorkflowState, Language } from '../../models/types.js'; +import type { PieceMovement, PieceState, Language } from '../../models/types.js'; import type { RunAgentOptions } from '../../../agents/runner.js'; import type { PhaseRunnerContext } from '../phase-runner.js'; -import type { WorkflowEngineOptions, PhaseName } from '../types.js'; +import type { PieceEngineOptions, PhaseName } from '../types.js'; export class OptionsBuilder { constructor( - private readonly engineOptions: WorkflowEngineOptions, + private readonly engineOptions: PieceEngineOptions, private readonly getCwd: () => string, private readonly getProjectCwd: () => string, private readonly getSessionId: (agent: string) => string | undefined, @@ -22,7 +22,7 @@ export class OptionsBuilder { ) {} /** Build common RunAgentOptions shared by all phases */ - buildBaseOptions(step: WorkflowMovement): RunAgentOptions { + buildBaseOptions(step: PieceMovement): RunAgentOptions { return { cwd: this.getCwd(), agentPath: step.agentPath, @@ -38,7 +38,7 @@ export class OptionsBuilder { } /** Build RunAgentOptions for Phase 1 (main execution) */ - buildAgentOptions(step: WorkflowMovement): RunAgentOptions { + buildAgentOptions(step: PieceMovement): RunAgentOptions { // Phase 1: exclude Write from allowedTools when movement has report config AND edit is NOT enabled // (If edit is enabled, Write is needed for code implementation even if report exists) // Note: edit defaults to undefined, so check !== true to catch both false and undefined @@ -58,7 +58,7 @@ export class OptionsBuilder { /** Build RunAgentOptions for session-resume phases (Phase 2, Phase 3) */ buildResumeOptions( - step: WorkflowMovement, + step: PieceMovement, sessionId: string, overrides: Pick, ): RunAgentOptions { @@ -74,10 +74,10 @@ export class OptionsBuilder { /** Build PhaseRunnerContext for Phase 2/3 execution */ buildPhaseRunnerContext( - state: WorkflowState, + state: PieceState, updateAgentSession: (agent: string, sessionId: string | undefined) => void, - onPhaseStart?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void, - onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void, + onPhaseStart?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void, + onPhaseComplete?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void, ): PhaseRunnerContext { return { cwd: this.getCwd(), diff --git a/src/core/workflow/engine/ParallelRunner.ts b/src/core/piece/engine/ParallelRunner.ts similarity index 91% rename from src/core/workflow/engine/ParallelRunner.ts rename to src/core/piece/engine/ParallelRunner.ts index 6c3b894..70c34e8 100644 --- a/src/core/workflow/engine/ParallelRunner.ts +++ b/src/core/piece/engine/ParallelRunner.ts @@ -1,13 +1,13 @@ /** - * Executes parallel workflow movements concurrently and aggregates results. + * Executes parallel piece movements concurrently and aggregates results. * * When onStream is provided, uses ParallelLogger to prefix each * sub-movement's output with `[name]` for readable interleaved display. */ import type { - WorkflowMovement, - WorkflowState, + PieceMovement, + PieceState, AgentResponse, } from '../../models/types.js'; import { runAgent } from '../../../agents/runner.js'; @@ -18,14 +18,14 @@ import { incrementMovementIteration } from './state-manager.js'; import { createLogger } from '../../../shared/utils/index.js'; import type { OptionsBuilder } from './OptionsBuilder.js'; import type { MovementExecutor } from './MovementExecutor.js'; -import type { WorkflowEngineOptions, PhaseName } from '../types.js'; +import type { PieceEngineOptions, PhaseName } from '../types.js'; const log = createLogger('parallel-runner'); export interface ParallelRunnerDeps { readonly optionsBuilder: OptionsBuilder; readonly movementExecutor: MovementExecutor; - readonly engineOptions: WorkflowEngineOptions; + readonly engineOptions: PieceEngineOptions; readonly getCwd: () => string; readonly getReportDir: () => string; readonly getInteractive: () => boolean; @@ -35,8 +35,8 @@ export interface ParallelRunnerDeps { conditions: Array<{ index: number; text: string }>, options: { cwd: string } ) => Promise; - readonly onPhaseStart?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; - readonly onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + readonly onPhaseStart?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; + readonly onPhaseComplete?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; } export class ParallelRunner { @@ -49,8 +49,8 @@ export class ParallelRunner { * The aggregated output becomes the parent movement's response for rules evaluation. */ async runParallelMovement( - step: WorkflowMovement, - state: WorkflowState, + step: PieceMovement, + state: PieceState, task: string, maxIterations: number, updateAgentSession: (agent: string, sessionId: string | undefined) => void, diff --git a/src/core/workflow/engine/WorkflowEngine.ts b/src/core/piece/engine/PieceEngine.ts similarity index 87% rename from src/core/workflow/engine/WorkflowEngine.ts rename to src/core/piece/engine/PieceEngine.ts index f8cdb81..05477ec 100644 --- a/src/core/workflow/engine/WorkflowEngine.ts +++ b/src/core/piece/engine/PieceEngine.ts @@ -1,5 +1,5 @@ /** - * Workflow execution engine. + * Piece execution engine. * * Orchestrates the main execution loop: movement transitions, abort handling, * loop detection, and iteration limits. Delegates movement execution to @@ -10,13 +10,13 @@ import { EventEmitter } from 'node:events'; import { mkdirSync, existsSync, symlinkSync } from 'node:fs'; import { join } from 'node:path'; import type { - WorkflowConfig, - WorkflowState, - WorkflowMovement, + PieceConfig, + PieceState, + PieceMovement, AgentResponse, } from '../../models/types.js'; import { COMPLETE_MOVEMENT, ABORT_MOVEMENT, ERROR_MESSAGES } from '../constants.js'; -import type { WorkflowEngineOptions } from '../types.js'; +import type { PieceEngineOptions } from '../types.js'; import { determineNextMovementByRules } from './transitions.js'; import { LoopDetector } from './loop-detector.js'; import { handleBlocked } from './blocked-handler.js'; @@ -33,23 +33,23 @@ import { ParallelRunner } from './ParallelRunner.js'; const log = createLogger('engine'); export type { - WorkflowEvents, + PieceEvents, UserInputRequest, IterationLimitRequest, SessionUpdateCallback, IterationLimitCallback, - WorkflowEngineOptions, + PieceEngineOptions, } from '../types.js'; export { COMPLETE_MOVEMENT, ABORT_MOVEMENT } from '../constants.js'; -/** Workflow engine for orchestrating agent execution */ -export class WorkflowEngine extends EventEmitter { - private state: WorkflowState; - private config: WorkflowConfig; +/** Piece engine for orchestrating agent execution */ +export class PieceEngine extends EventEmitter { + private state: PieceState; + private config: PieceConfig; private projectCwd: string; private cwd: string; private task: string; - private options: WorkflowEngineOptions; + private options: PieceEngineOptions; private loopDetector: LoopDetector; private reportDir: string; private abortRequested = false; @@ -64,7 +64,7 @@ export class WorkflowEngine extends EventEmitter { options: { cwd: string } ) => Promise; - constructor(config: WorkflowConfig, cwd: string, task: string, options: WorkflowEngineOptions) { + constructor(config: PieceConfig, cwd: string, task: string, options: PieceEngineOptions) { super(); this.config = config; this.projectCwd = options.projectCwd; @@ -100,7 +100,7 @@ export class WorkflowEngine extends EventEmitter { getReportDir: () => this.reportDir, getLanguage: () => this.options.language, getInteractive: () => this.options.interactive === true, - getWorkflowMovements: () => this.config.movements.map(s => ({ name: s.name, description: s.description })), + getPieceMovements: () => this.config.movements.map(s => ({ name: s.name, description: s.description })), detectRuleIndex: this.detectRuleIndex, callAiJudge: this.callAiJudge, onPhaseStart: (step, phase, phaseName, instruction) => { @@ -128,8 +128,8 @@ export class WorkflowEngine extends EventEmitter { }, }); - log.debug('WorkflowEngine initialized', { - workflow: config.name, + log.debug('PieceEngine initialized', { + piece: config.name, movements: config.movements.map(s => s.name), initialMovement: config.initialMovement, maxIterations: config.maxIterations, @@ -156,7 +156,7 @@ export class WorkflowEngine extends EventEmitter { } } - /** Validate workflow configuration at construction time */ + /** Validate piece configuration at construction time */ private validateConfig(): void { const initialMovement = this.config.movements.find((s) => s.name === this.config.initialMovement); if (!initialMovement) { @@ -180,8 +180,8 @@ export class WorkflowEngine extends EventEmitter { } } - /** Get current workflow state */ - getState(): WorkflowState { + /** Get current piece state */ + getState(): PieceState { return { ...this.state }; } @@ -218,7 +218,7 @@ export class WorkflowEngine extends EventEmitter { } /** Get movement by name */ - private getMovement(name: string): WorkflowMovement { + private getMovement(name: string): PieceMovement { const movement = this.config.movements.find((s) => s.name === name); if (!movement) { throw new Error(ERROR_MESSAGES.UNKNOWN_MOVEMENT(name)); @@ -246,7 +246,7 @@ export class WorkflowEngine extends EventEmitter { } /** Run a single movement (delegates to ParallelRunner if movement has parallel sub-movements) */ - private async runMovement(step: WorkflowMovement, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> { + private async runMovement(step: PieceMovement, prebuiltInstruction?: string): Promise<{ response: AgentResponse; instruction: string }> { const updateSession = this.updateAgentSession.bind(this); let result: { response: AgentResponse; instruction: string }; @@ -267,7 +267,7 @@ export class WorkflowEngine extends EventEmitter { /** * Determine next movement for a completed movement using rules-based routing. */ - private resolveNextMovement(step: WorkflowMovement, response: AgentResponse): string { + private resolveNextMovement(step: PieceMovement, response: AgentResponse): string { if (response.matchedRuleIndex != null && step.rules) { const nextByRules = determineNextMovementByRules(step, response.matchedRuleIndex); if (nextByRules) { @@ -278,19 +278,19 @@ export class WorkflowEngine extends EventEmitter { throw new Error(`No matching rule found for movement "${step.name}" (status: ${response.status})`); } - /** Build instruction (public, used by workflowExecution.ts for logging) */ - buildInstruction(step: WorkflowMovement, movementIteration: number): string { + /** Build instruction (public, used by pieceExecution.ts for logging) */ + buildInstruction(step: PieceMovement, movementIteration: number): string { return this.movementExecutor.buildInstruction( step, movementIteration, this.state, this.task, this.config.maxIterations, ); } - /** Run the workflow to completion */ - async run(): Promise { + /** Run the piece to completion */ + async run(): Promise { while (this.state.status === 'running') { if (this.abortRequested) { this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, 'Workflow interrupted by user (SIGINT)'); + this.emit('piece:abort', this.state, 'Piece interrupted by user (SIGINT)'); break; } @@ -314,7 +314,7 @@ export class WorkflowEngine extends EventEmitter { } this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, ERROR_MESSAGES.MAX_ITERATIONS_REACHED); + this.emit('piece:abort', this.state, ERROR_MESSAGES.MAX_ITERATIONS_REACHED); break; } @@ -327,7 +327,7 @@ export class WorkflowEngine extends EventEmitter { if (loopCheck.shouldAbort) { this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, ERROR_MESSAGES.LOOP_DETECTED(movement.name, loopCheck.count)); + this.emit('piece:abort', this.state, ERROR_MESSAGES.LOOP_DETECTED(movement.name, loopCheck.count)); break; } @@ -359,7 +359,7 @@ export class WorkflowEngine extends EventEmitter { } this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, 'Workflow blocked and no user input provided'); + this.emit('piece:abort', this.state, 'Piece blocked and no user input provided'); break; } @@ -376,7 +376,7 @@ export class WorkflowEngine extends EventEmitter { if (matchedRule?.requiresUserInput) { if (!this.options.onUserInput) { this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, 'User input required but no handler is configured'); + this.emit('piece:abort', this.state, 'User input required but no handler is configured'); break; } const userInput = await this.options.onUserInput({ @@ -386,7 +386,7 @@ export class WorkflowEngine extends EventEmitter { }); if (userInput === null) { this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, 'User input cancelled'); + this.emit('piece:abort', this.state, 'User input cancelled'); break; } this.addUserInput(userInput); @@ -398,13 +398,13 @@ export class WorkflowEngine extends EventEmitter { if (nextMovement === COMPLETE_MOVEMENT) { this.state.status = 'completed'; - this.emit('workflow:complete', this.state); + this.emit('piece:complete', this.state); break; } if (nextMovement === ABORT_MOVEMENT) { this.state.status = 'aborted'; - this.emit('workflow:abort', this.state, 'Workflow aborted by movement transition'); + this.emit('piece:abort', this.state, 'Piece aborted by movement transition'); break; } @@ -412,10 +412,10 @@ export class WorkflowEngine extends EventEmitter { } catch (error) { this.state.status = 'aborted'; if (this.abortRequested) { - this.emit('workflow:abort', this.state, 'Workflow interrupted by user (SIGINT)'); + this.emit('piece:abort', this.state, 'Piece interrupted by user (SIGINT)'); } else { const message = getErrorMessage(error); - this.emit('workflow:abort', this.state, ERROR_MESSAGES.MOVEMENT_EXECUTION_FAILED(message)); + this.emit('piece:abort', this.state, ERROR_MESSAGES.MOVEMENT_EXECUTION_FAILED(message)); } break; } diff --git a/src/core/workflow/engine/blocked-handler.ts b/src/core/piece/engine/blocked-handler.ts similarity index 77% rename from src/core/workflow/engine/blocked-handler.ts rename to src/core/piece/engine/blocked-handler.ts index 8930ba8..201da0d 100644 --- a/src/core/workflow/engine/blocked-handler.ts +++ b/src/core/piece/engine/blocked-handler.ts @@ -1,19 +1,19 @@ /** - * Blocked state handler for workflow execution + * Blocked state handler for piece execution * * Handles the case when an agent returns a blocked status, * requesting user input to continue. */ -import type { WorkflowMovement, AgentResponse } from '../../models/types.js'; -import type { UserInputRequest, WorkflowEngineOptions } from '../types.js'; +import type { PieceMovement, AgentResponse } from '../../models/types.js'; +import type { UserInputRequest, PieceEngineOptions } from '../types.js'; import { extractBlockedPrompt } from './transitions.js'; /** * Result of handling a blocked state. */ export interface BlockedHandlerResult { - /** Whether the workflow should continue */ + /** Whether the piece should continue */ shouldContinue: boolean; /** The user input provided (if any) */ userInput?: string; @@ -24,13 +24,13 @@ export interface BlockedHandlerResult { * * @param step - The movement that is blocked * @param response - The blocked response from the agent - * @param options - Workflow engine options containing callbacks + * @param options - Piece engine options containing callbacks * @returns Result indicating whether to continue and any user input */ export async function handleBlocked( - step: WorkflowMovement, + step: PieceMovement, response: AgentResponse, - options: WorkflowEngineOptions + options: PieceEngineOptions ): Promise { // If no user input callback is provided, cannot continue if (!options.onUserInput) { diff --git a/src/core/workflow/engine/index.ts b/src/core/piece/engine/index.ts similarity index 62% rename from src/core/workflow/engine/index.ts rename to src/core/piece/engine/index.ts index 9158c88..3c5bf20 100644 --- a/src/core/workflow/engine/index.ts +++ b/src/core/piece/engine/index.ts @@ -1,10 +1,10 @@ /** - * Workflow engine module. + * Piece engine module. * - * Re-exports the WorkflowEngine class and its supporting classes. + * Re-exports the PieceEngine class and its supporting classes. */ -export { WorkflowEngine } from './WorkflowEngine.js'; +export { PieceEngine } from './PieceEngine.js'; export { MovementExecutor } from './MovementExecutor.js'; export type { MovementExecutorDeps } from './MovementExecutor.js'; export { ParallelRunner } from './ParallelRunner.js'; diff --git a/src/core/workflow/engine/loop-detector.ts b/src/core/piece/engine/loop-detector.ts similarity index 93% rename from src/core/workflow/engine/loop-detector.ts rename to src/core/piece/engine/loop-detector.ts index 684ca26..9843ed9 100644 --- a/src/core/workflow/engine/loop-detector.ts +++ b/src/core/piece/engine/loop-detector.ts @@ -1,7 +1,7 @@ /** - * Loop detection for workflow execution + * Loop detection for piece execution * - * Detects when a workflow movement is executed repeatedly without progress, + * Detects when a piece movement is executed repeatedly without progress, * which may indicate an infinite loop. */ diff --git a/src/core/workflow/engine/parallel-logger.ts b/src/core/piece/engine/parallel-logger.ts similarity index 100% rename from src/core/workflow/engine/parallel-logger.ts rename to src/core/piece/engine/parallel-logger.ts diff --git a/src/core/workflow/engine/state-manager.ts b/src/core/piece/engine/state-manager.ts similarity index 74% rename from src/core/workflow/engine/state-manager.ts rename to src/core/piece/engine/state-manager.ts index 9145fdb..60fe1ab 100644 --- a/src/core/workflow/engine/state-manager.ts +++ b/src/core/piece/engine/state-manager.ts @@ -1,26 +1,26 @@ /** - * Workflow state management + * Piece state management * - * Manages the mutable state of a workflow execution including + * Manages the mutable state of a piece execution including * user inputs and agent sessions. */ -import type { WorkflowState, WorkflowConfig, AgentResponse } from '../../models/types.js'; +import type { PieceState, PieceConfig, AgentResponse } from '../../models/types.js'; import { MAX_USER_INPUTS, MAX_INPUT_LENGTH, } from '../constants.js'; -import type { WorkflowEngineOptions } from '../types.js'; +import type { PieceEngineOptions } from '../types.js'; /** - * Manages workflow execution state. + * Manages piece execution state. * - * Encapsulates WorkflowState and provides methods for state mutations. + * Encapsulates PieceState and provides methods for state mutations. */ export class StateManager { - readonly state: WorkflowState; + readonly state: PieceState; - constructor(config: WorkflowConfig, options: WorkflowEngineOptions) { + constructor(config: PieceConfig, options: PieceEngineOptions) { // Restore agent sessions from options if provided const agentSessions = new Map(); if (options.initialSessions) { @@ -35,7 +35,7 @@ export class StateManager { : []; this.state = { - workflowName: config.name, + pieceName: config.name, currentMovement: config.initialMovement, iteration: 0, movementOutputs: new Map(), @@ -78,19 +78,19 @@ export class StateManager { } /** - * Create initial workflow state from config and options. + * Create initial piece state from config and options. */ export function createInitialState( - config: WorkflowConfig, - options: WorkflowEngineOptions, -): WorkflowState { + config: PieceConfig, + options: PieceEngineOptions, +): PieceState { return new StateManager(config, options).state; } /** * Increment the iteration counter for a movement and return the new value. */ -export function incrementMovementIteration(state: WorkflowState, movementName: string): number { +export function incrementMovementIteration(state: PieceState, movementName: string): number { const current = state.movementIterations.get(movementName) ?? 0; const next = current + 1; state.movementIterations.set(movementName, next); @@ -100,7 +100,7 @@ export function incrementMovementIteration(state: WorkflowState, movementName: s /** * Add user input to state with truncation and limit handling. */ -export function addUserInput(state: WorkflowState, input: string): void { +export function addUserInput(state: PieceState, input: string): void { if (state.userInputs.length >= MAX_USER_INPUTS) { state.userInputs.shift(); } @@ -111,7 +111,7 @@ export function addUserInput(state: WorkflowState, input: string): void { /** * Get the most recent movement output. */ -export function getPreviousOutput(state: WorkflowState): AgentResponse | undefined { +export function getPreviousOutput(state: PieceState): AgentResponse | undefined { if (state.lastOutput) return state.lastOutput; const outputs = Array.from(state.movementOutputs.values()); return outputs[outputs.length - 1]; diff --git a/src/core/workflow/engine/transitions.ts b/src/core/piece/engine/transitions.ts similarity index 93% rename from src/core/workflow/engine/transitions.ts rename to src/core/piece/engine/transitions.ts index 1b300c8..1b81568 100644 --- a/src/core/workflow/engine/transitions.ts +++ b/src/core/piece/engine/transitions.ts @@ -1,11 +1,11 @@ /** - * Workflow state transition logic + * Piece state transition logic * * Handles determining the next movement based on rules-based routing. */ import type { - WorkflowMovement, + PieceMovement, } from '../../models/types.js'; /** @@ -13,7 +13,7 @@ import type { * Returns the next movement name from the matched rule, or null if no rule matched. */ export function determineNextMovementByRules( - step: WorkflowMovement, + step: PieceMovement, ruleIndex: number, ): string | null { const rule = step.rules?.[ruleIndex]; diff --git a/src/core/workflow/evaluation/AggregateEvaluator.ts b/src/core/piece/evaluation/AggregateEvaluator.ts similarity index 95% rename from src/core/workflow/evaluation/AggregateEvaluator.ts rename to src/core/piece/evaluation/AggregateEvaluator.ts index 506fd06..ba91def 100644 --- a/src/core/workflow/evaluation/AggregateEvaluator.ts +++ b/src/core/piece/evaluation/AggregateEvaluator.ts @@ -1,10 +1,10 @@ /** - * Aggregate condition evaluator for parallel workflow movements + * Aggregate condition evaluator for parallel piece movements * * Evaluates all()/any() aggregate conditions against sub-movement results. */ -import type { WorkflowMovement, WorkflowState } from '../../models/types.js'; +import type { PieceMovement, PieceState } from '../../models/types.js'; import { createLogger } from '../../../shared/utils/index.js'; const log = createLogger('aggregate-evaluator'); @@ -26,8 +26,8 @@ const log = createLogger('aggregate-evaluator'); */ export class AggregateEvaluator { constructor( - private readonly step: WorkflowMovement, - private readonly state: WorkflowState, + private readonly step: PieceMovement, + private readonly state: PieceState, ) {} /** diff --git a/src/core/workflow/evaluation/RuleEvaluator.ts b/src/core/piece/evaluation/RuleEvaluator.ts similarity index 94% rename from src/core/workflow/evaluation/RuleEvaluator.ts rename to src/core/piece/evaluation/RuleEvaluator.ts index b905d32..b2f8152 100644 --- a/src/core/workflow/evaluation/RuleEvaluator.ts +++ b/src/core/piece/evaluation/RuleEvaluator.ts @@ -1,14 +1,14 @@ /** - * Rule evaluation logic for workflow movements + * Rule evaluation logic for piece movements * - * Evaluates workflow movement rules to determine the matched rule index. + * Evaluates piece movement rules to determine the matched rule index. * Supports tag-based detection, ai() conditions, aggregate conditions, * and AI judge fallback. */ import type { - WorkflowMovement, - WorkflowState, + PieceMovement, + PieceState, RuleMatchMethod, } from '../../models/types.js'; import type { AiJudgeCaller, RuleIndexDetector } from '../types.js'; @@ -23,8 +23,8 @@ export interface RuleMatch { } export interface RuleEvaluatorContext { - /** Workflow state (for accessing movementOutputs in aggregate evaluation) */ - state: WorkflowState; + /** Piece state (for accessing movementOutputs in aggregate evaluation) */ + state: PieceState; /** Working directory (for AI judge calls) */ cwd: string; /** Whether interactive-only rules are enabled */ @@ -36,7 +36,7 @@ export interface RuleEvaluatorContext { } /** - * Evaluates rules for a workflow movement to determine the next transition. + * Evaluates rules for a piece movement to determine the next transition. * * Evaluation order (first match wins): * 1. Aggregate conditions: all()/any() — evaluate sub-movement results @@ -50,7 +50,7 @@ export interface RuleEvaluatorContext { */ export class RuleEvaluator { constructor( - private readonly step: WorkflowMovement, + private readonly step: PieceMovement, private readonly ctx: RuleEvaluatorContext, ) {} diff --git a/src/core/workflow/evaluation/index.ts b/src/core/piece/evaluation/index.ts similarity index 82% rename from src/core/workflow/evaluation/index.ts rename to src/core/piece/evaluation/index.ts index 3820a49..a5c6ec7 100644 --- a/src/core/workflow/evaluation/index.ts +++ b/src/core/piece/evaluation/index.ts @@ -2,7 +2,7 @@ * Rule evaluation - barrel exports */ -import type { WorkflowMovement, WorkflowState } from '../../models/types.js'; +import type { PieceMovement, PieceState } from '../../models/types.js'; import { RuleEvaluator } from './RuleEvaluator.js'; import { AggregateEvaluator } from './AggregateEvaluator.js'; @@ -18,7 +18,7 @@ import type { RuleMatch, RuleEvaluatorContext } from './RuleEvaluator.js'; * Function facade over RuleEvaluator class. */ export async function detectMatchedRule( - step: WorkflowMovement, + step: PieceMovement, agentContent: string, tagContent: string, ctx: RuleEvaluatorContext, @@ -30,6 +30,6 @@ export async function detectMatchedRule( * Evaluate aggregate conditions. * Function facade over AggregateEvaluator class. */ -export function evaluateAggregateConditions(step: WorkflowMovement, state: WorkflowState): number { +export function evaluateAggregateConditions(step: PieceMovement, state: PieceState): number { return new AggregateEvaluator(step, state).evaluate(); } diff --git a/src/core/workflow/evaluation/rule-utils.ts b/src/core/piece/evaluation/rule-utils.ts similarity index 80% rename from src/core/workflow/evaluation/rule-utils.ts rename to src/core/piece/evaluation/rule-utils.ts index 167ab66..2eae770 100644 --- a/src/core/workflow/evaluation/rule-utils.ts +++ b/src/core/piece/evaluation/rule-utils.ts @@ -2,7 +2,7 @@ * Shared rule utility functions used by both engine.ts and instruction-builder.ts. */ -import type { WorkflowMovement } from '../../models/types.js'; +import type { PieceMovement } from '../../models/types.js'; /** * Check whether a movement has tag-based rules (i.e., rules that require @@ -11,7 +11,7 @@ import type { WorkflowMovement } from '../../models/types.js'; * Returns false when all rules are ai() or aggregate conditions, * meaning no tag-based status output is needed. */ -export function hasTagBasedRules(step: WorkflowMovement): boolean { +export function hasTagBasedRules(step: PieceMovement): boolean { if (!step.rules || step.rules.length === 0) return false; const allNonTagConditions = step.rules.every((r) => r.isAiCondition || r.isAggregateCondition); return !allNonTagConditions; diff --git a/src/core/workflow/index.ts b/src/core/piece/index.ts similarity index 92% rename from src/core/workflow/index.ts rename to src/core/piece/index.ts index 89f1232..6a08774 100644 --- a/src/core/workflow/index.ts +++ b/src/core/piece/index.ts @@ -1,25 +1,25 @@ /** - * Workflow module public API + * Piece module public API * * This file exports all public types, functions, and classes - * from the workflow module. + * from the piece module. */ // Main engine -export { WorkflowEngine } from './engine/index.js'; +export { PieceEngine } from './engine/index.js'; // Constants export { COMPLETE_MOVEMENT, ABORT_MOVEMENT, ERROR_MESSAGES } from './constants.js'; // Types export type { - WorkflowEvents, + PieceEvents, PhaseName, UserInputRequest, IterationLimitRequest, SessionUpdateCallback, IterationLimitCallback, - WorkflowEngineOptions, + PieceEngineOptions, LoopCheckResult, StreamEvent, StreamCallback, diff --git a/src/core/workflow/instruction/InstructionBuilder.ts b/src/core/piece/instruction/InstructionBuilder.ts similarity index 86% rename from src/core/workflow/instruction/InstructionBuilder.ts rename to src/core/piece/instruction/InstructionBuilder.ts index 60aa10f..b570539 100644 --- a/src/core/workflow/instruction/InstructionBuilder.ts +++ b/src/core/piece/instruction/InstructionBuilder.ts @@ -5,7 +5,7 @@ * Assembles template variables and renders a single complete template. */ -import type { WorkflowMovement, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js'; +import type { PieceMovement, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js'; import type { InstructionContext } from './instruction-context.js'; import { buildEditRule } from './instruction-context.js'; import { escapeTemplateChars, replaceTemplatePlaceholders } from './escape.js'; @@ -26,7 +26,7 @@ export function isReportObjectConfig(report: string | ReportConfig[] | ReportObj */ export class InstructionBuilder { constructor( - private readonly step: WorkflowMovement, + private readonly step: PieceMovement, private readonly context: InstructionContext, ) {} @@ -42,8 +42,8 @@ export class InstructionBuilder { // Execution context variables const editRule = buildEditRule(this.step.edit, language); - // Workflow structure (loop expansion done in code) - const workflowStructure = this.buildWorkflowStructure(language); + // Piece structure (loop expansion done in code) + const pieceStructure = this.buildPieceStructure(language); // Report info const hasReport = !!(this.step.report && this.context.reportDir); @@ -92,7 +92,7 @@ export class InstructionBuilder { return loadTemplate('perform_phase1_message', language, { workingDirectory: this.context.cwd, editRule, - workflowStructure, + pieceStructure, iteration: `${this.context.iteration}/${this.context.maxIterations}`, movementIteration: String(this.context.movementIteration), movement: this.step.name, @@ -110,19 +110,19 @@ export class InstructionBuilder { } /** - * Build the workflow structure display string. - * Returns empty string if no workflow movements are available. + * Build the piece structure display string. + * Returns empty string if no piece movements are available. */ - private buildWorkflowStructure(language: Language): string { - if (!this.context.workflowMovements || this.context.workflowMovements.length === 0) { + private buildPieceStructure(language: Language): string { + if (!this.context.pieceMovements || this.context.pieceMovements.length === 0) { return ''; } const currentMovementMarker = language === 'ja' ? '現在' : 'current'; const structureHeader = language === 'ja' - ? `このワークフローは${this.context.workflowMovements.length}ムーブメントで構成されています:` - : `This workflow consists of ${this.context.workflowMovements.length} movements:`; - const movementLines = this.context.workflowMovements.map((ws, index) => { + ? `このピースは${this.context.pieceMovements.length}ムーブメントで構成されています:` + : `This piece consists of ${this.context.pieceMovements.length} movements:`; + const movementLines = this.context.pieceMovements.map((ws, index) => { const isCurrent = index === this.context.currentMovementIndex; const marker = isCurrent ? ` ← ${currentMovementMarker}` : ''; const desc = ws.description ? `(${ws.description})` : ''; @@ -133,7 +133,7 @@ export class InstructionBuilder { } /** - * Render report context info for Workflow Context section. + * Render report context info for Piece Context section. * Used by InstructionBuilder and ReportInstructionBuilder. */ export function renderReportContext( @@ -167,7 +167,7 @@ export function renderReportContext( * Returns empty string if movement has no report or no reportDir. */ export function renderReportOutputInstruction( - step: WorkflowMovement, + step: PieceMovement, context: InstructionContext, language: Language, ): string { diff --git a/src/core/workflow/instruction/ReportInstructionBuilder.ts b/src/core/piece/instruction/ReportInstructionBuilder.ts similarity index 94% rename from src/core/workflow/instruction/ReportInstructionBuilder.ts rename to src/core/piece/instruction/ReportInstructionBuilder.ts index 0cae94f..bbf84b0 100644 --- a/src/core/workflow/instruction/ReportInstructionBuilder.ts +++ b/src/core/piece/instruction/ReportInstructionBuilder.ts @@ -5,7 +5,7 @@ * Assembles template variables and renders a single complete template. */ -import type { WorkflowMovement, Language } from '../../models/types.js'; +import type { PieceMovement, Language } from '../../models/types.js'; import type { InstructionContext } from './instruction-context.js'; import { replaceTemplatePlaceholders } from './escape.js'; import { isReportObjectConfig, renderReportContext, renderReportOutputInstruction } from './InstructionBuilder.js'; @@ -34,7 +34,7 @@ export interface ReportInstructionContext { */ export class ReportInstructionBuilder { constructor( - private readonly step: WorkflowMovement, + private readonly step: PieceMovement, private readonly context: ReportInstructionContext, ) {} @@ -45,7 +45,7 @@ export class ReportInstructionBuilder { const language = this.context.language ?? 'en'; - // Build report context for Workflow Context section + // Build report context for Piece Context section let reportContext: string; if (this.context.targetFile) { reportContext = `- Report Directory: ${this.context.reportDir}/\n- Report File: ${this.context.reportDir}/${this.context.targetFile}`; diff --git a/src/core/workflow/instruction/StatusJudgmentBuilder.ts b/src/core/piece/instruction/StatusJudgmentBuilder.ts similarity index 93% rename from src/core/workflow/instruction/StatusJudgmentBuilder.ts rename to src/core/piece/instruction/StatusJudgmentBuilder.ts index bd4adc9..7260d85 100644 --- a/src/core/workflow/instruction/StatusJudgmentBuilder.ts +++ b/src/core/piece/instruction/StatusJudgmentBuilder.ts @@ -8,7 +8,7 @@ * and status rules (criteria table + output format). */ -import type { WorkflowMovement, Language } from '../../models/types.js'; +import type { PieceMovement, Language } from '../../models/types.js'; import { generateStatusRulesComponents } from './status-rules.js'; import { loadTemplate } from '../../../shared/prompts/index.js'; @@ -29,7 +29,7 @@ export interface StatusJudgmentContext { */ export class StatusJudgmentBuilder { constructor( - private readonly step: WorkflowMovement, + private readonly step: PieceMovement, private readonly context: StatusJudgmentContext, ) {} diff --git a/src/core/workflow/instruction/escape.ts b/src/core/piece/instruction/escape.ts similarity index 95% rename from src/core/workflow/instruction/escape.ts rename to src/core/piece/instruction/escape.ts index 8e9d0ea..fbf6718 100644 --- a/src/core/workflow/instruction/escape.ts +++ b/src/core/piece/instruction/escape.ts @@ -4,7 +4,7 @@ * Used by instruction builders to process instruction_template content. */ -import type { WorkflowMovement } from '../../models/types.js'; +import type { PieceMovement } from '../../models/types.js'; import type { InstructionContext } from './instruction-context.js'; /** @@ -22,7 +22,7 @@ export function escapeTemplateChars(str: string): string { */ export function replaceTemplatePlaceholders( template: string, - step: WorkflowMovement, + step: PieceMovement, context: InstructionContext, ): string { let result = template; diff --git a/src/core/workflow/instruction/index.ts b/src/core/piece/instruction/index.ts similarity index 100% rename from src/core/workflow/instruction/index.ts rename to src/core/piece/instruction/index.ts diff --git a/src/core/workflow/instruction/instruction-context.ts b/src/core/piece/instruction/instruction-context.ts similarity index 87% rename from src/core/workflow/instruction/instruction-context.ts rename to src/core/piece/instruction/instruction-context.ts index 764fa07..761882e 100644 --- a/src/core/workflow/instruction/instruction-context.ts +++ b/src/core/piece/instruction/instruction-context.ts @@ -12,7 +12,7 @@ import type { AgentResponse, Language } from '../../models/types.js'; export interface InstructionContext { /** The main task/prompt */ task: string; - /** Current iteration number (workflow-wide turn count) */ + /** Current iteration number (piece-wide turn count) */ iteration: number; /** Maximum iterations allowed */ maxIterations: number; @@ -22,7 +22,7 @@ export interface InstructionContext { cwd: string; /** Project root directory (where .takt/ lives). */ projectCwd: string; - /** User inputs accumulated during workflow */ + /** User inputs accumulated during piece */ userInputs: string[]; /** Previous movement output if available */ previousOutput?: AgentResponse; @@ -32,9 +32,9 @@ export interface InstructionContext { language?: Language; /** Whether interactive-only rules are enabled */ interactive?: boolean; - /** Top-level workflow movements for workflow structure display */ - workflowMovements?: ReadonlyArray<{ name: string; description?: string }>; - /** Index of the current movement in workflowMovements (0-based) */ + /** Top-level piece movements for piece structure display */ + pieceMovements?: ReadonlyArray<{ name: string; description?: string }>; + /** Index of the current movement in pieceMovements (0-based) */ currentMovementIndex?: number; } diff --git a/src/core/workflow/instruction/status-rules.ts b/src/core/piece/instruction/status-rules.ts similarity index 95% rename from src/core/workflow/instruction/status-rules.ts rename to src/core/piece/instruction/status-rules.ts index d3eb676..c17b340 100644 --- a/src/core/workflow/instruction/status-rules.ts +++ b/src/core/piece/instruction/status-rules.ts @@ -1,5 +1,5 @@ /** - * Status rules prompt generation for workflow movements + * Status rules prompt generation for piece movements * * Generates structured status rules content that tells agents which * numbered tags to output based on the movement's rule configuration. @@ -8,7 +8,7 @@ * that are passed as template variables to Phase 1/Phase 3 templates. */ -import type { WorkflowRule, Language } from '../../models/types.js'; +import type { PieceRule, Language } from '../../models/types.js'; /** Components of the generated status rules */ export interface StatusRulesComponents { @@ -27,7 +27,7 @@ export interface StatusRulesComponents { */ export function generateStatusRulesComponents( movementName: string, - rules: WorkflowRule[], + rules: PieceRule[], language: Language, options?: { interactive?: boolean }, ): StatusRulesComponents { diff --git a/src/core/workflow/parallel-logger.ts b/src/core/piece/parallel-logger.ts similarity index 100% rename from src/core/workflow/parallel-logger.ts rename to src/core/piece/parallel-logger.ts diff --git a/src/core/workflow/phase-runner.ts b/src/core/piece/phase-runner.ts similarity index 91% rename from src/core/workflow/phase-runner.ts rename to src/core/piece/phase-runner.ts index 6672e89..138a99b 100644 --- a/src/core/workflow/phase-runner.ts +++ b/src/core/piece/phase-runner.ts @@ -7,7 +7,7 @@ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, resolve, sep } from 'node:path'; -import type { WorkflowMovement, Language } from '../models/types.js'; +import type { PieceMovement, Language } from '../models/types.js'; import type { PhaseName } from './types.js'; import { runAgent, type RunAgentOptions } from '../../agents/runner.js'; import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js'; @@ -30,24 +30,24 @@ export interface PhaseRunnerContext { /** Get agent session ID */ getSessionId: (agent: string) => string | undefined; /** Build resume options for a movement */ - buildResumeOptions: (step: WorkflowMovement, sessionId: string, overrides: Pick) => RunAgentOptions; + buildResumeOptions: (step: PieceMovement, sessionId: string, overrides: Pick) => RunAgentOptions; /** Update agent session after a phase run */ updateAgentSession: (agent: string, sessionId: string | undefined) => void; /** Callback for phase lifecycle logging */ - onPhaseStart?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; + onPhaseStart?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; /** Callback for phase completion logging */ - onPhaseComplete?: (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + onPhaseComplete?: (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; } /** * Check if a movement needs Phase 3 (status judgment). * Returns true when at least one rule requires tag-based detection. */ -export function needsStatusJudgmentPhase(step: WorkflowMovement): boolean { +export function needsStatusJudgmentPhase(step: PieceMovement): boolean { return hasTagBasedRules(step); } -function getReportFiles(report: WorkflowMovement['report']): string[] { +function getReportFiles(report: PieceMovement['report']): string[] { if (!report) return []; if (typeof report === 'string') return [report]; if (isReportObjectConfig(report)) return [report.name]; @@ -76,7 +76,7 @@ function writeReportFile(reportDir: string, fileName: string, content: string): * Plain text responses are written directly to files (no JSON parsing). */ export async function runReportPhase( - step: WorkflowMovement, + step: PieceMovement, movementIteration: number, ctx: PhaseRunnerContext, ): Promise { @@ -156,7 +156,7 @@ export async function runReportPhase( * Returns the Phase 3 response content (containing the status tag). */ export async function runStatusJudgmentPhase( - step: WorkflowMovement, + step: PieceMovement, ctx: PhaseRunnerContext, ): Promise { const sessionKey = step.agent ?? step.name; diff --git a/src/core/workflow/types.ts b/src/core/piece/types.ts similarity index 78% rename from src/core/workflow/types.ts rename to src/core/piece/types.ts index c54816e..f5e179a 100644 --- a/src/core/workflow/types.ts +++ b/src/core/piece/types.ts @@ -1,12 +1,12 @@ /** - * Workflow engine type definitions + * Piece engine type definitions * - * Contains types for workflow events, requests, and callbacks - * used by the workflow execution engine. + * Contains types for piece events, requests, and callbacks + * used by the piece execution engine. */ import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'; -import type { WorkflowMovement, AgentResponse, WorkflowState, Language } from '../models/types.js'; +import type { PieceMovement, AgentResponse, PieceState, Language } from '../models/types.js'; export type ProviderType = 'claude' | 'codex' | 'mock'; @@ -106,25 +106,25 @@ export type AiJudgeCaller = ( export type PhaseName = 'execute' | 'report' | 'judge'; -/** Events emitted by workflow engine */ -export interface WorkflowEvents { - 'movement:start': (step: WorkflowMovement, iteration: number, instruction: string) => void; - 'movement:complete': (step: WorkflowMovement, response: AgentResponse, instruction: string) => void; - 'movement:report': (step: WorkflowMovement, filePath: string, fileName: string) => void; - 'movement:blocked': (step: WorkflowMovement, response: AgentResponse) => void; - 'movement:user_input': (step: WorkflowMovement, userInput: string) => void; - 'phase:start': (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; - 'phase:complete': (step: WorkflowMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; - 'workflow:complete': (state: WorkflowState) => void; - 'workflow:abort': (state: WorkflowState, reason: string) => void; +/** Events emitted by piece engine */ +export interface PieceEvents { + 'movement:start': (step: PieceMovement, iteration: number, instruction: string) => void; + 'movement:complete': (step: PieceMovement, response: AgentResponse, instruction: string) => void; + 'movement:report': (step: PieceMovement, filePath: string, fileName: string) => void; + 'movement:blocked': (step: PieceMovement, response: AgentResponse) => void; + 'movement:user_input': (step: PieceMovement, userInput: string) => void; + 'phase:start': (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, instruction: string) => void; + 'phase:complete': (step: PieceMovement, phase: 1 | 2 | 3, phaseName: PhaseName, content: string, status: string, error?: string) => void; + 'piece:complete': (state: PieceState) => void; + 'piece:abort': (state: PieceState, reason: string) => void; 'iteration:limit': (iteration: number, maxIterations: number) => void; - 'movement:loop_detected': (step: WorkflowMovement, consecutiveCount: number) => void; + 'movement:loop_detected': (step: PieceMovement, consecutiveCount: number) => void; } /** User input request for blocked state */ export interface UserInputRequest { /** The movement that is blocked */ - movement: WorkflowMovement; + movement: PieceMovement; /** The blocked response from the agent */ response: AgentResponse; /** Prompt for the user (extracted from blocked message) */ @@ -150,8 +150,8 @@ export type SessionUpdateCallback = (agentName: string, sessionId: string) => vo */ export type IterationLimitCallback = (request: IterationLimitRequest) => Promise; -/** Options for workflow engine */ -export interface WorkflowEngineOptions { +/** Options for piece engine */ +export interface PieceEngineOptions { /** Callback for streaming real-time output */ onStream?: StreamCallback; /** Callback for requesting user input when an agent is blocked */ diff --git a/src/features/config/ejectBuiltin.ts b/src/features/config/ejectBuiltin.ts index a968208..d2816a5 100644 --- a/src/features/config/ejectBuiltin.ts +++ b/src/features/config/ejectBuiltin.ts @@ -1,7 +1,7 @@ /** * /eject command implementation * - * Copies a builtin workflow (and its agents) to ~/.takt/ for user customization. + * Copies a builtin piece (and its agents) to ~/.takt/ for user customization. * Once ejected, the user copy takes priority over the builtin version. */ @@ -17,48 +17,48 @@ import { import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js'; /** - * Eject a builtin workflow to user space for customization. - * Copies the workflow YAML and related agent .md files to ~/.takt/. - * Agent paths in the ejected workflow are rewritten from ../agents/ to ~/.takt/agents/. + * 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/. */ export async function ejectBuiltin(name?: string): Promise { header('Eject Builtin'); const lang = getLanguage(); - const builtinWorkflowsDir = getBuiltinPiecesDir(lang); + const builtinPiecesDir = getBuiltinPiecesDir(lang); if (!name) { // List available builtins - listAvailableBuiltins(builtinWorkflowsDir); + listAvailableBuiltins(builtinPiecesDir); return; } - const builtinPath = join(builtinWorkflowsDir, `${name}.yaml`); + const builtinPath = join(builtinPiecesDir, `${name}.yaml`); if (!existsSync(builtinPath)) { - error(`Builtin workflow not found: ${name}`); + error(`Builtin piece not found: ${name}`); info('Run "takt eject" to see available builtins.'); return; } - const userWorkflowsDir = getGlobalPiecesDir(); + const userPiecesDir = getGlobalPiecesDir(); const userAgentsDir = getGlobalAgentsDir(); const builtinAgentsDir = getBuiltinAgentsDir(lang); - // Copy workflow YAML (rewrite agent paths) - const workflowDest = join(userWorkflowsDir, `${name}.yaml`); - if (existsSync(workflowDest)) { - warn(`User workflow already exists: ${workflowDest}`); - warn('Skipping workflow copy (user version takes priority).'); + // Copy piece YAML (rewrite agent paths) + const pieceDest = join(userPiecesDir, `${name}.yaml`); + if (existsSync(pieceDest)) { + warn(`User piece already exists: ${pieceDest}`); + warn('Skipping piece copy (user version takes priority).'); } else { - mkdirSync(dirname(workflowDest), { recursive: true }); + 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(workflowDest, rewritten, 'utf-8'); - success(`Ejected workflow: ${workflowDest}`); + writeFileSync(pieceDest, rewritten, 'utf-8'); + success(`Ejected piece: ${pieceDest}`); } // Copy related agent files @@ -87,19 +87,19 @@ export async function ejectBuiltin(name?: string): Promise { } } -/** List available builtin workflows for ejection */ -function listAvailableBuiltins(builtinWorkflowsDir: string): void { - if (!existsSync(builtinWorkflowsDir)) { - warn('No builtin workflows found.'); +/** List available builtin pieces for ejection */ +function listAvailableBuiltins(builtinPiecesDir: string): void { + if (!existsSync(builtinPiecesDir)) { + warn('No builtin pieces found.'); return; } - info('Available builtin workflows:'); + info('Available builtin pieces:'); blankLine(); - for (const entry of readdirSync(builtinWorkflowsDir).sort()) { + for (const entry of readdirSync(builtinPiecesDir).sort()) { if (!entry.endsWith('.yaml') && !entry.endsWith('.yml')) continue; - if (!statSync(join(builtinWorkflowsDir, entry)).isFile()) continue; + if (!statSync(join(builtinPiecesDir, entry)).isFile()) continue; const name = entry.replace(/\.ya?ml$/, ''); info(` ${name}`); @@ -110,11 +110,11 @@ function listAvailableBuiltins(builtinWorkflowsDir: string): void { } /** - * Extract agent relative paths from a builtin workflow YAML. + * Extract agent relative paths from a builtin piece YAML. * Matches `agent: ../agents/{path}` and returns the {path} portions. */ -function extractAgentRelativePaths(workflowPath: string): string[] { - const content = readFileSync(workflowPath, 'utf-8'); +function extractAgentRelativePaths(piecePath: string): string[] { + const content = readFileSync(piecePath, 'utf-8'); const paths = new Set(); const regex = /agent:\s*\.\.\/agents\/(.+)/g; diff --git a/src/features/config/index.ts b/src/features/config/index.ts index 32676d9..a0b4f0f 100644 --- a/src/features/config/index.ts +++ b/src/features/config/index.ts @@ -2,6 +2,6 @@ * Config feature exports */ -export { switchWorkflow } from './switchWorkflow.js'; +export { switchPiece } from './switchPiece.js'; export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './switchConfig.js'; export { ejectBuiltin } from './ejectBuiltin.js'; diff --git a/src/features/config/switchConfig.ts b/src/features/config/switchConfig.ts index ded0a00..048e8a1 100644 --- a/src/features/config/switchConfig.ts +++ b/src/features/config/switchConfig.ts @@ -1,8 +1,8 @@ /** - * Config switching command (like workflow switching) + * Config switching command (like piece switching) * * Permission mode selection that works from CLI. - * Uses selectOption for prompt selection, same pattern as switchWorkflow. + * Uses selectOption for prompt selection, same pattern as switchPiece. */ import chalk from 'chalk'; @@ -89,7 +89,7 @@ export function setPermissionMode(cwd: string, mode: PermissionMode): void { } /** - * Switch permission mode (like switchWorkflow) + * Switch permission mode (like switchPiece) * @returns true if switch was successful */ export async function switchConfig(cwd: string, modeName?: string): Promise { diff --git a/src/features/config/switchPiece.ts b/src/features/config/switchPiece.ts new file mode 100644 index 0000000..1551fad --- /dev/null +++ b/src/features/config/switchPiece.ts @@ -0,0 +1,69 @@ +/** + * Piece switching command + */ + +import { + listPieceEntries, + loadAllPiecesWithSources, + getPieceCategories, + buildCategorizedPieces, + loadPiece, + getCurrentPiece, + setCurrentPiece, +} from '../../infra/config/index.js'; +import { info, success, error } from '../../shared/ui/index.js'; +import { + warnMissingPieces, + selectPieceFromCategorizedPieces, + selectPieceFromEntries, +} from '../pieceSelection/index.js'; + +/** + * Switch to a different piece + * @returns true if switch was successful + */ +export async function switchPiece(cwd: string, pieceName?: string): Promise { + // No piece specified - show selection prompt + if (!pieceName) { + const current = getCurrentPiece(cwd); + info(`Current piece: ${current}`); + + const categoryConfig = getPieceCategories(cwd); + let selected: string | null; + if (categoryConfig) { + const allPieces = loadAllPiecesWithSources(cwd); + if (allPieces.size === 0) { + info('No pieces found.'); + selected = null; + } else { + const categorized = buildCategorizedPieces(allPieces, categoryConfig); + warnMissingPieces(categorized.missingPieces); + selected = await selectPieceFromCategorizedPieces(categorized, current); + } + } else { + const entries = listPieceEntries(cwd); + selected = await selectPieceFromEntries(entries, current); + } + + if (!selected) { + info('Cancelled'); + return false; + } + + pieceName = selected; + } + + // Check if piece exists + const config = loadPiece(pieceName, cwd); + + if (!config) { + error(`Piece "${pieceName}" not found`); + return false; + } + + // Save to project config + setCurrentPiece(cwd, pieceName); + success(`Switched to piece: ${pieceName}`); + + return true; +} diff --git a/src/features/config/switchWorkflow.ts b/src/features/config/switchWorkflow.ts deleted file mode 100644 index 2b913d2..0000000 --- a/src/features/config/switchWorkflow.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Workflow switching command - */ - -import { - listWorkflowEntries, - loadAllWorkflowsWithSources, - getWorkflowCategories, - buildCategorizedWorkflows, - loadWorkflow, - getCurrentWorkflow, - setCurrentWorkflow, -} from '../../infra/config/index.js'; -import { info, success, error } from '../../shared/ui/index.js'; -import { - warnMissingWorkflows, - selectWorkflowFromCategorizedWorkflows, - selectWorkflowFromEntries, -} from '../workflowSelection/index.js'; - -/** - * Switch to a different workflow - * @returns true if switch was successful - */ -export async function switchWorkflow(cwd: string, workflowName?: string): Promise { - // No workflow specified - show selection prompt - if (!workflowName) { - const current = getCurrentWorkflow(cwd); - info(`Current workflow: ${current}`); - - const categoryConfig = getWorkflowCategories(cwd); - let selected: string | null; - if (categoryConfig) { - const allWorkflows = loadAllWorkflowsWithSources(cwd); - if (allWorkflows.size === 0) { - info('No workflows found.'); - selected = null; - } else { - const categorized = buildCategorizedWorkflows(allWorkflows, categoryConfig); - warnMissingWorkflows(categorized.missingWorkflows); - selected = await selectWorkflowFromCategorizedWorkflows(categorized, current); - } - } else { - const entries = listWorkflowEntries(cwd); - selected = await selectWorkflowFromEntries(entries, current); - } - - if (!selected) { - info('Cancelled'); - return false; - } - - workflowName = selected; - } - - // Check if workflow exists - const config = loadWorkflow(workflowName, cwd); - - if (!config) { - error(`Workflow "${workflowName}" not found`); - return false; - } - - // Save to project config - setCurrentWorkflow(cwd, workflowName); - success(`Switched to workflow: ${workflowName}`); - - return true; -} diff --git a/src/features/interactive/index.ts b/src/features/interactive/index.ts index 4537ea1..51142e4 100644 --- a/src/features/interactive/index.ts +++ b/src/features/interactive/index.ts @@ -2,4 +2,4 @@ * Interactive mode commands. */ -export { interactiveMode, type WorkflowContext, type InteractiveModeResult } from './interactive.js'; +export { interactiveMode, type PieceContext, type InteractiveModeResult } from './interactive.js'; diff --git a/src/features/interactive/interactive.ts b/src/features/interactive/interactive.ts index 99730ca..e555f59 100644 --- a/src/features/interactive/interactive.ts +++ b/src/features/interactive/interactive.ts @@ -2,7 +2,7 @@ * Interactive task input mode * * Allows users to refine task requirements through conversation with AI - * before executing the task. Uses the same SDK call pattern as workflow + * before executing the task. Uses the same SDK call pattern as piece * execution (with onStream) to ensure compatibility. * * Commands: @@ -39,19 +39,19 @@ function resolveLanguage(lang?: Language): 'en' | 'ja' { return lang === 'ja' ? 'ja' : 'en'; } -function getInteractivePrompts(lang: 'en' | 'ja', workflowContext?: WorkflowContext) { - const hasWorkflow = !!workflowContext; +function getInteractivePrompts(lang: 'en' | 'ja', pieceContext?: PieceContext) { + const hasPiece = !!pieceContext; const systemPrompt = loadTemplate('score_interactive_system_prompt', lang, { - workflowInfo: hasWorkflow, - workflowName: workflowContext?.name ?? '', - workflowDescription: workflowContext?.description ?? '', + pieceInfo: hasPiece, + pieceName: pieceContext?.name ?? '', + pieceDescription: pieceContext?.description ?? '', }); return { systemPrompt, lang, - workflowContext, + pieceContext, conversationLabel: getLabel('interactive.conversationLabel', lang), noTranscript: getLabel('interactive.noTranscript', lang), ui: getLabelObject('interactive.ui', lang), @@ -89,7 +89,7 @@ function buildSummaryPrompt( lang: 'en' | 'ja', noTranscriptNote: string, conversationLabel: string, - workflowContext?: WorkflowContext, + pieceContext?: PieceContext, ): string { let conversation = ''; if (history.length > 0) { @@ -101,11 +101,11 @@ function buildSummaryPrompt( return ''; } - const hasWorkflow = !!workflowContext; + const hasPiece = !!pieceContext; return loadTemplate('score_summary_system_prompt', lang, { - workflowInfo: hasWorkflow, - workflowName: workflowContext?.name ?? '', - workflowDescription: workflowContext?.description ?? '', + pieceInfo: hasPiece, + pieceName: pieceContext?.name ?? '', + pieceDescription: pieceContext?.description ?? '', conversation, }); } @@ -155,7 +155,7 @@ function readLine(prompt: string): Promise { } /** - * Call AI with the same pattern as workflow execution. + * Call AI with the same pattern as piece execution. * The key requirement is passing onStream — the Agent SDK requires * includePartialMessages to be true for the async iterator to yield. */ @@ -189,10 +189,10 @@ export interface InteractiveModeResult { task: string; } -export interface WorkflowContext { - /** Workflow name (e.g. "minimal") */ +export interface PieceContext { + /** Piece name (e.g. "minimal") */ name: string; - /** Workflow description */ + /** Piece description */ description: string; } @@ -208,11 +208,11 @@ export interface WorkflowContext { export async function interactiveMode( cwd: string, initialInput?: string, - workflowContext?: WorkflowContext, + pieceContext?: PieceContext, ): Promise { const globalConfig = loadGlobalConfig(); const lang = resolveLanguage(globalConfig.language); - const prompts = getInteractivePrompts(lang, workflowContext); + const prompts = getInteractivePrompts(lang, pieceContext); if (!globalConfig.provider) { throw new Error('Provider is not configured.'); } @@ -318,7 +318,7 @@ export async function interactiveMode( prompts.lang, prompts.noTranscript, prompts.conversationLabel, - prompts.workflowContext, + prompts.pieceContext, ); if (!summaryPrompt) { info(prompts.ui.noConversation); diff --git a/src/features/workflowSelection/index.ts b/src/features/pieceSelection/index.ts similarity index 56% rename from src/features/workflowSelection/index.ts rename to src/features/pieceSelection/index.ts index 17c2774..5bb6359 100644 --- a/src/features/workflowSelection/index.ts +++ b/src/features/pieceSelection/index.ts @@ -1,29 +1,29 @@ /** - * Workflow selection helpers (UI layer). + * Piece selection helpers (UI layer). */ import { selectOption } from '../../shared/prompt/index.js'; import type { SelectOptionItem } from '../../shared/prompt/index.js'; import { info, warn } from '../../shared/ui/index.js'; import { - getBookmarkedWorkflows, + getBookmarkedPieces, addBookmark, removeBookmark, } from '../../infra/config/global/index.js'; import { - findWorkflowCategories, - type WorkflowDirEntry, - type WorkflowCategoryNode, - type CategorizedWorkflows, - type MissingWorkflow, - type WorkflowSource, - type WorkflowWithSource, + findPieceCategories, + type PieceDirEntry, + type PieceCategoryNode, + type CategorizedPieces, + type MissingPiece, + type PieceSource, + type PieceWithSource, } from '../../infra/config/index.js'; -/** Top-level selection item: either a workflow or a category containing workflows */ -export type WorkflowSelectionItem = - | { type: 'workflow'; name: string } - | { type: 'category'; name: string; workflows: string[] }; +/** Top-level selection item: either a piece or a category containing pieces */ +export type PieceSelectionItem = + | { type: 'piece'; name: string } + | { type: 'category'; name: string; pieces: string[] }; /** Option item for prompt UI */ export interface SelectionOption { @@ -32,28 +32,28 @@ export interface SelectionOption { } /** - * Build top-level selection items for the workflow chooser UI. - * Root-level workflows and categories are displayed at the same level. + * Build top-level selection items for the piece chooser UI. + * Root-level pieces and categories are displayed at the same level. */ -export function buildWorkflowSelectionItems(entries: WorkflowDirEntry[]): WorkflowSelectionItem[] { +export function buildPieceSelectionItems(entries: PieceDirEntry[]): PieceSelectionItem[] { const categories = new Map(); - const items: WorkflowSelectionItem[] = []; + const items: PieceSelectionItem[] = []; for (const entry of entries) { if (entry.category) { - let workflows = categories.get(entry.category); - if (!workflows) { - workflows = []; - categories.set(entry.category, workflows); + let pieces = categories.get(entry.category); + if (!pieces) { + pieces = []; + categories.set(entry.category, pieces); } - workflows.push(entry.name); + pieces.push(entry.name); } else { - items.push({ type: 'workflow', name: entry.name }); + items.push({ type: 'piece', name: entry.name }); } } - for (const [name, workflows] of categories) { - items.push({ type: 'category', name, workflows: workflows.sort() }); + for (const [name, pieces] of categories) { + items.push({ type: 'category', name, pieces: pieces.sort() }); } return items.sort((a, b) => a.name.localeCompare(b.name)); @@ -62,20 +62,20 @@ export function buildWorkflowSelectionItems(entries: WorkflowDirEntry[]): Workfl const CATEGORY_VALUE_PREFIX = '__category__:'; /** - * Build top-level select options from WorkflowSelectionItems. + * Build top-level select options from PieceSelectionItems. * Categories are encoded with a prefix in the value field. */ export function buildTopLevelSelectOptions( - items: WorkflowSelectionItem[], - currentWorkflow: string, + items: PieceSelectionItem[], + currentPiece: string, ): SelectionOption[] { return items.map((item) => { - if (item.type === 'workflow') { - const isCurrent = item.name === currentWorkflow; + if (item.type === 'piece') { + const isCurrent = item.name === currentPiece; const label = isCurrent ? `${item.name} (current)` : item.name; return { label, value: item.name }; } - const containsCurrent = item.workflows.some((w) => w === currentWorkflow); + const containsCurrent = item.pieces.some((w) => w === currentPiece); const label = containsCurrent ? `📁 ${item.name}/ (current)` : `📁 ${item.name}/`; return { label, value: `${CATEGORY_VALUE_PREFIX}${item.name}` }; }); @@ -83,7 +83,7 @@ export function buildTopLevelSelectOptions( /** * Parse a top-level selection result. - * Returns the category name if a category was selected, or null if a workflow was selected directly. + * Returns the category name if a category was selected, or null if a piece was selected directly. */ export function parseCategorySelection(selected: string): string | null { if (selected.startsWith(CATEGORY_VALUE_PREFIX)) { @@ -93,21 +93,21 @@ export function parseCategorySelection(selected: string): string | null { } /** - * Build select options for workflows within a category. + * Build select options for pieces within a category. */ -export function buildCategoryWorkflowOptions( - items: WorkflowSelectionItem[], +export function buildCategoryPieceOptions( + items: PieceSelectionItem[], categoryName: string, - currentWorkflow: string, + currentPiece: string, ): SelectionOption[] | null { const categoryItem = items.find( (item) => item.type === 'category' && item.name === categoryName, ); if (!categoryItem || categoryItem.type !== 'category') return null; - return categoryItem.workflows.map((qualifiedName) => { + return categoryItem.pieces.map((qualifiedName) => { const displayName = qualifiedName.split('/').pop() ?? qualifiedName; - const isCurrent = qualifiedName === currentWorkflow; + const isCurrent = qualifiedName === currentPiece; const label = isCurrent ? `${displayName} (current)` : displayName; return { label, value: qualifiedName }; }); @@ -121,9 +121,9 @@ const BOOKMARK_MARK = ' [*]'; */ export function applyBookmarks( options: SelectionOption[], - bookmarkedWorkflows: string[], + bookmarkedPieces: string[], ): SelectionOption[] { - const bookmarkedSet = new Set(bookmarkedWorkflows); + const bookmarkedSet = new Set(bookmarkedPieces); return options.map((opt) => { if (bookmarkedSet.has(opt.value)) { @@ -134,38 +134,38 @@ export function applyBookmarks( } /** - * Warn about missing workflows referenced by categories. + * Warn about missing pieces referenced by categories. */ -export function warnMissingWorkflows(missing: MissingWorkflow[]): void { - for (const { categoryPath, workflowName } of missing) { +export function warnMissingPieces(missing: MissingPiece[]): void { + for (const { categoryPath, pieceName } of missing) { const pathLabel = categoryPath.join(' / '); - warn(`Workflow "${workflowName}" in category "${pathLabel}" not found`); + warn(`Piece "${pieceName}" in category "${pathLabel}" not found`); } } -function categoryContainsWorkflow(node: WorkflowCategoryNode, workflow: string): boolean { - if (node.workflows.includes(workflow)) return true; +function categoryContainsPiece(node: PieceCategoryNode, piece: string): boolean { + if (node.pieces.includes(piece)) return true; for (const child of node.children) { - if (categoryContainsWorkflow(child, workflow)) return true; + if (categoryContainsPiece(child, piece)) return true; } return false; } function buildCategoryLevelOptions( - categories: WorkflowCategoryNode[], - workflows: string[], - currentWorkflow: string, - rootCategories: WorkflowCategoryNode[], + categories: PieceCategoryNode[], + pieces: string[], + currentPiece: string, + rootCategories: PieceCategoryNode[], currentPathLabel: string, ): { options: SelectionOption[]; - categoryMap: Map; + categoryMap: Map; } { const options: SelectionOption[] = []; - const categoryMap = new Map(); + const categoryMap = new Map(); for (const category of categories) { - const containsCurrent = currentWorkflow.length > 0 && categoryContainsWorkflow(category, currentWorkflow); + const containsCurrent = currentPiece.length > 0 && categoryContainsPiece(category, currentPiece); const label = containsCurrent ? `📁 ${category.name}/ (current)` : `📁 ${category.name}/`; @@ -174,57 +174,57 @@ function buildCategoryLevelOptions( categoryMap.set(category.name, category); } - for (const workflowName of workflows) { - const isCurrent = workflowName === currentWorkflow; - const alsoIn = findWorkflowCategories(workflowName, rootCategories) + for (const pieceName of pieces) { + const isCurrent = pieceName === currentPiece; + const alsoIn = findPieceCategories(pieceName, rootCategories) .filter((path) => path !== currentPathLabel); const alsoInLabel = alsoIn.length > 0 ? `also in ${alsoIn.join(', ')}` : ''; - let label = `🎼 ${workflowName}`; + let label = `🎼 ${pieceName}`; if (isCurrent && alsoInLabel) { - label = `🎼 ${workflowName} (current, ${alsoInLabel})`; + label = `🎼 ${pieceName} (current, ${alsoInLabel})`; } else if (isCurrent) { - label = `🎼 ${workflowName} (current)`; + label = `🎼 ${pieceName} (current)`; } else if (alsoInLabel) { - label = `🎼 ${workflowName} (${alsoInLabel})`; + label = `🎼 ${pieceName} (${alsoInLabel})`; } - options.push({ label, value: workflowName }); + options.push({ label, value: pieceName }); } return { options, categoryMap }; } -async function selectWorkflowFromCategoryTree( - categories: WorkflowCategoryNode[], - currentWorkflow: string, +async function selectPieceFromCategoryTree( + categories: PieceCategoryNode[], + currentPiece: string, hasSourceSelection: boolean, - rootWorkflows: string[] = [], + rootPieces: string[] = [], ): Promise { - if (categories.length === 0 && rootWorkflows.length === 0) { - info('No workflows available for configured categories.'); + if (categories.length === 0 && rootPieces.length === 0) { + info('No pieces available for configured categories.'); return null; } - const stack: WorkflowCategoryNode[] = []; + const stack: PieceCategoryNode[] = []; while (true) { const currentNode = stack.length > 0 ? stack[stack.length - 1] : undefined; const currentCategories = currentNode ? currentNode.children : categories; - const currentWorkflows = currentNode ? currentNode.workflows : rootWorkflows; + const currentPieces = currentNode ? currentNode.pieces : rootPieces; const currentPathLabel = stack.map((node) => node.name).join(' / '); const { options, categoryMap } = buildCategoryLevelOptions( currentCategories, - currentWorkflows, - currentWorkflow, + currentPieces, + currentPiece, categories, currentPathLabel, ); if (options.length === 0) { if (stack.length === 0) { - info('No workflows available for configured categories.'); + info('No pieces available for configured categories.'); return null; } stack.pop(); @@ -232,11 +232,11 @@ async function selectWorkflowFromCategoryTree( } const buildOptionsWithBookmarks = (): SelectionOption[] => - applyBookmarks(options, getBookmarkedWorkflows()); + applyBookmarks(options, getBookmarkedPieces()); const message = currentPathLabel.length > 0 - ? `Select workflow in ${currentPathLabel}:` - : 'Select workflow category:'; + ? `Select piece in ${currentPathLabel}:` + : 'Select piece category:'; const selected = await selectOption(message, buildOptionsWithBookmarks(), { cancelLabel: (stack.length > 0 || hasSourceSelection) ? '← Go back' : 'Cancel', @@ -280,16 +280,16 @@ async function selectWorkflowFromCategoryTree( } } -function countWorkflowsIncludingCategories( - categories: WorkflowCategoryNode[], - allWorkflows: Map, - sourceFilter: WorkflowSource, +function countPiecesIncludingCategories( + categories: PieceCategoryNode[], + allPieces: Map, + sourceFilter: PieceSource, ): number { - const categorizedWorkflows = new Set(); - const visit = (nodes: WorkflowCategoryNode[]): void => { + const categorizedPieces = new Set(); + const visit = (nodes: PieceCategoryNode[]): void => { for (const node of nodes) { - for (const w of node.workflows) { - categorizedWorkflows.add(w); + for (const w of node.pieces) { + categorizedPieces.add(w); } if (node.children.length > 0) { visit(node.children); @@ -299,7 +299,7 @@ function countWorkflowsIncludingCategories( visit(categories); let count = 0; - for (const [, { source }] of allWorkflows) { + for (const [, { source }] of allPieces) { if (source === sourceFilter) { count++; } @@ -307,51 +307,51 @@ function countWorkflowsIncludingCategories( return count; } -const CURRENT_WORKFLOW_VALUE = '__current__'; +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: 'workflow'; name: string } - | { type: 'custom_category'; node: WorkflowCategoryNode } + | { type: 'piece'; name: string } + | { type: 'custom_category'; node: PieceCategoryNode } | { type: 'custom_uncategorized' } | { type: 'builtin' }; -async function selectTopLevelWorkflowOption( - categorized: CategorizedWorkflows, - currentWorkflow: string, +async function selectTopLevelPieceOption( + categorized: CategorizedPieces, + currentPiece: string, ): Promise { - const uncategorizedCustom = getRootLevelWorkflows( + const uncategorizedCustom = getRootLevelPieces( categorized.categories, - categorized.allWorkflows, + categorized.allPieces, 'user' ); - const builtinCount = countWorkflowsIncludingCategories( + const builtinCount = countPiecesIncludingCategories( categorized.builtinCategories, - categorized.allWorkflows, + categorized.allPieces, 'builtin' ); const buildOptions = (): SelectOptionItem[] => { const options: SelectOptionItem[] = []; - const bookmarkedWorkflows = getBookmarkedWorkflows(); // Get fresh bookmarks on every build + const bookmarkedPieces = getBookmarkedPieces(); // Get fresh bookmarks on every build - // 1. Current workflow - if (currentWorkflow) { + // 1. Current piece + if (currentPiece) { options.push({ - label: `🎼 ${currentWorkflow} (current)`, - value: CURRENT_WORKFLOW_VALUE, + label: `🎼 ${currentPiece} (current)`, + value: CURRENT_PIECE_VALUE, }); } - // 2. Bookmarked workflows (individual items) - for (const workflowName of bookmarkedWorkflows) { - if (workflowName === currentWorkflow) continue; // Skip if already shown as current + // 2. Bookmarked pieces (individual items) + for (const pieceName of bookmarkedPieces) { + if (pieceName === currentPiece) continue; // Skip if already shown as current options.push({ - label: `🎼 ${workflowName} [*]`, - value: workflowName, + label: `🎼 ${pieceName} [*]`, + value: pieceName, }); } @@ -363,7 +363,7 @@ async function selectTopLevelWorkflowOption( }); } - // 4. Builtin workflows + // 4. Builtin pieces if (builtinCount > 0) { options.push({ label: `📂 Builtin/ (${builtinCount})`, @@ -371,7 +371,7 @@ async function selectTopLevelWorkflowOption( }); } - // 5. Uncategorized custom workflows + // 5. Uncategorized custom pieces if (uncategorizedCustom.length > 0) { options.push({ label: `📂 Custom/ (${uncategorizedCustom.length})`, @@ -384,10 +384,10 @@ async function selectTopLevelWorkflowOption( if (buildOptions().length === 0) return null; - const result = await selectOption('Select workflow:', buildOptions(), { + 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_WORKFLOW_VALUE || + if (value === CURRENT_PIECE_VALUE || value === CUSTOM_UNCATEGORIZED_VALUE || value === BUILTIN_SOURCE_VALUE || value.startsWith(CUSTOM_CATEGORY_PREFIX)) { @@ -410,7 +410,7 @@ async function selectTopLevelWorkflowOption( if (!result) return null; - if (result === CURRENT_WORKFLOW_VALUE) { + if (result === CURRENT_PIECE_VALUE) { return { type: 'current' }; } @@ -429,20 +429,20 @@ async function selectTopLevelWorkflowOption( return { type: 'custom_category', node }; } - // Direct workflow selection (bookmarked or other) - return { type: 'workflow', name: result }; + // Direct piece selection (bookmarked or other) + return { type: 'piece', name: result }; } -function getRootLevelWorkflows( - categories: WorkflowCategoryNode[], - allWorkflows: Map, - sourceFilter: WorkflowSource, +function getRootLevelPieces( + categories: PieceCategoryNode[], + allPieces: Map, + sourceFilter: PieceSource, ): string[] { - const categorizedWorkflows = new Set(); - const visit = (nodes: WorkflowCategoryNode[]): void => { + const categorizedPieces = new Set(); + const visit = (nodes: PieceCategoryNode[]): void => { for (const node of nodes) { - for (const w of node.workflows) { - categorizedWorkflows.add(w); + for (const w of node.pieces) { + categorizedPieces.add(w); } if (node.children.length > 0) { visit(node.children); @@ -451,91 +451,91 @@ function getRootLevelWorkflows( }; visit(categories); - const rootWorkflows: string[] = []; - for (const [name, { source }] of allWorkflows) { - if (source === sourceFilter && !categorizedWorkflows.has(name)) { - rootWorkflows.push(name); + const rootPieces: string[] = []; + for (const [name, { source }] of allPieces) { + if (source === sourceFilter && !categorizedPieces.has(name)) { + rootPieces.push(name); } } - return rootWorkflows.sort(); + return rootPieces.sort(); } /** - * Select workflow from categorized workflows (hierarchical UI). + * Select piece from categorized pieces (hierarchical UI). */ -export async function selectWorkflowFromCategorizedWorkflows( - categorized: CategorizedWorkflows, - currentWorkflow: string, +export async function selectPieceFromCategorizedPieces( + categorized: CategorizedPieces, + currentPiece: string, ): Promise { while (true) { - const selection = await selectTopLevelWorkflowOption(categorized, currentWorkflow); + const selection = await selectTopLevelPieceOption(categorized, currentPiece); if (!selection) { return null; } - // 1. Current workflow selected + // 1. Current piece selected if (selection.type === 'current') { - return currentWorkflow; + return currentPiece; } - // 2. Direct workflow selected (e.g., bookmarked workflow) - if (selection.type === 'workflow') { + // 2. Direct piece selected (e.g., bookmarked piece) + if (selection.type === 'piece') { return selection.name; } // 3. User-defined category selected if (selection.type === 'custom_category') { - const workflow = await selectWorkflowFromCategoryTree( + const piece = await selectPieceFromCategoryTree( [selection.node], - currentWorkflow, + currentPiece, true, - selection.node.workflows + selection.node.pieces ); - if (workflow) { - return workflow; + if (piece) { + return piece; } // null → go back to top-level selection continue; } - // 4. Builtin workflows selected + // 4. Builtin pieces selected if (selection.type === 'builtin') { - const rootWorkflows = getRootLevelWorkflows( + const rootPieces = getRootLevelPieces( categorized.builtinCategories, - categorized.allWorkflows, + categorized.allPieces, 'builtin' ); - const workflow = await selectWorkflowFromCategoryTree( + const piece = await selectPieceFromCategoryTree( categorized.builtinCategories, - currentWorkflow, + currentPiece, true, - rootWorkflows + rootPieces ); - if (workflow) { - return workflow; + if (piece) { + return piece; } // null → go back to top-level selection continue; } - // 5. Custom uncategorized workflows selected + // 5. Custom uncategorized pieces selected if (selection.type === 'custom_uncategorized') { - const uncategorizedCustom = getRootLevelWorkflows( + const uncategorizedCustom = getRootLevelPieces( categorized.categories, - categorized.allWorkflows, + categorized.allPieces, 'user' ); const baseOptions: SelectionOption[] = uncategorizedCustom.map((name) => ({ - label: name === currentWorkflow ? `🎼 ${name} (current)` : `🎼 ${name}`, + label: name === currentPiece ? `🎼 ${name} (current)` : `🎼 ${name}`, value: name, })); const buildFlatOptions = (): SelectionOption[] => - applyBookmarks(baseOptions, getBookmarkedWorkflows()); + applyBookmarks(baseOptions, getBookmarkedPieces()); - const workflow = await selectOption('Select workflow:', buildFlatOptions(), { + const piece = await selectOption('Select piece:', buildFlatOptions(), { cancelLabel: '← Go back', onKeyPress: (key: string, value: string): SelectOptionItem[] | null => { if (key === 'b') { @@ -550,8 +550,8 @@ export async function selectWorkflowFromCategorizedWorkflows( }, }); - if (workflow) { - return workflow; + if (piece) { + return piece; } // null → go back to top-level selection continue; @@ -559,26 +559,26 @@ export async function selectWorkflowFromCategorizedWorkflows( } } -async function selectWorkflowFromEntriesWithCategories( - entries: WorkflowDirEntry[], - currentWorkflow: string, +async function selectPieceFromEntriesWithCategories( + entries: PieceDirEntry[], + currentPiece: string, ): Promise { if (entries.length === 0) return null; - const items = buildWorkflowSelectionItems(entries); - const availableWorkflows = entries.map((entry) => entry.name); + const items = buildPieceSelectionItems(entries); + const availablePieces = entries.map((entry) => entry.name); const hasCategories = items.some((item) => item.type === 'category'); if (!hasCategories) { - const baseOptions: SelectionOption[] = availableWorkflows.map((name) => ({ - label: name === currentWorkflow ? `🎼 ${name} (current)` : `🎼 ${name}`, + const baseOptions: SelectionOption[] = availablePieces.map((name) => ({ + label: name === currentPiece ? `🎼 ${name} (current)` : `🎼 ${name}`, value: name, })); const buildFlatOptions = (): SelectionOption[] => - applyBookmarks(baseOptions, getBookmarkedWorkflows()); + applyBookmarks(baseOptions, getBookmarkedPieces()); - return selectOption('Select workflow:', buildFlatOptions(), { + return selectOption('Select piece:', buildFlatOptions(), { onKeyPress: (key: string, value: string): SelectOptionItem[] | null => { if (key === 'b') { addBookmark(value); @@ -593,12 +593,12 @@ async function selectWorkflowFromEntriesWithCategories( }); } - // Loop until user selects a workflow or cancels at top level + // Loop until user selects a piece or cancels at top level while (true) { const buildTopLevelOptions = (): SelectionOption[] => - applyBookmarks(buildTopLevelSelectOptions(items, currentWorkflow), getBookmarkedWorkflows()); + applyBookmarks(buildTopLevelSelectOptions(items, currentPiece), getBookmarkedPieces()); - const selected = await selectOption('Select workflow:', buildTopLevelOptions(), { + const selected = await selectOption('Select piece:', buildTopLevelOptions(), { onKeyPress: (key: string, value: string): SelectOptionItem[] | null => { // Don't handle bookmark keys for categories if (parseCategorySelection(value)) { @@ -622,13 +622,13 @@ async function selectWorkflowFromEntriesWithCategories( const categoryName = parseCategorySelection(selected); if (categoryName) { - const categoryOptions = buildCategoryWorkflowOptions(items, categoryName, currentWorkflow); + const categoryOptions = buildCategoryPieceOptions(items, categoryName, currentPiece); if (!categoryOptions) continue; const buildCategoryOptions = (): SelectionOption[] => - applyBookmarks(categoryOptions, getBookmarkedWorkflows()); + applyBookmarks(categoryOptions, getBookmarkedPieces()); - const workflowSelection = await selectOption(`Select workflow in ${categoryName}:`, buildCategoryOptions(), { + const pieceSelection = await selectOption(`Select piece in ${categoryName}:`, buildCategoryOptions(), { cancelLabel: '← Go back', onKeyPress: (key: string, value: string): SelectOptionItem[] | null => { if (key === 'b') { @@ -643,8 +643,8 @@ async function selectWorkflowFromEntriesWithCategories( }, }); - // If workflow selected, return it. If cancelled (null), go back to top level - if (workflowSelection) return workflowSelection; + // If piece selected, return it. If cancelled (null), go back to top level + if (pieceSelection) return pieceSelection; continue; } @@ -653,25 +653,25 @@ async function selectWorkflowFromEntriesWithCategories( } /** - * Select workflow from directory entries (builtin separated). + * Select piece from directory entries (builtin separated). */ -export async function selectWorkflowFromEntries( - entries: WorkflowDirEntry[], - currentWorkflow: string, +export async function selectPieceFromEntries( + entries: PieceDirEntry[], + currentPiece: string, ): Promise { const builtinEntries = entries.filter((entry) => entry.source === 'builtin'); const customEntries = entries.filter((entry) => entry.source !== 'builtin'); if (builtinEntries.length > 0 && customEntries.length > 0) { - const selectedSource = await selectOption<'custom' | 'builtin'>('Select workflow source:', [ - { label: `Custom workflows (${customEntries.length})`, value: 'custom' }, - { label: `Builtin workflows (${builtinEntries.length})`, value: 'builtin' }, + const selectedSource = await selectOption<'custom' | 'builtin'>('Select piece source:', [ + { label: `Custom pieces (${customEntries.length})`, value: 'custom' }, + { label: `Builtin pieces (${builtinEntries.length})`, value: 'builtin' }, ]); if (!selectedSource) return null; const sourceEntries = selectedSource === 'custom' ? customEntries : builtinEntries; - return selectWorkflowFromEntriesWithCategories(sourceEntries, currentWorkflow); + return selectPieceFromEntriesWithCategories(sourceEntries, currentPiece); } const entriesToUse = customEntries.length > 0 ? customEntries : builtinEntries; - return selectWorkflowFromEntriesWithCategories(entriesToUse, currentWorkflow); + return selectPieceFromEntriesWithCategories(entriesToUse, currentPiece); } diff --git a/src/features/pipeline/execute.ts b/src/features/pipeline/execute.ts index 4f56966..e9262fe 100644 --- a/src/features/pipeline/execute.ts +++ b/src/features/pipeline/execute.ts @@ -4,7 +4,7 @@ * Orchestrates the full pipeline: * 1. Fetch issue content * 2. Create branch - * 3. Run workflow + * 3. Run piece * 4. Commit & push * 5. Create PR */ @@ -27,7 +27,7 @@ import { createLogger, getErrorMessage } from '../../shared/utils/index.js'; import type { PipelineConfig } from '../../core/models/index.js'; import { EXIT_ISSUE_FETCH_FAILED, - EXIT_WORKFLOW_FAILED, + EXIT_PIECE_FAILED, EXIT_GIT_OPERATION_FAILED, EXIT_PR_CREATION_FAILED, } from '../../shared/exitCodes.js'; @@ -105,7 +105,7 @@ function buildPipelinePrBody( * Returns a process exit code (0 on success, 2-5 on specific failures). */ export async function executePipeline(options: PipelineExecutionOptions): Promise { - const { cwd, workflow, autoPr, skipGit } = options; + const { cwd, piece, autoPr, skipGit } = options; const globalConfig = loadGlobalConfig(); const pipelineConfig = globalConfig.pipeline; let issue: GitHubIssue | undefined; @@ -148,9 +148,9 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis } } - // --- Step 3: Run workflow --- - info(`Running workflow: ${workflow}`); - log.info('Pipeline workflow execution starting', { workflow, branch, skipGit, issueNumber: options.issueNumber }); + // --- Step 3: Run piece --- + info(`Running piece: ${piece}`); + log.info('Pipeline piece execution starting', { piece, branch, skipGit, issueNumber: options.issueNumber }); const agentOverrides: TaskExecutionOptions | undefined = (options.provider || options.model) ? { provider: options.provider, model: options.model } @@ -159,16 +159,16 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis const taskSuccess = await executeTask({ task, cwd, - workflowIdentifier: workflow, + pieceIdentifier: piece, projectCwd: cwd, agentOverrides, }); if (!taskSuccess) { - error(`Workflow '${workflow}' failed`); - return EXIT_WORKFLOW_FAILED; + error(`Piece '${piece}' failed`); + return EXIT_PIECE_FAILED; } - success(`Workflow '${workflow}' completed`); + success(`Piece '${piece}' completed`); // --- Step 4: Commit & push (skip if --skip-git) --- if (!skipGit && branch) { @@ -199,7 +199,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis } else if (branch) { info('Creating pull request...'); const prTitle = issue ? issue.title : (options.task ?? 'Pipeline task'); - const report = `Workflow \`${workflow}\` completed successfully.`; + const report = `Piece \`${piece}\` completed successfully.`; const prBody = buildPipelinePrBody(pipelineConfig, issue, report); const prResult = createPullRequest(cwd, { @@ -222,7 +222,7 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis blankLine(); status('Issue', issue ? `#${issue.number} "${issue.title}"` : 'N/A'); status('Branch', branch ?? '(current)'); - status('Workflow', workflow); + status('Piece', piece); status('Result', 'Success', 'green'); return 0; diff --git a/src/features/prompt/preview.ts b/src/features/prompt/preview.ts index 15d0dce..ec0c1f2 100644 --- a/src/features/prompt/preview.ts +++ b/src/features/prompt/preview.ts @@ -1,31 +1,31 @@ /** * Prompt preview feature * - * Loads a workflow and displays the assembled prompt for each movement and phase. + * Loads a piece and displays the assembled prompt for each movement and phase. * Useful for debugging and understanding what prompts agents will receive. */ -import { loadWorkflowByIdentifier, getCurrentWorkflow, loadGlobalConfig } from '../../infra/config/index.js'; -import { InstructionBuilder } from '../../core/workflow/instruction/InstructionBuilder.js'; -import { ReportInstructionBuilder } from '../../core/workflow/instruction/ReportInstructionBuilder.js'; -import { StatusJudgmentBuilder } from '../../core/workflow/instruction/StatusJudgmentBuilder.js'; -import { needsStatusJudgmentPhase } from '../../core/workflow/index.js'; -import type { InstructionContext } from '../../core/workflow/instruction/instruction-context.js'; +import { loadPieceByIdentifier, getCurrentPiece, loadGlobalConfig } from '../../infra/config/index.js'; +import { InstructionBuilder } from '../../core/piece/instruction/InstructionBuilder.js'; +import { ReportInstructionBuilder } from '../../core/piece/instruction/ReportInstructionBuilder.js'; +import { StatusJudgmentBuilder } from '../../core/piece/instruction/StatusJudgmentBuilder.js'; +import { needsStatusJudgmentPhase } from '../../core/piece/index.js'; +import type { InstructionContext } from '../../core/piece/instruction/instruction-context.js'; import type { Language } from '../../core/models/types.js'; import { header, info, error, blankLine } from '../../shared/ui/index.js'; /** - * Preview all prompts for a workflow. + * Preview all prompts for a piece. * - * Loads the workflow definition, then for each movement builds and displays + * Loads the piece definition, then for each movement builds and displays * the Phase 1, Phase 2, and Phase 3 prompts with sample variable values. */ -export async function previewPrompts(cwd: string, workflowIdentifier?: string): Promise { - const identifier = workflowIdentifier ?? getCurrentWorkflow(cwd); - const config = loadWorkflowByIdentifier(identifier, cwd); +export async function previewPrompts(cwd: string, pieceIdentifier?: string): Promise { + const identifier = pieceIdentifier ?? getCurrentPiece(cwd); + const config = loadPieceByIdentifier(identifier, cwd); if (!config) { - error(`Workflow "${identifier}" not found.`); + error(`Piece "${identifier}" not found.`); return; } @@ -53,7 +53,7 @@ export async function previewPrompts(cwd: string, workflowIdentifier?: string): cwd, projectCwd: cwd, userInputs: [], - workflowMovements: config.movements, + pieceMovements: config.movements, currentMovementIndex: i, reportDir: movement.report ? '.takt/reports/preview' : undefined, language, diff --git a/src/features/tasks/add/index.ts b/src/features/tasks/add/index.ts index cb4afbf..910d192 100644 --- a/src/features/tasks/add/index.ts +++ b/src/features/tasks/add/index.ts @@ -11,8 +11,8 @@ import { stringify as stringifyYaml } from 'yaml'; import { promptInput, confirm } from '../../../shared/prompt/index.js'; import { success, info } from '../../../shared/ui/index.js'; import { summarizeTaskName, type TaskFileData } from '../../../infra/task/index.js'; -import { getWorkflowDescription } from '../../../infra/config/index.js'; -import { determineWorkflow } from '../execute/selectAndExecute.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 { interactiveMode } from '../../interactive/index.js'; @@ -38,7 +38,7 @@ async function generateFilename(tasksDir: string, taskContent: string, cwd: stri * add command handler * * Flow: - * 1. ワークフロー選択 + * 1. ピース選択 * 2. AI対話モードでタスクを詰める * 3. 会話履歴からAIがタスク要約を生成 * 4. 要約からファイル名をAIで生成 @@ -49,10 +49,10 @@ export async function addTask(cwd: string, task?: string): Promise { const tasksDir = path.join(cwd, '.takt', 'tasks'); fs.mkdirSync(tasksDir, { recursive: true }); - // 1. ワークフロー選択(Issue参照以外の場合、対話モードの前に実施) + // 1. ピース選択(Issue参照以外の場合、対話モードの前に実施) let taskContent: string; let issueNumber: number | undefined; - let workflow: string | undefined; + let piece: string | undefined; if (task && isIssueReference(task)) { // Issue reference: fetch issue and use directly as task content @@ -70,18 +70,18 @@ export async function addTask(cwd: string, task?: string): Promise { return; } } else { - // ワークフロー選択を先に行い、結果を対話モードに渡す - const workflowId = await determineWorkflow(cwd); - if (workflowId === null) { + // ピース選択を先に行い、結果を対話モードに渡す + const pieceId = await determinePiece(cwd); + if (pieceId === null) { info('Cancelled.'); return; } - workflow = workflowId; + piece = pieceId; - const workflowContext = getWorkflowDescription(workflowId, cwd); + const pieceContext = getPieceDescription(pieceId, cwd); // Interactive mode: AI conversation to refine task - const result = await interactiveMode(cwd, undefined, workflowContext); + const result = await interactiveMode(cwd, undefined, pieceContext); if (!result.confirmed) { info('Cancelled.'); return; @@ -118,8 +118,8 @@ export async function addTask(cwd: string, task?: string): Promise { if (branch) { taskData.branch = branch; } - if (workflow) { - taskData.workflow = workflow; + if (piece) { + taskData.piece = piece; } if (issueNumber !== undefined) { taskData.issue = issueNumber; @@ -139,7 +139,7 @@ export async function addTask(cwd: string, task?: string): Promise { if (branch) { info(` Branch: ${branch}`); } - if (workflow) { - info(` Workflow: ${workflow}`); + if (piece) { + info(` Piece: ${piece}`); } } diff --git a/src/features/tasks/execute/workflowExecution.ts b/src/features/tasks/execute/pieceExecution.ts similarity index 76% rename from src/features/tasks/execute/workflowExecution.ts rename to src/features/tasks/execute/pieceExecution.ts index 6a4666c..7551c4c 100644 --- a/src/features/tasks/execute/workflowExecution.ts +++ b/src/features/tasks/execute/pieceExecution.ts @@ -1,14 +1,14 @@ /** - * Workflow execution logic + * Piece execution logic */ import { readFileSync } from 'node:fs'; -import { WorkflowEngine, type IterationLimitRequest, type UserInputRequest } from '../../../core/workflow/index.js'; -import type { WorkflowConfig } from '../../../core/models/index.js'; -import type { WorkflowExecutionResult, WorkflowExecutionOptions } from './types.js'; +import { PieceEngine, type IterationLimitRequest, type UserInputRequest } from '../../../core/piece/index.js'; +import type { PieceConfig } from '../../../core/models/index.js'; +import type { PieceExecutionResult, PieceExecutionOptions } from './types.js'; import { callAiJudge, detectRuleIndex, interruptAllQueries } from '../../../infra/claude/index.js'; -export type { WorkflowExecutionResult, WorkflowExecutionOptions }; +export type { PieceExecutionResult, PieceExecutionOptions }; import { loadAgentSessions, @@ -37,8 +37,8 @@ import { appendNdjsonLine, type NdjsonStepStart, type NdjsonStepComplete, - type NdjsonWorkflowComplete, - type NdjsonWorkflowAbort, + type NdjsonPieceComplete, + type NdjsonPieceAbort, type NdjsonPhaseStart, type NdjsonPhaseComplete, type NdjsonInteractiveStart, @@ -49,7 +49,7 @@ import { selectOption, promptInput } from '../../../shared/prompt/index.js'; import { EXIT_SIGINT } from '../../../shared/exitCodes.js'; import { getLabel } from '../../../shared/i18n/index.js'; -const log = createLogger('workflow'); +const log = createLogger('piece'); /** * Format elapsed time in human-readable format @@ -70,16 +70,16 @@ function formatElapsedTime(startTime: string, endTime: string): string { } /** - * Execute a workflow and handle all events + * Execute a piece and handle all events */ -export async function executeWorkflow( - workflowConfig: WorkflowConfig, +export async function executePiece( + pieceConfig: PieceConfig, task: string, cwd: string, - options: WorkflowExecutionOptions -): Promise { + options: PieceExecutionOptions +): Promise { const { - headerPrefix = 'Running Workflow:', + headerPrefix = 'Running Piece:', interactiveUserInput = false, } = options; @@ -89,16 +89,16 @@ export async function executeWorkflow( // Always continue from previous sessions (use /clear to reset) log.debug('Continuing session (use /clear to reset)'); - header(`${headerPrefix} ${workflowConfig.name}`); + header(`${headerPrefix} ${pieceConfig.name}`); - const workflowSessionId = generateSessionId(); - let sessionLog = createSessionLog(task, projectCwd, workflowConfig.name); + const pieceSessionId = generateSessionId(); + let sessionLog = createSessionLog(task, projectCwd, pieceConfig.name); - // Initialize NDJSON log file + pointer at workflow start - const ndjsonLogPath = initNdjsonLog(workflowSessionId, task, workflowConfig.name, projectCwd); - updateLatestPointer(sessionLog, workflowSessionId, projectCwd, { copyToPrevious: true }); + // Initialize NDJSON log file + pointer at piece start + const ndjsonLogPath = initNdjsonLog(pieceSessionId, task, pieceConfig.name, projectCwd); + updateLatestPointer(sessionLog, pieceSessionId, projectCwd, { copyToPrevious: true }); - // Write interactive mode records if interactive mode was used before this workflow + // Write interactive mode records if interactive mode was used before this piece if (options.interactiveMetadata) { const startRecord: NdjsonInteractiveStart = { type: 'interactive_start', @@ -154,20 +154,20 @@ export async function executeWorkflow( blankLine(); warn( - getLabel('workflow.iterationLimit.maxReached', undefined, { + getLabel('piece.iterationLimit.maxReached', undefined, { currentIteration: String(request.currentIteration), maxIterations: String(request.maxIterations), }) ); - info(getLabel('workflow.iterationLimit.currentMovement', undefined, { currentMovement: request.currentMovement })); + info(getLabel('piece.iterationLimit.currentMovement', undefined, { currentMovement: request.currentMovement })); - const action = await selectOption(getLabel('workflow.iterationLimit.continueQuestion'), [ + const action = await selectOption(getLabel('piece.iterationLimit.continueQuestion'), [ { - label: getLabel('workflow.iterationLimit.continueLabel'), + label: getLabel('piece.iterationLimit.continueLabel'), value: 'continue', - description: getLabel('workflow.iterationLimit.continueDescription'), + description: getLabel('piece.iterationLimit.continueDescription'), }, - { label: getLabel('workflow.iterationLimit.stopLabel'), value: 'stop' }, + { label: getLabel('piece.iterationLimit.stopLabel'), value: 'stop' }, ]); if (action !== 'continue') { @@ -175,18 +175,18 @@ export async function executeWorkflow( } while (true) { - const input = await promptInput(getLabel('workflow.iterationLimit.inputPrompt')); + const input = await promptInput(getLabel('piece.iterationLimit.inputPrompt')); if (!input) { return null; } const additionalIterations = Number.parseInt(input, 10); if (Number.isInteger(additionalIterations) && additionalIterations > 0) { - workflowConfig.maxIterations += additionalIterations; + pieceConfig.maxIterations += additionalIterations; return additionalIterations; } - warn(getLabel('workflow.iterationLimit.invalidInput')); + warn(getLabel('piece.iterationLimit.invalidInput')); } }; @@ -198,12 +198,12 @@ export async function executeWorkflow( } blankLine(); info(request.prompt.trim()); - const input = await promptInput(getLabel('workflow.iterationLimit.userInputPrompt')); + const input = await promptInput(getLabel('piece.iterationLimit.userInputPrompt')); return input && input.trim() ? input.trim() : null; } : undefined; - const engine = new WorkflowEngine(workflowConfig, cwd, task, { + const engine = new PieceEngine(pieceConfig, cwd, task, { onStream: streamHandler, onUserInput, initialSessions: savedSessions, @@ -250,7 +250,7 @@ export async function executeWorkflow( engine.on('movement:start', (step, iteration, instruction) => { log.debug('Movement starting', { step: step.name, agent: step.agentDisplayName, iteration }); - info(`[${iteration}/${workflowConfig.maxIterations}] ${step.name} (${step.agentDisplayName})`); + info(`[${iteration}/${pieceConfig.maxIterations}] ${step.name} (${step.agentDisplayName})`); // Log prompt content for debugging if (instruction) { @@ -326,7 +326,7 @@ export async function executeWorkflow( // Update in-memory log for pointer metadata (immutable) sessionLog = { ...sessionLog, iterations: sessionLog.iterations + 1 }; - updateLatestPointer(sessionLog, workflowSessionId, projectCwd); + updateLatestPointer(sessionLog, pieceSessionId, projectCwd); }); engine.on('movement:report', (_step, filePath, fileName) => { @@ -335,32 +335,32 @@ export async function executeWorkflow( console.log(content); }); - engine.on('workflow:complete', (state) => { - log.info('Workflow completed successfully', { iterations: state.iteration }); + engine.on('piece:complete', (state) => { + log.info('Piece completed successfully', { iterations: state.iteration }); sessionLog = finalizeSessionLog(sessionLog, 'completed'); - // Write workflow_complete record to NDJSON log - const record: NdjsonWorkflowComplete = { - type: 'workflow_complete', + // Write piece_complete record to NDJSON log + const record: NdjsonPieceComplete = { + type: 'piece_complete', iterations: state.iteration, endTime: new Date().toISOString(), }; appendNdjsonLine(ndjsonLogPath, record); - updateLatestPointer(sessionLog, workflowSessionId, projectCwd); + updateLatestPointer(sessionLog, pieceSessionId, projectCwd); const elapsed = sessionLog.endTime ? formatElapsedTime(sessionLog.startTime, sessionLog.endTime) : ''; const elapsedDisplay = elapsed ? `, ${elapsed}` : ''; - success(`Workflow completed (${state.iteration} iterations${elapsedDisplay})`); + success(`Piece completed (${state.iteration} iterations${elapsedDisplay})`); info(`Session log: ${ndjsonLogPath}`); - notifySuccess('TAKT', getLabel('workflow.notifyComplete', undefined, { iteration: String(state.iteration) })); + notifySuccess('TAKT', getLabel('piece.notifyComplete', undefined, { iteration: String(state.iteration) })); }); - engine.on('workflow:abort', (state, reason) => { + engine.on('piece:abort', (state, reason) => { interruptAllQueries(); - log.error('Workflow aborted', { reason, iterations: state.iteration }); + log.error('Piece aborted', { reason, iterations: state.iteration }); if (displayRef.current) { displayRef.current.flush(); displayRef.current = null; @@ -368,24 +368,24 @@ export async function executeWorkflow( abortReason = reason; sessionLog = finalizeSessionLog(sessionLog, 'aborted'); - // Write workflow_abort record to NDJSON log - const record: NdjsonWorkflowAbort = { - type: 'workflow_abort', + // Write piece_abort record to NDJSON log + const record: NdjsonPieceAbort = { + type: 'piece_abort', iterations: state.iteration, reason, endTime: new Date().toISOString(), }; appendNdjsonLine(ndjsonLogPath, record); - updateLatestPointer(sessionLog, workflowSessionId, projectCwd); + updateLatestPointer(sessionLog, pieceSessionId, projectCwd); const elapsed = sessionLog.endTime ? formatElapsedTime(sessionLog.startTime, sessionLog.endTime) : ''; const elapsedDisplay = elapsed ? ` (${elapsed})` : ''; - error(`Workflow aborted after ${state.iteration} iterations${elapsedDisplay}: ${reason}`); + error(`Piece aborted after ${state.iteration} iterations${elapsedDisplay}: ${reason}`); info(`Session log: ${ndjsonLogPath}`); - notifyError('TAKT', getLabel('workflow.notifyAbort', undefined, { reason })); + notifyError('TAKT', getLabel('piece.notifyAbort', undefined, { reason })); }); // SIGINT handler: 1st Ctrl+C = graceful abort, 2nd = force exit @@ -394,11 +394,11 @@ export async function executeWorkflow( sigintCount++; if (sigintCount === 1) { blankLine(); - warn(getLabel('workflow.sigintGraceful')); + warn(getLabel('piece.sigintGraceful')); engine.abort(); } else { blankLine(); - error(getLabel('workflow.sigintForce')); + error(getLabel('piece.sigintForce')); process.exit(EXIT_SIGINT); } }; diff --git a/src/features/tasks/execute/selectAndExecute.ts b/src/features/tasks/execute/selectAndExecute.ts index 274dc61..bfa039c 100644 --- a/src/features/tasks/execute/selectAndExecute.ts +++ b/src/features/tasks/execute/selectAndExecute.ts @@ -1,99 +1,99 @@ /** * Task execution orchestration. * - * Coordinates workflow selection, worktree creation, task execution, + * Coordinates piece selection, worktree creation, task execution, * auto-commit, and PR creation. Extracted from cli.ts to avoid * mixing CLI parsing with business logic. */ import { - getCurrentWorkflow, - listWorkflows, - listWorkflowEntries, - isWorkflowPath, - loadAllWorkflowsWithSources, - getWorkflowCategories, - buildCategorizedWorkflows, + getCurrentPiece, + listPieces, + listPieceEntries, + isPiecePath, + loadAllPiecesWithSources, + getPieceCategories, + buildCategorizedPieces, } from '../../../infra/config/index.js'; import { confirm } from '../../../shared/prompt/index.js'; import { createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js'; +import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js'; import { info, error, success } from '../../../shared/ui/index.js'; import { createLogger } from '../../../shared/utils/index.js'; import { createPullRequest, buildPrBody } from '../../../infra/github/index.js'; import { executeTask } from './taskExecution.js'; import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js'; import { - warnMissingWorkflows, - selectWorkflowFromCategorizedWorkflows, - selectWorkflowFromEntries, -} from '../../workflowSelection/index.js'; + warnMissingPieces, + selectPieceFromCategorizedPieces, + selectPieceFromEntries, +} from '../../pieceSelection/index.js'; export type { WorktreeConfirmationResult, SelectAndExecuteOptions }; const log = createLogger('selectAndExecute'); /** - * Select a workflow interactively with directory categories and bookmarks. + * Select a piece interactively with directory categories and bookmarks. */ -async function selectWorkflowWithDirectoryCategories(cwd: string): Promise { - const availableWorkflows = listWorkflows(cwd); - const currentWorkflow = getCurrentWorkflow(cwd); +async function selectPieceWithDirectoryCategories(cwd: string): Promise { + const availablePieces = listPieces(cwd); + const currentPiece = getCurrentPiece(cwd); - if (availableWorkflows.length === 0) { - info(`No workflows found. Using default: ${DEFAULT_WORKFLOW_NAME}`); - return DEFAULT_WORKFLOW_NAME; + if (availablePieces.length === 0) { + info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`); + return DEFAULT_PIECE_NAME; } - if (availableWorkflows.length === 1 && availableWorkflows[0]) { - return availableWorkflows[0]; + if (availablePieces.length === 1 && availablePieces[0]) { + return availablePieces[0]; } - const entries = listWorkflowEntries(cwd); - return selectWorkflowFromEntries(entries, currentWorkflow); + const entries = listPieceEntries(cwd); + return selectPieceFromEntries(entries, currentPiece); } /** - * Select a workflow interactively with 2-stage category support. + * Select a piece interactively with 2-stage category support. */ -async function selectWorkflow(cwd: string): Promise { - const categoryConfig = getWorkflowCategories(cwd); +async function selectPiece(cwd: string): Promise { + const categoryConfig = getPieceCategories(cwd); if (categoryConfig) { - const current = getCurrentWorkflow(cwd); - const allWorkflows = loadAllWorkflowsWithSources(cwd); - if (allWorkflows.size === 0) { - info(`No workflows found. Using default: ${DEFAULT_WORKFLOW_NAME}`); - return DEFAULT_WORKFLOW_NAME; + const current = getCurrentPiece(cwd); + const allPieces = loadAllPiecesWithSources(cwd); + if (allPieces.size === 0) { + info(`No pieces found. Using default: ${DEFAULT_PIECE_NAME}`); + return DEFAULT_PIECE_NAME; } - const categorized = buildCategorizedWorkflows(allWorkflows, categoryConfig); - warnMissingWorkflows(categorized.missingWorkflows); - return selectWorkflowFromCategorizedWorkflows(categorized, current); + const categorized = buildCategorizedPieces(allPieces, categoryConfig); + warnMissingPieces(categorized.missingPieces); + return selectPieceFromCategorizedPieces(categorized, current); } - return selectWorkflowWithDirectoryCategories(cwd); + return selectPieceWithDirectoryCategories(cwd); } /** - * Determine workflow to use. + * Determine piece to use. * - * - If override looks like a path (isWorkflowPath), return it directly (validation is done at load time). - * - If override is a name, validate it exists in available workflows. + * - If override looks like a path (isPiecePath), return it directly (validation is done at load time). + * - If override is a name, validate it exists in available pieces. * - If no override, prompt user to select interactively. */ -export async function determineWorkflow(cwd: string, override?: string): Promise { +export async function determinePiece(cwd: string, override?: string): Promise { if (override) { - if (isWorkflowPath(override)) { + if (isPiecePath(override)) { return override; } - const availableWorkflows = listWorkflows(cwd); - const knownWorkflows = availableWorkflows.length === 0 ? [DEFAULT_WORKFLOW_NAME] : availableWorkflows; - if (!knownWorkflows.includes(override)) { - error(`Workflow not found: ${override}`); + const availablePieces = listPieces(cwd); + const knownPieces = availablePieces.length === 0 ? [DEFAULT_PIECE_NAME] : availablePieces; + if (!knownPieces.includes(override)) { + error(`Piece not found: ${override}`); return null; } return override; } - return selectWorkflow(cwd); + return selectPiece(cwd); } export async function confirmAndCreateWorktree( @@ -123,7 +123,7 @@ export async function confirmAndCreateWorktree( } /** - * Execute a task with workflow selection, optional worktree, and auto-commit. + * Execute a task with piece selection, optional worktree, and auto-commit. * Shared by direct task execution and interactive mode. */ export async function selectAndExecuteTask( @@ -132,9 +132,9 @@ export async function selectAndExecuteTask( options?: SelectAndExecuteOptions, agentOverrides?: TaskExecutionOptions, ): Promise { - const workflowIdentifier = await determineWorkflow(cwd, options?.workflow); + const pieceIdentifier = await determinePiece(cwd, options?.piece); - if (workflowIdentifier === null) { + if (pieceIdentifier === null) { info('Cancelled'); return; } @@ -145,11 +145,11 @@ export async function selectAndExecuteTask( options?.createWorktree, ); - log.info('Starting task execution', { workflow: workflowIdentifier, worktree: isWorktree }); + log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree }); const taskSuccess = await executeTask({ task, cwd: execCwd, - workflowIdentifier, + pieceIdentifier, projectCwd: cwd, agentOverrides, interactiveUserInput: options?.interactiveUserInput === true, @@ -168,7 +168,7 @@ export async function selectAndExecuteTask( const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false); if (shouldCreatePr) { info('Creating pull request...'); - const prBody = buildPrBody(undefined, `Workflow \`${workflowIdentifier}\` completed successfully.`); + const prBody = buildPrBody(undefined, `Piece \`${pieceIdentifier}\` completed successfully.`); const prResult = createPullRequest(execCwd, { branch, title: task.length > 100 ? `${task.slice(0, 97)}...` : task, diff --git a/src/features/tasks/execute/taskExecution.ts b/src/features/tasks/execute/taskExecution.ts index e842dc1..312b8cf 100644 --- a/src/features/tasks/execute/taskExecution.ts +++ b/src/features/tasks/execute/taskExecution.ts @@ -2,7 +2,7 @@ * Task execution logic */ -import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../../infra/config/index.js'; +import { loadPieceByIdentifier, isPiecePath, loadGlobalConfig } from '../../../infra/config/index.js'; import { TaskRunner, type TaskInfo, createSharedClone, autoCommitAndPush, summarizeTaskName } from '../../../infra/task/index.js'; import { header, @@ -13,8 +13,8 @@ import { blankLine, } from '../../../shared/ui/index.js'; import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; -import { executeWorkflow } from './workflowExecution.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js'; +import { executePiece } from './pieceExecution.js'; +import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js'; import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js'; export type { TaskExecutionOptions, ExecuteTaskOptions }; @@ -22,30 +22,30 @@ export type { TaskExecutionOptions, ExecuteTaskOptions }; const log = createLogger('task'); /** - * Execute a single task with workflow. + * Execute a single task with piece. */ export async function executeTask(options: ExecuteTaskOptions): Promise { - const { task, cwd, workflowIdentifier, projectCwd, agentOverrides, interactiveUserInput, interactiveMetadata } = options; - const workflowConfig = loadWorkflowByIdentifier(workflowIdentifier, projectCwd); + const { task, cwd, pieceIdentifier, projectCwd, agentOverrides, interactiveUserInput, interactiveMetadata } = options; + const pieceConfig = loadPieceByIdentifier(pieceIdentifier, projectCwd); - if (!workflowConfig) { - if (isWorkflowPath(workflowIdentifier)) { - error(`Workflow file not found: ${workflowIdentifier}`); + if (!pieceConfig) { + if (isPiecePath(pieceIdentifier)) { + error(`Piece file not found: ${pieceIdentifier}`); } else { - error(`Workflow "${workflowIdentifier}" not found.`); - info('Available workflows are in ~/.takt/pieces/ or .takt/workflows/'); - info('Use "takt switch" to select a workflow.'); + error(`Piece "${pieceIdentifier}" not found.`); + info('Available pieces are in ~/.takt/pieces/ or .takt/pieces/'); + info('Use "takt switch" to select a piece.'); } return false; } - log.debug('Running workflow', { - name: workflowConfig.name, - movements: workflowConfig.movements.map((s: { name: string }) => s.name), + log.debug('Running piece', { + name: pieceConfig.name, + movements: pieceConfig.movements.map((s: { name: string }) => s.name), }); const globalConfig = loadGlobalConfig(); - const result = await executeWorkflow(workflowConfig, task, cwd, { + const result = await executePiece(pieceConfig, task, cwd, { projectCwd, language: globalConfig.language, provider: agentOverrides?.provider, @@ -57,7 +57,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise } /** - * Execute a task: resolve clone → run workflow → auto-commit+push → remove clone → record completion. + * Execute a task: resolve clone → run piece → auto-commit+push → remove clone → record completion. * * Shared by runAllTasks() and watchTasks() to avoid duplicated * resolve → execute → autoCommit → complete logic. @@ -68,20 +68,20 @@ export async function executeAndCompleteTask( task: TaskInfo, taskRunner: TaskRunner, cwd: string, - workflowName: string, + pieceName: string, options?: TaskExecutionOptions, ): Promise { const startedAt = new Date().toISOString(); const executionLog: string[] = []; try { - const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName); + const { execCwd, execPiece, isWorktree } = await resolveTaskExecution(task, cwd, pieceName); // cwd is always the project root; pass it as projectCwd so reports/sessions go there const taskSuccess = await executeTask({ task: task.content, cwd: execCwd, - workflowIdentifier: execWorkflow, + pieceIdentifier: execPiece, projectCwd: cwd, agentOverrides: options, }); @@ -139,7 +139,7 @@ export async function executeAndCompleteTask( */ export async function runAllTasks( cwd: string, - workflowName: string = DEFAULT_WORKFLOW_NAME, + pieceName: string = DEFAULT_PIECE_NAME, options?: TaskExecutionOptions, ): Promise { const taskRunner = new TaskRunner(cwd); @@ -163,7 +163,7 @@ export async function runAllTasks( info(`=== Task: ${task.name} ===`); blankLine(); - const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options); + const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, pieceName, options); if (taskSuccess) { successCount++; @@ -186,20 +186,20 @@ export async function runAllTasks( } /** - * Resolve execution directory and workflow from task data. + * Resolve execution directory and piece from task data. * If the task has worktree settings, create a shared clone and use it as cwd. * Task name is summarized to English by AI for use in branch/clone names. */ export async function resolveTaskExecution( task: TaskInfo, defaultCwd: string, - defaultWorkflow: string -): Promise<{ execCwd: string; execWorkflow: string; isWorktree: boolean; branch?: string }> { + defaultPiece: string +): Promise<{ execCwd: string; execPiece: string; isWorktree: boolean; branch?: string }> { const data = task.data; // No structured data: use defaults if (!data) { - return { execCwd: defaultCwd, execWorkflow: defaultWorkflow, isWorktree: false }; + return { execCwd: defaultCwd, execPiece: defaultPiece, isWorktree: false }; } let execCwd = defaultCwd; @@ -224,8 +224,8 @@ export async function resolveTaskExecution( info(`Clone created: ${result.path} (branch: ${result.branch})`); } - // Handle workflow override - const execWorkflow = data.workflow || defaultWorkflow; + // Handle piece override + const execPiece = data.piece || defaultPiece; - return { execCwd, execWorkflow, isWorktree, branch }; + return { execCwd, execPiece, isWorktree, branch }; } diff --git a/src/features/tasks/execute/types.ts b/src/features/tasks/execute/types.ts index d2c4f98..63ab06b 100644 --- a/src/features/tasks/execute/types.ts +++ b/src/features/tasks/execute/types.ts @@ -5,8 +5,8 @@ import type { Language } from '../../../core/models/index.js'; import type { ProviderType } from '../../../infra/providers/index.js'; -/** Result of workflow execution */ -export interface WorkflowExecutionResult { +/** Result of piece execution */ +export interface PieceExecutionResult { success: boolean; reason?: string; } @@ -19,8 +19,8 @@ export interface InteractiveMetadata { task?: string; } -/** Options for workflow execution */ -export interface WorkflowExecutionOptions { +/** Options for piece execution */ +export interface PieceExecutionOptions { /** Header prefix for display */ headerPrefix?: string; /** Project root directory (where .takt/ lives). */ @@ -45,8 +45,8 @@ export interface ExecuteTaskOptions { task: string; /** Working directory (may be a clone path) */ cwd: string; - /** Workflow name or path (auto-detected by isWorkflowPath) */ - workflowIdentifier: string; + /** Piece name or path (auto-detected by isPiecePath) */ + pieceIdentifier: string; /** Project root (where .takt/ lives) */ projectCwd: string; /** Agent provider/model overrides */ @@ -62,15 +62,15 @@ export interface PipelineExecutionOptions { issueNumber?: number; /** Task content (alternative to issue) */ task?: string; - /** Workflow name or path to workflow file */ - workflow: string; + /** Piece name or path to piece file */ + piece: string; /** Branch name (auto-generated if omitted) */ branch?: string; /** Whether to create a PR after successful execution */ autoPr: boolean; /** Repository in owner/repo format */ repo?: string; - /** Skip branch creation, commit, and push (workflow-only execution) */ + /** Skip branch creation, commit, and push (piece-only execution) */ skipGit?: boolean; /** Working directory */ cwd: string; @@ -87,7 +87,7 @@ export interface WorktreeConfirmationResult { export interface SelectAndExecuteOptions { autoPr?: boolean; repo?: string; - workflow?: string; + piece?: string; createWorktree?: boolean | undefined; /** Enable interactive user input during step transitions */ interactiveUserInput?: boolean; diff --git a/src/features/tasks/index.ts b/src/features/tasks/index.ts index 0aa0f78..04b8805 100644 --- a/src/features/tasks/index.ts +++ b/src/features/tasks/index.ts @@ -2,7 +2,7 @@ * Task feature exports */ -export { executeWorkflow, type WorkflowExecutionResult, type WorkflowExecutionOptions } from './execute/workflowExecution.js'; +export { executePiece, type PieceExecutionResult, type PieceExecutionOptions } from './execute/pieceExecution.js'; export { executeTask, runAllTasks, type TaskExecutionOptions } from './execute/taskExecution.js'; export { executeAndCompleteTask, resolveTaskExecution } from './execute/taskExecution.js'; export { withAgentSession } from './execute/session.js'; @@ -10,7 +10,7 @@ export type { PipelineExecutionOptions } from './execute/types.js'; export { selectAndExecuteTask, confirmAndCreateWorktree, - determineWorkflow, + determinePiece, type SelectAndExecuteOptions, type WorktreeConfirmationResult, } from './execute/selectAndExecute.js'; diff --git a/src/features/tasks/list/taskActions.ts b/src/features/tasks/list/taskActions.ts index f78df0c..617fa64 100644 --- a/src/features/tasks/list/taskActions.ts +++ b/src/features/tasks/list/taskActions.ts @@ -25,8 +25,8 @@ import { info, success, error as logError, warn, header, blankLine } from '../.. import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; import { executeTask } from '../execute/taskExecution.js'; import type { TaskExecutionOptions } from '../execute/types.js'; -import { listWorkflows, getCurrentWorkflow } from '../../../infra/config/index.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js'; +import { listPieces, getCurrentPiece } from '../../../infra/config/index.js'; +import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js'; import { encodeWorktreePath } from '../../../infra/config/project/sessionStore.js'; const log = createLogger('list-tasks'); @@ -229,26 +229,26 @@ export function deleteBranch(projectDir: string, item: BranchListItem): boolean } /** - * Get the workflow to use for instruction. + * Get the piece to use for instruction. */ -async function selectWorkflowForInstruction(projectDir: string): Promise { - const availableWorkflows = listWorkflows(projectDir); - const currentWorkflow = getCurrentWorkflow(projectDir); +async function selectPieceForInstruction(projectDir: string): Promise { + const availablePieces = listPieces(projectDir); + const currentPiece = getCurrentPiece(projectDir); - if (availableWorkflows.length === 0) { - return DEFAULT_WORKFLOW_NAME; + if (availablePieces.length === 0) { + return DEFAULT_PIECE_NAME; } - if (availableWorkflows.length === 1 && availableWorkflows[0]) { - return availableWorkflows[0]; + if (availablePieces.length === 1 && availablePieces[0]) { + return availablePieces[0]; } - const options = availableWorkflows.map((name) => ({ - label: name === currentWorkflow ? `${name} (current)` : name, + const options = availablePieces.map((name) => ({ + label: name === currentPiece ? `${name} (current)` : name, value: name, })); - return await selectOption('Select workflow:', options); + return await selectOption('Select piece:', options); } /** @@ -309,13 +309,13 @@ export async function instructBranch( return false; } - const selectedWorkflow = await selectWorkflowForInstruction(projectDir); - if (!selectedWorkflow) { + const selectedPiece = await selectPieceForInstruction(projectDir); + if (!selectedPiece) { info('Cancelled'); return false; } - log.info('Instructing branch via temp clone', { branch, workflow: selectedWorkflow }); + log.info('Instructing branch via temp clone', { branch, piece: selectedPiece }); info(`Running instruction on ${branch}...`); const clone = createTempCloneForBranch(projectDir, branch); @@ -329,7 +329,7 @@ export async function instructBranch( const taskSuccess = await executeTask({ task: fullInstruction, cwd: clone.path, - workflowIdentifier: selectedWorkflow, + pieceIdentifier: selectedPiece, projectCwd: projectDir, agentOverrides: options, }); diff --git a/src/features/tasks/watch/index.ts b/src/features/tasks/watch/index.ts index 8d68d67..dd57dd2 100644 --- a/src/features/tasks/watch/index.ts +++ b/src/features/tasks/watch/index.ts @@ -6,7 +6,7 @@ */ import { TaskRunner, type TaskInfo, TaskWatcher } from '../../../infra/task/index.js'; -import { getCurrentWorkflow } from '../../../infra/config/index.js'; +import { getCurrentPiece } from '../../../infra/config/index.js'; import { header, info, @@ -15,7 +15,7 @@ import { blankLine, } from '../../../shared/ui/index.js'; import { executeAndCompleteTask } from '../execute/taskExecution.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../../shared/constants.js'; +import { DEFAULT_PIECE_NAME } from '../../../shared/constants.js'; import type { TaskExecutionOptions } from '../execute/types.js'; /** @@ -23,7 +23,7 @@ import type { TaskExecutionOptions } from '../execute/types.js'; * Runs until Ctrl+C. */ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): Promise { - const workflowName = getCurrentWorkflow(cwd) || DEFAULT_WORKFLOW_NAME; + const pieceName = getCurrentPiece(cwd) || DEFAULT_PIECE_NAME; const taskRunner = new TaskRunner(cwd); const watcher = new TaskWatcher(cwd); @@ -32,7 +32,7 @@ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): P let failCount = 0; header('TAKT Watch Mode'); - info(`Workflow: ${workflowName}`); + info(`Piece: ${pieceName}`); info(`Watching: ${taskRunner.getTasksDir()}`); info('Waiting for tasks... (Ctrl+C to stop)'); blankLine(); @@ -52,7 +52,7 @@ export async function watchTasks(cwd: string, options?: TaskExecutionOptions): P info(`=== Task ${taskCount}: ${task.name} ===`); blankLine(); - const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options); + const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, pieceName, options); if (taskSuccess) { successCount++; diff --git a/src/index.ts b/src/index.ts index df01220..7b7e9e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,8 +15,8 @@ export { loadProjectConfig, saveProjectConfig, updateProjectConfig, - getCurrentWorkflow, - setCurrentWorkflow, + getCurrentPiece, + setCurrentPiece, isVerboseMode, type ProjectLocalConfig, writeFileAtomic, @@ -98,9 +98,9 @@ export * from './infra/codex/index.js'; // Agent execution export * from './agents/index.js'; -// Workflow engine +// Piece engine export { - WorkflowEngine, + PieceEngine, COMPLETE_MOVEMENT, ABORT_MOVEMENT, ERROR_MESSAGES, @@ -124,14 +124,14 @@ export { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase, -} from './core/workflow/index.js'; +} from './core/piece/index.js'; export type { - WorkflowEvents, + PieceEvents, UserInputRequest, IterationLimitRequest, SessionUpdateCallback, IterationLimitCallback, - WorkflowEngineOptions, + PieceEngineOptions, LoopCheckResult, ProviderType, RuleMatch, @@ -141,7 +141,7 @@ export type { InstructionContext, StatusRulesComponents, BlockedHandlerResult, -} from './core/workflow/index.js'; +} from './core/piece/index.js'; // Utilities export * from './shared/utils/index.js'; diff --git a/src/infra/claude/types.ts b/src/infra/claude/types.ts index 494d1ed..00eb588 100644 --- a/src/infra/claude/types.ts +++ b/src/infra/claude/types.ts @@ -7,7 +7,7 @@ import type { PermissionUpdate, AgentDefinition } from '@anthropic-ai/claude-agent-sdk'; import type { PermissionMode } from '../../core/models/index.js'; -import type { PermissionResult } from '../../core/workflow/index.js'; +import type { PermissionResult } from '../../core/piece/index.js'; // Re-export PermissionResult for convenience export type { PermissionResult, PermissionUpdate }; @@ -126,7 +126,7 @@ export interface ClaudeCallOptions { systemPrompt?: string; /** SDK agents to register for sub-agent execution */ agents?: Record; - /** Permission mode for tool execution (from workflow step) */ + /** Permission mode for tool execution (from piece step) */ permissionMode?: PermissionMode; /** Enable streaming mode with callback for real-time output */ onStream?: StreamCallback; diff --git a/src/infra/config/global/bookmarks.ts b/src/infra/config/global/bookmarks.ts index bbb54cd..001b664 100644 --- a/src/infra/config/global/bookmarks.ts +++ b/src/infra/config/global/bookmarks.ts @@ -1,5 +1,5 @@ /** - * Workflow bookmarks management (separate from config.yaml) + * Piece bookmarks management (separate from config.yaml) * * Bookmarks are stored in a configurable location (default: ~/.takt/preferences/bookmarks.yaml) */ @@ -11,7 +11,7 @@ import { getGlobalConfigDir } from '../paths.js'; import { loadGlobalConfig } from './globalConfig.js'; interface BookmarksFile { - workflows: string[]; + pieces: string[]; } function getDefaultBookmarksPath(): string { @@ -33,20 +33,20 @@ function getBookmarksPath(): string { function loadBookmarksFile(): BookmarksFile { const bookmarksPath = getBookmarksPath(); if (!existsSync(bookmarksPath)) { - return { workflows: [] }; + return { pieces: [] }; } try { const content = readFileSync(bookmarksPath, 'utf-8'); const parsed = parseYaml(content); - if (parsed && typeof parsed === 'object' && 'workflows' in parsed && Array.isArray(parsed.workflows)) { - return { workflows: parsed.workflows }; + if (parsed && typeof parsed === 'object' && 'pieces' in parsed && Array.isArray(parsed.pieces)) { + return { pieces: parsed.pieces }; } } catch { // Ignore parse errors } - return { workflows: [] }; + return { pieces: [] }; } function saveBookmarksFile(bookmarks: BookmarksFile): void { @@ -59,43 +59,43 @@ function saveBookmarksFile(bookmarks: BookmarksFile): void { writeFileSync(bookmarksPath, content, 'utf-8'); } -/** Get bookmarked workflow names */ -export function getBookmarkedWorkflows(): string[] { +/** Get bookmarked piece names */ +export function getBookmarkedPieces(): string[] { const bookmarks = loadBookmarksFile(); - return bookmarks.workflows; + return bookmarks.pieces; } /** - * Add a workflow to bookmarks. + * Add a piece to bookmarks. * Persists to ~/.takt/bookmarks.yaml and returns the updated bookmarks list. */ -export function addBookmark(workflowName: string): string[] { +export function addBookmark(pieceName: string): string[] { const bookmarks = loadBookmarksFile(); - if (!bookmarks.workflows.includes(workflowName)) { - bookmarks.workflows.push(workflowName); + if (!bookmarks.pieces.includes(pieceName)) { + bookmarks.pieces.push(pieceName); saveBookmarksFile(bookmarks); } - return bookmarks.workflows; + return bookmarks.pieces; } /** - * Remove a workflow from bookmarks. + * Remove a piece from bookmarks. * Persists to ~/.takt/bookmarks.yaml and returns the updated bookmarks list. */ -export function removeBookmark(workflowName: string): string[] { +export function removeBookmark(pieceName: string): string[] { const bookmarks = loadBookmarksFile(); - const index = bookmarks.workflows.indexOf(workflowName); + const index = bookmarks.pieces.indexOf(pieceName); if (index >= 0) { - bookmarks.workflows.splice(index, 1); + bookmarks.pieces.splice(index, 1); saveBookmarksFile(bookmarks); } - return bookmarks.workflows; + return bookmarks.pieces; } /** - * Check if a workflow is bookmarked. + * Check if a piece is bookmarked. */ -export function isBookmarked(workflowName: string): boolean { +export function isBookmarked(pieceName: string): boolean { const bookmarks = loadBookmarksFile(); - return bookmarks.workflows.includes(workflowName); + return bookmarks.pieces.includes(pieceName); } diff --git a/src/infra/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts index c7f6030..484188f 100644 --- a/src/infra/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -18,10 +18,10 @@ function createDefaultGlobalConfig(): GlobalConfig { return { language: DEFAULT_LANGUAGE, trustedDirectories: [], - defaultWorkflow: 'default', + defaultPiece: 'default', logLevel: 'info', provider: 'claude', - enableBuiltinWorkflows: true, + enableBuiltinPieces: true, }; } @@ -69,7 +69,7 @@ export class GlobalConfigManager { const config: GlobalConfig = { language: parsed.language, trustedDirectories: parsed.trusted_directories, - defaultWorkflow: parsed.default_workflow, + defaultPiece: parsed.default_piece, logLevel: parsed.log_level, provider: parsed.provider, model: parsed.model, @@ -79,7 +79,7 @@ export class GlobalConfigManager { } : undefined, worktreeDir: parsed.worktree_dir, disabledBuiltins: parsed.disabled_builtins, - enableBuiltinWorkflows: parsed.enable_builtin_workflows, + enableBuiltinPieces: parsed.enable_builtin_pieces, anthropicApiKey: parsed.anthropic_api_key, openaiApiKey: parsed.openai_api_key, pipeline: parsed.pipeline ? { @@ -89,7 +89,7 @@ export class GlobalConfigManager { } : undefined, minimalOutput: parsed.minimal_output, bookmarksFile: parsed.bookmarks_file, - workflowCategoriesFile: parsed.workflow_categories_file, + pieceCategoriesFile: parsed.piece_categories_file, }; this.cachedConfig = config; return config; @@ -101,7 +101,7 @@ export class GlobalConfigManager { const raw: Record = { language: config.language, trusted_directories: config.trustedDirectories, - default_workflow: config.defaultWorkflow, + default_piece: config.defaultPiece, log_level: config.logLevel, provider: config.provider, }; @@ -120,8 +120,8 @@ export class GlobalConfigManager { if (config.disabledBuiltins && config.disabledBuiltins.length > 0) { raw.disabled_builtins = config.disabledBuiltins; } - if (config.enableBuiltinWorkflows !== undefined) { - raw.enable_builtin_workflows = config.enableBuiltinWorkflows; + if (config.enableBuiltinPieces !== undefined) { + raw.enable_builtin_pieces = config.enableBuiltinPieces; } if (config.anthropicApiKey) { raw.anthropic_api_key = config.anthropicApiKey; @@ -144,8 +144,8 @@ export class GlobalConfigManager { if (config.bookmarksFile) { raw.bookmarks_file = config.bookmarksFile; } - if (config.workflowCategoriesFile) { - raw.workflow_categories_file = config.workflowCategoriesFile; + if (config.pieceCategoriesFile) { + raw.piece_categories_file = config.pieceCategoriesFile; } writeFileSync(configPath, stringifyYaml(raw), 'utf-8'); this.invalidateCache(); @@ -173,10 +173,10 @@ export function getDisabledBuiltins(): string[] { } } -export function getBuiltinWorkflowsEnabled(): boolean { +export function getBuiltinPiecesEnabled(): boolean { try { const config = loadGlobalConfig(); - return config.enableBuiltinWorkflows !== false; + return config.enableBuiltinPieces !== false; } catch { return true; } diff --git a/src/infra/config/global/index.ts b/src/infra/config/global/index.ts index 45f764d..df2e4b6 100644 --- a/src/infra/config/global/index.ts +++ b/src/infra/config/global/index.ts @@ -8,7 +8,7 @@ export { loadGlobalConfig, saveGlobalConfig, getDisabledBuiltins, - getBuiltinWorkflowsEnabled, + getBuiltinPiecesEnabled, getLanguage, setLanguage, setProvider, @@ -21,20 +21,20 @@ export { } from './globalConfig.js'; export { - getBookmarkedWorkflows, + getBookmarkedPieces, addBookmark, removeBookmark, isBookmarked, } from './bookmarks.js'; export { - getWorkflowCategoriesConfig, - setWorkflowCategoriesConfig, + getPieceCategoriesConfig, + setPieceCategoriesConfig, getShowOthersCategory, setShowOthersCategory, getOthersCategoryName, setOthersCategoryName, -} from './workflowCategories.js'; +} from './pieceCategories.js'; export { needsLanguageSetup, diff --git a/src/infra/config/global/initialization.ts b/src/infra/config/global/initialization.ts index 43a04dd..deccc53 100644 --- a/src/infra/config/global/initialization.ts +++ b/src/infra/config/global/initialization.ts @@ -2,7 +2,7 @@ * Initialization module for first-time setup * * Handles language selection and initial config.yaml creation. - * Builtin agents/workflows are loaded via fallback from resources/ + * Builtin agents/pieces are loaded via fallback from resources/ * and no longer copied to ~/.takt/ on setup. */ @@ -40,7 +40,7 @@ export async function promptLanguageSelection(): Promise { ]; const result = await selectOptionWithDefault( - 'Select language for default agents and workflows / デフォルトのエージェントとワークフローの言語を選択してください:', + 'Select language for default agents and pieces / デフォルトのエージェントとピースの言語を選択してください:', options, DEFAULT_LANGUAGE ); @@ -84,7 +84,7 @@ export interface InitGlobalDirsOptions { /** * Initialize global takt directory structure with language selection. * On first run, creates config.yaml from language template. - * Agents/workflows are NOT copied — they are loaded via builtin fallback. + * Agents/pieces are NOT copied — they are loaded via builtin fallback. * * In non-interactive mode (pipeline mode or no TTY), skips prompts * and uses default values so takt works in pipeline/CI environments without config.yaml. diff --git a/src/infra/config/global/workflowCategories.ts b/src/infra/config/global/pieceCategories.ts similarity index 52% rename from src/infra/config/global/workflowCategories.ts rename to src/infra/config/global/pieceCategories.ts index 1c4fb61..4bc818f 100644 --- a/src/infra/config/global/workflowCategories.ts +++ b/src/infra/config/global/pieceCategories.ts @@ -1,7 +1,7 @@ /** - * Workflow categories management (separate from config.yaml) + * Piece categories management (separate from config.yaml) * - * Categories are stored in a configurable location (default: ~/.takt/preferences/workflow-categories.yaml) + * Categories are stored in a configurable location (default: ~/.takt/preferences/piece-categories.yaml) */ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'; @@ -9,32 +9,32 @@ import { join, dirname } from 'node:path'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { getGlobalConfigDir } from '../paths.js'; import { loadGlobalConfig } from './globalConfig.js'; -import type { WorkflowCategoryConfigNode } from '../../../core/models/index.js'; +import type { PieceCategoryConfigNode } from '../../../core/models/index.js'; -interface WorkflowCategoriesFile { - categories?: WorkflowCategoryConfigNode; +interface PieceCategoriesFile { + categories?: PieceCategoryConfigNode; show_others_category?: boolean; others_category_name?: string; } -function getDefaultWorkflowCategoriesPath(): string { - return join(getGlobalConfigDir(), 'preferences', 'workflow-categories.yaml'); +function getDefaultPieceCategoriesPath(): string { + return join(getGlobalConfigDir(), 'preferences', 'piece-categories.yaml'); } -function getWorkflowCategoriesPath(): string { +function getPieceCategoriesPath(): string { try { const config = loadGlobalConfig(); - if (config.workflowCategoriesFile) { - return config.workflowCategoriesFile; + if (config.pieceCategoriesFile) { + return config.pieceCategoriesFile; } } catch { // Ignore errors, use default } - return getDefaultWorkflowCategoriesPath(); + return getDefaultPieceCategoriesPath(); } -function loadWorkflowCategoriesFile(): WorkflowCategoriesFile { - const categoriesPath = getWorkflowCategoriesPath(); +function loadPieceCategoriesFile(): PieceCategoriesFile { + const categoriesPath = getPieceCategoriesPath(); if (!existsSync(categoriesPath)) { return {}; } @@ -43,7 +43,7 @@ function loadWorkflowCategoriesFile(): WorkflowCategoriesFile { const content = readFileSync(categoriesPath, 'utf-8'); const parsed = parseYaml(content); if (parsed && typeof parsed === 'object') { - return parsed as WorkflowCategoriesFile; + return parsed as PieceCategoriesFile; } } catch { // Ignore parse errors @@ -52,8 +52,8 @@ function loadWorkflowCategoriesFile(): WorkflowCategoriesFile { return {}; } -function saveWorkflowCategoriesFile(data: WorkflowCategoriesFile): void { - const categoriesPath = getWorkflowCategoriesPath(); +function savePieceCategoriesFile(data: PieceCategoriesFile): void { + const categoriesPath = getPieceCategoriesPath(); const dir = dirname(categoriesPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); @@ -62,41 +62,41 @@ function saveWorkflowCategoriesFile(data: WorkflowCategoriesFile): void { writeFileSync(categoriesPath, content, 'utf-8'); } -/** Get workflow categories configuration */ -export function getWorkflowCategoriesConfig(): WorkflowCategoryConfigNode | undefined { - const data = loadWorkflowCategoriesFile(); +/** Get piece categories configuration */ +export function getPieceCategoriesConfig(): PieceCategoryConfigNode | undefined { + const data = loadPieceCategoriesFile(); return data.categories; } -/** Set workflow categories configuration */ -export function setWorkflowCategoriesConfig(categories: WorkflowCategoryConfigNode): void { - const data = loadWorkflowCategoriesFile(); +/** Set piece categories configuration */ +export function setPieceCategoriesConfig(categories: PieceCategoryConfigNode): void { + const data = loadPieceCategoriesFile(); data.categories = categories; - saveWorkflowCategoriesFile(data); + savePieceCategoriesFile(data); } /** Get show others category flag */ export function getShowOthersCategory(): boolean | undefined { - const data = loadWorkflowCategoriesFile(); + const data = loadPieceCategoriesFile(); return data.show_others_category; } /** Set show others category flag */ export function setShowOthersCategory(show: boolean): void { - const data = loadWorkflowCategoriesFile(); + const data = loadPieceCategoriesFile(); data.show_others_category = show; - saveWorkflowCategoriesFile(data); + savePieceCategoriesFile(data); } /** Get others category name */ export function getOthersCategoryName(): string | undefined { - const data = loadWorkflowCategoriesFile(); + const data = loadPieceCategoriesFile(); return data.others_category_name; } /** Set others category name */ export function setOthersCategoryName(name: string): void { - const data = loadWorkflowCategoriesFile(); + const data = loadPieceCategoriesFile(); data.others_category_name = name; - saveWorkflowCategoriesFile(data); + savePieceCategoriesFile(data); } diff --git a/src/infra/config/loaders/agentLoader.ts b/src/infra/config/loaders/agentLoader.ts index 90ead67..61bfd9b 100644 --- a/src/infra/config/loaders/agentLoader.ts +++ b/src/infra/config/loaders/agentLoader.ts @@ -69,7 +69,7 @@ export function listCustomAgents(): string[] { * Load agent prompt content. * Agents can be loaded from: * - ~/.takt/agents/*.md (global agents) - * - ~/.takt/pieces/{workflow}/*.md (workflow-specific agents) + * - ~/.takt/pieces/{piece}/*.md (piece-specific agents) */ export function loadAgentPrompt(agent: CustomAgentConfig): string { if (agent.prompt) { @@ -95,7 +95,7 @@ export function loadAgentPrompt(agent: CustomAgentConfig): string { /** * Load agent prompt from a resolved path. - * Used by workflow engine when agentPath is already resolved. + * Used by piece engine when agentPath is already resolved. */ export function loadAgentPromptFromPath(agentPath: string): string { const isValid = getAllowedAgentBases().some((base) => isPathSafe(base, agentPath)); diff --git a/src/infra/config/loaders/index.ts b/src/infra/config/loaders/index.ts index 26146cd..bd9cd98 100644 --- a/src/infra/config/loaders/index.ts +++ b/src/infra/config/loaders/index.ts @@ -3,30 +3,30 @@ */ export { - getBuiltinWorkflow, - loadWorkflow, - loadWorkflowByIdentifier, - isWorkflowPath, - getWorkflowDescription, - loadAllWorkflows, - loadAllWorkflowsWithSources, - listWorkflows, - listWorkflowEntries, - type WorkflowDirEntry, - type WorkflowSource, - type WorkflowWithSource, -} from './workflowLoader.js'; + getBuiltinPiece, + loadPiece, + loadPieceByIdentifier, + isPiecePath, + getPieceDescription, + loadAllPieces, + loadAllPiecesWithSources, + listPieces, + listPieceEntries, + type PieceDirEntry, + type PieceSource, + type PieceWithSource, +} from './pieceLoader.js'; export { loadDefaultCategories, - getWorkflowCategories, - buildCategorizedWorkflows, - findWorkflowCategories, + getPieceCategories, + buildCategorizedPieces, + findPieceCategories, type CategoryConfig, - type CategorizedWorkflows, - type MissingWorkflow, - type WorkflowCategoryNode, -} from './workflowCategories.js'; + type CategorizedPieces, + type MissingPiece, + type PieceCategoryNode, +} from './pieceCategories.js'; export { loadAgentsFromDir, diff --git a/src/infra/config/loaders/loader.ts b/src/infra/config/loaders/loader.ts index 307657c..a4f0cda 100644 --- a/src/infra/config/loaders/loader.ts +++ b/src/infra/config/loaders/loader.ts @@ -4,15 +4,15 @@ * Re-exports from specialized loaders. */ -// Workflow loading +// Piece loading export { - getBuiltinWorkflow, - loadWorkflow, - loadWorkflowByIdentifier, - isWorkflowPath, - loadAllWorkflows, - listWorkflows, -} from './workflowLoader.js'; + getBuiltinPiece, + loadPiece, + loadPieceByIdentifier, + isPiecePath, + loadAllPieces, + listPieces, +} from './pieceLoader.js'; // Agent loading export { diff --git a/src/infra/config/loaders/workflowCategories.ts b/src/infra/config/loaders/pieceCategories.ts similarity index 52% rename from src/infra/config/loaders/workflowCategories.ts rename to src/infra/config/loaders/pieceCategories.ts index 5243777..ad2277b 100644 --- a/src/infra/config/loaders/workflowCategories.ts +++ b/src/infra/config/loaders/pieceCategories.ts @@ -1,5 +1,5 @@ /** - * Workflow category configuration loader and helpers. + * Piece category configuration loader and helpers. */ import { existsSync, readFileSync } from 'node:fs'; @@ -7,48 +7,48 @@ import { join } from 'node:path'; import { parse as parseYaml } from 'yaml'; import { z } from 'zod/v4'; import { getProjectConfigPath } from '../paths.js'; -import { getLanguage, getBuiltinWorkflowsEnabled, getDisabledBuiltins } from '../global/globalConfig.js'; +import { getLanguage, getBuiltinPiecesEnabled, getDisabledBuiltins } from '../global/globalConfig.js'; import { - getWorkflowCategoriesConfig, + getPieceCategoriesConfig, getShowOthersCategory, getOthersCategoryName, -} from '../global/workflowCategories.js'; +} from '../global/pieceCategories.js'; import { getLanguageResourcesDir } from '../../resources/index.js'; -import { listBuiltinWorkflowNames } from './workflowResolver.js'; -import type { WorkflowSource, WorkflowWithSource } from './workflowResolver.js'; +import { listBuiltinPieceNames } from './pieceResolver.js'; +import type { PieceSource, PieceWithSource } from './pieceResolver.js'; const CategoryConfigSchema = z.object({ - workflow_categories: z.record(z.string(), z.unknown()).optional(), + piece_categories: z.record(z.string(), z.unknown()).optional(), show_others_category: z.boolean().optional(), others_category_name: z.string().min(1).optional(), }).passthrough(); -export interface WorkflowCategoryNode { +export interface PieceCategoryNode { name: string; - workflows: string[]; - children: WorkflowCategoryNode[]; + pieces: string[]; + children: PieceCategoryNode[]; } export interface CategoryConfig { - workflowCategories: WorkflowCategoryNode[]; + pieceCategories: PieceCategoryNode[]; showOthersCategory: boolean; othersCategoryName: string; } -export interface CategorizedWorkflows { - categories: WorkflowCategoryNode[]; - builtinCategories: WorkflowCategoryNode[]; - allWorkflows: Map; - missingWorkflows: MissingWorkflow[]; +export interface CategorizedPieces { + categories: PieceCategoryNode[]; + builtinCategories: PieceCategoryNode[]; + allPieces: Map; + missingPieces: MissingPiece[]; } -export interface MissingWorkflow { +export interface MissingPiece { categoryPath: string[]; - workflowName: string; + pieceName: string; } interface RawCategoryConfig { - workflow_categories?: Record; + piece_categories?: Record; show_others_category?: boolean; others_category_name?: string; } @@ -57,19 +57,19 @@ function isRecord(value: unknown): value is Record { return !!value && typeof value === 'object' && !Array.isArray(value); } -function parseWorkflows(raw: unknown, sourceLabel: string, path: string[]): string[] { +function parsePieces(raw: unknown, sourceLabel: string, path: string[]): string[] { if (raw === undefined) return []; if (!Array.isArray(raw)) { - throw new Error(`workflows must be an array in ${sourceLabel} at ${path.join(' > ')}`); + throw new Error(`pieces must be an array in ${sourceLabel} at ${path.join(' > ')}`); } - const workflows: string[] = []; + const pieces: string[] = []; for (const item of raw) { if (typeof item !== 'string' || item.trim().length === 0) { - throw new Error(`workflow name must be a non-empty string in ${sourceLabel} at ${path.join(' > ')}`); + throw new Error(`piece name must be a non-empty string in ${sourceLabel} at ${path.join(' > ')}`); } - workflows.push(item); + pieces.push(item); } - return workflows; + return pieces; } function parseCategoryNode( @@ -77,30 +77,30 @@ function parseCategoryNode( raw: unknown, sourceLabel: string, path: string[], -): WorkflowCategoryNode { +): PieceCategoryNode { if (!isRecord(raw)) { throw new Error(`category "${name}" must be an object in ${sourceLabel} at ${path.join(' > ')}`); } - const workflows = parseWorkflows(raw.workflows, sourceLabel, path); - const children: WorkflowCategoryNode[] = []; + const pieces = parsePieces(raw.pieces, sourceLabel, path); + const children: PieceCategoryNode[] = []; for (const [key, value] of Object.entries(raw)) { - if (key === 'workflows') continue; + if (key === 'pieces') continue; if (!isRecord(value)) { throw new Error(`category "${key}" must be an object in ${sourceLabel} at ${[...path, key].join(' > ')}`); } children.push(parseCategoryNode(key, value, sourceLabel, [...path, key])); } - return { name, workflows, children }; + return { name, pieces, children }; } -function parseCategoryTree(raw: unknown, sourceLabel: string): WorkflowCategoryNode[] { +function parseCategoryTree(raw: unknown, sourceLabel: string): PieceCategoryNode[] { if (!isRecord(raw)) { - throw new Error(`workflow_categories must be an object in ${sourceLabel}`); + throw new Error(`piece_categories must be an object in ${sourceLabel}`); } - const categories: WorkflowCategoryNode[] = []; + const categories: PieceCategoryNode[] = []; for (const [name, value] of Object.entries(raw)) { categories.push(parseCategoryNode(name, value, sourceLabel, [name])); } @@ -112,14 +112,14 @@ function parseCategoryConfig(raw: unknown, sourceLabel: string): CategoryConfig return null; } - const hasWorkflowCategories = Object.prototype.hasOwnProperty.call(raw, 'workflow_categories'); - if (!hasWorkflowCategories) { + const hasPieceCategories = Object.prototype.hasOwnProperty.call(raw, 'piece_categories'); + if (!hasPieceCategories) { return null; } const parsed = CategoryConfigSchema.parse(raw) as RawCategoryConfig; - if (!parsed.workflow_categories) { - throw new Error(`workflow_categories is required in ${sourceLabel}`); + if (!parsed.piece_categories) { + throw new Error(`piece_categories is required in ${sourceLabel}`); } const showOthersCategory = parsed.show_others_category === undefined @@ -131,7 +131,7 @@ function parseCategoryConfig(raw: unknown, sourceLabel: string): CategoryConfig : parsed.others_category_name; return { - workflowCategories: parseCategoryTree(parsed.workflow_categories, sourceLabel), + pieceCategories: parseCategoryTree(parsed.piece_categories, sourceLabel), showOthersCategory, othersCategoryName, }; @@ -148,7 +148,7 @@ function loadCategoryConfigFromPath(path: string, sourceLabel: string): Category /** * Load default categories from builtin resource file. - * Returns null if file doesn't exist or has no workflow_categories. + * Returns null if file doesn't exist or has no piece_categories. */ export function loadDefaultCategories(): CategoryConfig | null { const lang = getLanguage(); @@ -157,17 +157,17 @@ export function loadDefaultCategories(): CategoryConfig | null { } /** - * Get effective workflow categories configuration. + * Get effective piece categories configuration. * Priority: user config -> project config -> default categories. */ -export function getWorkflowCategories(cwd: string): CategoryConfig | null { - // Check user config from separate file (~/.takt/workflow-categories.yaml) - const userCategoriesNode = getWorkflowCategoriesConfig(); +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 { - workflowCategories: parseCategoryTree(userCategoriesNode, 'user config'), + pieceCategories: parseCategoryTree(userCategoriesNode, 'user config'), showOthersCategory, othersCategoryName, }; @@ -181,19 +181,19 @@ export function getWorkflowCategories(cwd: string): CategoryConfig | null { return loadDefaultCategories(); } -function collectMissingWorkflows( - categories: WorkflowCategoryNode[], - allWorkflows: Map, - ignoreWorkflows: Set, -): MissingWorkflow[] { - const missing: MissingWorkflow[] = []; - const visit = (nodes: WorkflowCategoryNode[], path: string[]): void => { +function collectMissingPieces( + categories: PieceCategoryNode[], + allPieces: Map, + ignorePieces: Set, +): MissingPiece[] { + const missing: MissingPiece[] = []; + const visit = (nodes: PieceCategoryNode[], path: string[]): void => { for (const node of nodes) { const nextPath = [...path, node.name]; - for (const workflowName of node.workflows) { - if (ignoreWorkflows.has(workflowName)) continue; - if (!allWorkflows.has(workflowName)) { - missing.push({ categoryPath: nextPath, workflowName }); + for (const pieceName of node.pieces) { + if (ignorePieces.has(pieceName)) continue; + if (!allPieces.has(pieceName)) { + missing.push({ categoryPath: nextPath, pieceName }); } } if (node.children.length > 0) { @@ -207,26 +207,26 @@ function collectMissingWorkflows( } function buildCategoryTreeForSource( - categories: WorkflowCategoryNode[], - allWorkflows: Map, - sourceFilter: (source: WorkflowSource) => boolean, + categories: PieceCategoryNode[], + allPieces: Map, + sourceFilter: (source: PieceSource) => boolean, categorized: Set, -): WorkflowCategoryNode[] { - const result: WorkflowCategoryNode[] = []; +): PieceCategoryNode[] { + const result: PieceCategoryNode[] = []; for (const node of categories) { - const workflows: string[] = []; - for (const workflowName of node.workflows) { - const entry = allWorkflows.get(workflowName); + const pieces: string[] = []; + for (const pieceName of node.pieces) { + const entry = allPieces.get(pieceName); if (!entry) continue; if (!sourceFilter(entry.source)) continue; - workflows.push(workflowName); - categorized.add(workflowName); + pieces.push(pieceName); + categorized.add(pieceName); } - const children = buildCategoryTreeForSource(node.children, allWorkflows, sourceFilter, categorized); - if (workflows.length > 0 || children.length > 0) { - result.push({ name: node.name, workflows, children }); + const children = buildCategoryTreeForSource(node.children, allPieces, sourceFilter, categorized); + if (pieces.length > 0 || children.length > 0) { + result.push({ name: node.name, pieces, children }); } } @@ -234,40 +234,40 @@ function buildCategoryTreeForSource( } function appendOthersCategory( - categories: WorkflowCategoryNode[], - allWorkflows: Map, + categories: PieceCategoryNode[], + allPieces: Map, categorized: Set, - sourceFilter: (source: WorkflowSource) => boolean, + sourceFilter: (source: PieceSource) => boolean, othersCategoryName: string, -): WorkflowCategoryNode[] { +): PieceCategoryNode[] { if (categories.some((node) => node.name === othersCategoryName)) { return categories; } const uncategorized: string[] = []; - for (const [workflowName, entry] of allWorkflows.entries()) { + for (const [pieceName, entry] of allPieces.entries()) { if (!sourceFilter(entry.source)) continue; - if (categorized.has(workflowName)) continue; - uncategorized.push(workflowName); + if (categorized.has(pieceName)) continue; + uncategorized.push(pieceName); } if (uncategorized.length === 0) { return categories; } - return [...categories, { name: othersCategoryName, workflows: uncategorized, children: [] }]; + return [...categories, { name: othersCategoryName, pieces: uncategorized, children: [] }]; } /** - * Build categorized workflows map from configuration. + * Build categorized pieces map from configuration. */ -export function buildCategorizedWorkflows( - allWorkflows: Map, +export function buildCategorizedPieces( + allPieces: Map, config: CategoryConfig, -): CategorizedWorkflows { +): CategorizedPieces { const ignoreMissing = new Set(); - if (!getBuiltinWorkflowsEnabled()) { - for (const name of listBuiltinWorkflowNames({ includeDisabled: true })) { + if (!getBuiltinPiecesEnabled()) { + for (const name of listBuiltinPieceNames({ includeDisabled: true })) { ignoreMissing.add(name); } } else { @@ -276,27 +276,27 @@ export function buildCategorizedWorkflows( } } - const missingWorkflows = collectMissingWorkflows( - config.workflowCategories, - allWorkflows, + const missingPieces = collectMissingPieces( + config.pieceCategories, + allPieces, ignoreMissing, ); - const isBuiltin = (source: WorkflowSource): boolean => source === 'builtin'; - const isCustom = (source: WorkflowSource): boolean => source !== 'builtin'; + const isBuiltin = (source: PieceSource): boolean => source === 'builtin'; + const isCustom = (source: PieceSource): boolean => source !== 'builtin'; const categorizedCustom = new Set(); const categories = buildCategoryTreeForSource( - config.workflowCategories, - allWorkflows, + config.pieceCategories, + allPieces, isCustom, categorizedCustom, ); const categorizedBuiltin = new Set(); const builtinCategories = buildCategoryTreeForSource( - config.workflowCategories, - allWorkflows, + config.pieceCategories, + allPieces, isBuiltin, categorizedBuiltin, ); @@ -304,7 +304,7 @@ export function buildCategorizedWorkflows( const finalCategories = config.showOthersCategory ? appendOthersCategory( categories, - allWorkflows, + allPieces, categorizedCustom, isCustom, config.othersCategoryName, @@ -314,7 +314,7 @@ export function buildCategorizedWorkflows( const finalBuiltinCategories = config.showOthersCategory ? appendOthersCategory( builtinCategories, - allWorkflows, + allPieces, categorizedBuiltin, isBuiltin, config.othersCategoryName, @@ -324,36 +324,36 @@ export function buildCategorizedWorkflows( return { categories: finalCategories, builtinCategories: finalBuiltinCategories, - allWorkflows, - missingWorkflows, + allPieces, + missingPieces, }; } -function findWorkflowCategoryPaths( - workflow: string, - categories: WorkflowCategoryNode[], +function findPieceCategoryPaths( + piece: string, + categories: PieceCategoryNode[], prefix: string[], results: string[], ): void { for (const node of categories) { const path = [...prefix, node.name]; - if (node.workflows.includes(workflow)) { + if (node.pieces.includes(piece)) { results.push(path.join(' / ')); } if (node.children.length > 0) { - findWorkflowCategoryPaths(workflow, node.children, path, results); + findPieceCategoryPaths(piece, node.children, path, results); } } } /** - * Find which categories contain a given workflow (for duplicate indication). + * Find which categories contain a given piece (for duplicate indication). */ -export function findWorkflowCategories( - workflow: string, - categories: WorkflowCategoryNode[], +export function findPieceCategories( + piece: string, + categories: PieceCategoryNode[], ): string[] { const result: string[] = []; - findWorkflowCategoryPaths(workflow, categories, [], result); + findPieceCategoryPaths(piece, categories, [], result); return result; } diff --git a/src/infra/config/loaders/pieceLoader.ts b/src/infra/config/loaders/pieceLoader.ts new file mode 100644 index 0000000..278bdc9 --- /dev/null +++ b/src/infra/config/loaders/pieceLoader.ts @@ -0,0 +1,26 @@ +/** + * Piece configuration loader — re-export hub. + * + * Implementations have been split into: + * - pieceParser.ts: YAML parsing, step/rule normalization + * - pieceResolver.ts: 3-layer resolution (builtin → user → project-local) + */ + +// Parser exports +export { normalizePieceConfig, loadPieceFromFile } from './pieceParser.js'; + +// Resolver exports (public API) +export { + getBuiltinPiece, + loadPiece, + isPiecePath, + loadPieceByIdentifier, + getPieceDescription, + loadAllPieces, + loadAllPiecesWithSources, + listPieces, + listPieceEntries, + type PieceDirEntry, + type PieceSource, + type PieceWithSource, +} from './pieceResolver.js'; diff --git a/src/infra/config/loaders/workflowParser.ts b/src/infra/config/loaders/pieceParser.ts similarity index 72% rename from src/infra/config/loaders/workflowParser.ts rename to src/infra/config/loaders/pieceParser.ts index 1644566..f960769 100644 --- a/src/infra/config/loaders/workflowParser.ts +++ b/src/infra/config/loaders/pieceParser.ts @@ -1,7 +1,7 @@ /** - * Workflow YAML parsing and normalization. + * Piece YAML parsing and normalization. * - * Converts raw YAML structures into internal WorkflowConfig format, + * Converts raw YAML structures into internal PieceConfig format, * resolving agent paths, content paths, and rule conditions. */ @@ -9,20 +9,20 @@ import { readFileSync, existsSync } from 'node:fs'; import { join, dirname, basename } from 'node:path'; import { parse as parseYaml } from 'yaml'; import type { z } from 'zod'; -import { WorkflowConfigRawSchema, WorkflowMovementRawSchema } from '../../../core/models/index.js'; -import type { WorkflowConfig, WorkflowMovement, WorkflowRule, ReportConfig, ReportObjectConfig } from '../../../core/models/index.js'; +import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js'; +import type { PieceConfig, PieceMovement, PieceRule, ReportConfig, ReportObjectConfig } from '../../../core/models/index.js'; /** Parsed movement type from Zod schema (replaces `any`) */ -type RawStep = z.output; +type RawStep = z.output; /** - * Resolve agent path from workflow specification. - * - Relative path (./agent.md): relative to workflow directory + * Resolve agent path from piece specification. + * - Relative path (./agent.md): relative to piece directory * - Absolute path (/path/to/agent.md or ~/...): use as-is */ -function resolveAgentPathForWorkflow(agentSpec: string, workflowDir: string): string { +function resolveAgentPathForPiece(agentSpec: string, pieceDir: string): string { if (agentSpec.startsWith('./')) { - return join(workflowDir, agentSpec.slice(2)); + return join(pieceDir, agentSpec.slice(2)); } if (agentSpec.startsWith('~')) { const homedir = process.env.HOME || process.env.USERPROFILE || ''; @@ -31,7 +31,7 @@ function resolveAgentPathForWorkflow(agentSpec: string, workflowDir: string): st if (agentSpec.startsWith('/')) { return agentSpec; } - return join(workflowDir, agentSpec); + return join(pieceDir, agentSpec); } /** @@ -44,20 +44,20 @@ function extractAgentDisplayName(agentPath: string): string { /** * Resolve a string value that may be a file path. - * If the value ends with .md and the file exists (resolved relative to workflowDir), + * If the value ends with .md and the file exists (resolved relative to pieceDir), * read and return the file contents. Otherwise return the value as-is. */ -function resolveContentPath(value: string | undefined, workflowDir: string): string | undefined { +function resolveContentPath(value: string | undefined, pieceDir: string): string | undefined { if (value == null) return undefined; if (value.endsWith('.md')) { let resolvedPath = value; if (value.startsWith('./')) { - resolvedPath = join(workflowDir, value.slice(2)); + resolvedPath = join(pieceDir, value.slice(2)); } else if (value.startsWith('~')) { const homedir = process.env.HOME || process.env.USERPROFILE || ''; resolvedPath = join(homedir, value.slice(1)); } else if (!value.startsWith('/')) { - resolvedPath = join(workflowDir, value); + resolvedPath = join(pieceDir, value); } if (existsSync(resolvedPath)) { return readFileSync(resolvedPath, 'utf-8'); @@ -76,15 +76,15 @@ function isReportObject(raw: unknown): raw is { name: string; order?: string; fo */ function normalizeReport( raw: string | Record[] | { name: string; order?: string; format?: string } | undefined, - workflowDir: string, + pieceDir: string, ): string | ReportConfig[] | ReportObjectConfig | undefined { if (raw == null) return undefined; if (typeof raw === 'string') return raw; if (isReportObject(raw)) { return { name: raw.name, - order: resolveContentPath(raw.order, workflowDir), - format: resolveContentPath(raw.format, workflowDir), + order: resolveContentPath(raw.order, pieceDir), + format: resolveContentPath(raw.format, pieceDir), }; } return (raw as Record[]).flatMap((entry) => @@ -128,7 +128,7 @@ function normalizeRule(r: { appendix?: string; requires_user_input?: boolean; interactive_only?: boolean; -}): WorkflowRule { +}): PieceRule { const next = r.next ?? ''; const aiMatch = r.condition.match(AI_CONDITION_REGEX); if (aiMatch?.[1]) { @@ -170,22 +170,22 @@ function normalizeRule(r: { }; } -/** Normalize a raw step into internal WorkflowMovement format. */ -function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowMovement { - const rules: WorkflowRule[] | undefined = step.rules?.map(normalizeRule); +/** Normalize a raw step into internal PieceMovement format. */ +function normalizeStepFromRaw(step: RawStep, pieceDir: string): PieceMovement { + const rules: PieceRule[] | undefined = step.rules?.map(normalizeRule); const agentSpec: string | undefined = step.agent || undefined; // Resolve agent path: if the resolved path exists on disk, use it; otherwise leave agentPath undefined // so that the runner treats agentSpec as an inline system prompt string. let agentPath: string | undefined; if (agentSpec) { - const resolved = resolveAgentPathForWorkflow(agentSpec, workflowDir); + const resolved = resolveAgentPathForPiece(agentSpec, pieceDir); if (existsSync(resolved)) { agentPath = resolved; } } - const result: WorkflowMovement = { + const result: PieceMovement = { name: step.name, description: step.description, agent: agentSpec, @@ -197,28 +197,28 @@ function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowMovem model: step.model, permissionMode: step.permission_mode, edit: step.edit, - instructionTemplate: resolveContentPath(step.instruction_template, workflowDir) || step.instruction || '{task}', + instructionTemplate: resolveContentPath(step.instruction_template, pieceDir) || step.instruction || '{task}', rules, - report: normalizeReport(step.report, workflowDir), + report: normalizeReport(step.report, pieceDir), passPreviousResponse: step.pass_previous_response ?? true, }; if (step.parallel && step.parallel.length > 0) { - result.parallel = step.parallel.map((sub: RawStep) => normalizeStepFromRaw(sub, workflowDir)); + result.parallel = step.parallel.map((sub: RawStep) => normalizeStepFromRaw(sub, pieceDir)); } return result; } /** - * Convert raw YAML workflow config to internal format. - * Agent paths are resolved relative to the workflow directory. + * Convert raw YAML piece config to internal format. + * Agent paths are resolved relative to the piece directory. */ -export function normalizeWorkflowConfig(raw: unknown, workflowDir: string): WorkflowConfig { - const parsed = WorkflowConfigRawSchema.parse(raw); +export function normalizePieceConfig(raw: unknown, pieceDir: string): PieceConfig { + const parsed = PieceConfigRawSchema.parse(raw); - const movements: WorkflowMovement[] = parsed.movements.map((step) => - normalizeStepFromRaw(step, workflowDir), + const movements: PieceMovement[] = parsed.movements.map((step) => + normalizeStepFromRaw(step, pieceDir), ); const initialMovement = parsed.initial_movement ?? movements[0]?.name ?? ''; @@ -234,15 +234,15 @@ export function normalizeWorkflowConfig(raw: unknown, workflowDir: string): Work } /** - * Load a workflow from a YAML file. - * @param filePath Path to the workflow YAML file + * Load a piece from a YAML file. + * @param filePath Path to the piece YAML file */ -export function loadWorkflowFromFile(filePath: string): WorkflowConfig { +export function loadPieceFromFile(filePath: string): PieceConfig { if (!existsSync(filePath)) { - throw new Error(`Workflow file not found: ${filePath}`); + throw new Error(`Piece file not found: ${filePath}`); } const content = readFileSync(filePath, 'utf-8'); const raw = parseYaml(content); - const workflowDir = dirname(filePath); - return normalizeWorkflowConfig(raw, workflowDir); + const pieceDir = dirname(filePath); + return normalizePieceConfig(raw, pieceDir); } diff --git a/src/infra/config/loaders/pieceResolver.ts b/src/infra/config/loaders/pieceResolver.ts new file mode 100644 index 0000000..2946ceb --- /dev/null +++ b/src/infra/config/loaders/pieceResolver.ts @@ -0,0 +1,304 @@ +/** + * Piece resolution — 3-layer lookup logic. + * + * Resolves piece names and paths to concrete PieceConfig objects, + * using the priority chain: project-local → user → builtin. + */ + +import { existsSync, readdirSync, statSync } from 'node:fs'; +import { join, resolve, isAbsolute } from 'node:path'; +import { homedir } from 'node:os'; +import type { PieceConfig } from '../../../core/models/index.js'; +import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js'; +import { getLanguage, getDisabledBuiltins, getBuiltinPiecesEnabled } from '../global/globalConfig.js'; +import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; +import { loadPieceFromFile } from './pieceParser.js'; + +const log = createLogger('piece-resolver'); + +export type PieceSource = 'builtin' | 'user' | 'project'; + +export interface PieceWithSource { + config: PieceConfig; + source: PieceSource; +} + +export function listBuiltinPieceNames(options?: { includeDisabled?: boolean }): string[] { + const lang = getLanguage(); + const dir = getBuiltinPiecesDir(lang); + const disabled = options?.includeDisabled ? undefined : getDisabledBuiltins(); + const names = new Set(); + for (const entry of iteratePieceDir(dir, 'builtin', disabled)) { + names.add(entry.name); + } + return Array.from(names); +} + +/** Get builtin piece by name */ +export function getBuiltinPiece(name: string): PieceConfig | null { + if (!getBuiltinPiecesEnabled()) return null; + const lang = getLanguage(); + const disabled = getDisabledBuiltins(); + if (disabled.includes(name)) return null; + + const builtinDir = getBuiltinPiecesDir(lang); + const yamlPath = join(builtinDir, `${name}.yaml`); + if (existsSync(yamlPath)) { + return loadPieceFromFile(yamlPath); + } + return null; +} + +/** + * Resolve a path that may be relative, absolute, or home-directory-relative. + */ +function resolvePath(pathInput: string, basePath: string): string { + if (pathInput.startsWith('~')) { + const home = homedir(); + return resolve(home, pathInput.slice(1).replace(/^\//, '')); + } + if (isAbsolute(pathInput)) { + return pathInput; + } + return resolve(basePath, pathInput); +} + +/** + * Load piece from a file path. + */ +function loadPieceFromPath( + filePath: string, + basePath: string, +): PieceConfig | null { + const resolvedPath = resolvePath(filePath, basePath); + if (!existsSync(resolvedPath)) { + return null; + } + return loadPieceFromFile(resolvedPath); +} + +/** + * Resolve a piece YAML file path by trying both .yaml and .yml extensions. + * For category/name identifiers (e.g. "frontend/react"), resolves to + * {piecesDir}/frontend/react.yaml (or .yml). + */ +function resolvePieceFile(piecesDir: string, name: string): string | null { + for (const ext of ['.yaml', '.yml']) { + const filePath = join(piecesDir, `${name}${ext}`); + if (existsSync(filePath)) return filePath; + } + return null; +} + +/** + * Load piece by name (name-based loading only, no path detection). + * Supports category/name identifiers (e.g. "frontend/react"). + * + * Priority: + * 1. Project-local pieces → .takt/pieces/{name}.yaml + * 2. User pieces → ~/.takt/pieces/{name}.yaml + * 3. Builtin pieces → resources/global/{lang}/pieces/{name}.yaml + */ +export function loadPiece( + name: string, + projectCwd: string, +): PieceConfig | null { + const projectPiecesDir = join(getProjectConfigDir(projectCwd), 'pieces'); + const projectMatch = resolvePieceFile(projectPiecesDir, name); + if (projectMatch) { + return loadPieceFromFile(projectMatch); + } + + const globalPiecesDir = getGlobalPiecesDir(); + const globalMatch = resolvePieceFile(globalPiecesDir, name); + if (globalMatch) { + return loadPieceFromFile(globalMatch); + } + + return getBuiltinPiece(name); +} + +/** + * Check if a piece identifier looks like a file path (vs a piece name). + */ +export function isPiecePath(identifier: string): boolean { + return ( + identifier.startsWith('/') || + identifier.startsWith('~') || + identifier.startsWith('./') || + identifier.startsWith('../') || + identifier.endsWith('.yaml') || + identifier.endsWith('.yml') + ); +} + +/** + * Load piece by identifier (auto-detects name vs path). + */ +export function loadPieceByIdentifier( + identifier: string, + projectCwd: string, +): PieceConfig | null { + if (isPiecePath(identifier)) { + return loadPieceFromPath(identifier, projectCwd); + } + return loadPiece(identifier, projectCwd); +} + +/** + * Get piece description by identifier. + * Returns the piece name and description (if available). + */ +export function getPieceDescription( + identifier: string, + projectCwd: string, +): { name: string; description: string } { + const piece = loadPieceByIdentifier(identifier, projectCwd); + if (!piece) { + return { name: identifier, description: '' }; + } + return { + name: piece.name, + description: piece.description ?? '', + }; +} + +/** Entry for a piece file found in a directory */ +export interface PieceDirEntry { + /** Piece name (e.g. "react") */ + name: string; + /** Full file path */ + path: string; + /** Category (subdirectory name), undefined for root-level pieces */ + category?: string; + /** Piece source (builtin, user, project) */ + source: PieceSource; +} + +/** + * Iterate piece YAML files in a directory, yielding name, path, and category. + * Scans root-level files (no category) and 1-level subdirectories (category = dir name). + * Shared by both loadAllPieces and listPieces to avoid DRY violation. + */ +function* iteratePieceDir( + dir: string, + source: PieceSource, + disabled?: string[], +): Generator { + if (!existsSync(dir)) return; + for (const entry of readdirSync(dir)) { + const entryPath = join(dir, entry); + const stat = statSync(entryPath); + + if (stat.isFile() && (entry.endsWith('.yaml') || entry.endsWith('.yml'))) { + const pieceName = entry.replace(/\.ya?ml$/, ''); + if (disabled?.includes(pieceName)) continue; + yield { name: pieceName, path: entryPath, source }; + continue; + } + + // 1-level subdirectory scan: directory name becomes the category + if (stat.isDirectory()) { + const category = entry; + for (const subEntry of readdirSync(entryPath)) { + if (!subEntry.endsWith('.yaml') && !subEntry.endsWith('.yml')) continue; + const subEntryPath = join(entryPath, subEntry); + if (!statSync(subEntryPath).isFile()) continue; + const pieceName = subEntry.replace(/\.ya?ml$/, ''); + const qualifiedName = `${category}/${pieceName}`; + if (disabled?.includes(qualifiedName)) continue; + yield { name: qualifiedName, path: subEntryPath, category, source }; + } + } + } +} + +/** Get the 3-layer directory list (builtin → user → project-local) */ +function getPieceDirs(cwd: string): { dir: string; source: PieceSource; disabled?: string[] }[] { + const disabled = getDisabledBuiltins(); + const lang = getLanguage(); + const dirs: { dir: string; source: PieceSource; disabled?: string[] }[] = []; + if (getBuiltinPiecesEnabled()) { + dirs.push({ dir: getBuiltinPiecesDir(lang), disabled, source: 'builtin' }); + } + dirs.push({ dir: getGlobalPiecesDir(), source: 'user' }); + dirs.push({ dir: join(getProjectConfigDir(cwd), 'pieces'), source: 'project' }); + return dirs; +} + +/** + * Load all pieces with source metadata. + * + * Priority (later entries override earlier): + * 1. Builtin pieces + * 2. User pieces (~/.takt/pieces/) + * 3. Project-local pieces (.takt/pieces/) + */ +export function loadAllPiecesWithSources(cwd: string): Map { + const pieces = new Map(); + + for (const { dir, source, disabled } of getPieceDirs(cwd)) { + for (const entry of iteratePieceDir(dir, source, disabled)) { + try { + pieces.set(entry.name, { config: loadPieceFromFile(entry.path), source: entry.source }); + } catch (err) { + log.debug('Skipping invalid piece file', { path: entry.path, error: getErrorMessage(err) }); + } + } + } + + return pieces; +} + +/** + * Load all pieces with descriptions (for switch command). + * + * Priority (later entries override earlier): + * 1. Builtin pieces + * 2. User pieces (~/.takt/pieces/) + * 3. Project-local pieces (.takt/pieces/) + */ +export function loadAllPieces(cwd: string): Map { + const pieces = new Map(); + const withSources = loadAllPiecesWithSources(cwd); + for (const [name, entry] of withSources) { + pieces.set(name, entry.config); + } + return pieces; +} + +/** + * List available piece names (builtin + user + project-local, excluding disabled). + * Category pieces use qualified names like "frontend/react". + */ +export function listPieces(cwd: string): string[] { + const pieces = new Set(); + + for (const { dir, source, disabled } of getPieceDirs(cwd)) { + for (const entry of iteratePieceDir(dir, source, disabled)) { + pieces.add(entry.name); + } + } + + return Array.from(pieces).sort(); +} + +/** + * List available pieces with category information for UI display. + * Returns entries grouped by category for 2-stage selection. + * + * Root-level pieces (no category) and category names are presented + * at the same level. Selecting a category drills into its pieces. + */ +export function listPieceEntries(cwd: string): PieceDirEntry[] { + // Later entries override earlier (project-local > user > builtin) + const pieces = new Map(); + + for (const { dir, source, disabled } of getPieceDirs(cwd)) { + for (const entry of iteratePieceDir(dir, source, disabled)) { + pieces.set(entry.name, entry); + } + } + + return Array.from(pieces.values()); +} diff --git a/src/infra/config/loaders/workflowLoader.ts b/src/infra/config/loaders/workflowLoader.ts deleted file mode 100644 index 5ec0f9a..0000000 --- a/src/infra/config/loaders/workflowLoader.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Workflow configuration loader — re-export hub. - * - * Implementations have been split into: - * - workflowParser.ts: YAML parsing, step/rule normalization - * - workflowResolver.ts: 3-layer resolution (builtin → user → project-local) - */ - -// Parser exports -export { normalizeWorkflowConfig, loadWorkflowFromFile } from './workflowParser.js'; - -// Resolver exports (public API) -export { - getBuiltinWorkflow, - loadWorkflow, - isWorkflowPath, - loadWorkflowByIdentifier, - getWorkflowDescription, - loadAllWorkflows, - loadAllWorkflowsWithSources, - listWorkflows, - listWorkflowEntries, - type WorkflowDirEntry, - type WorkflowSource, - type WorkflowWithSource, -} from './workflowResolver.js'; diff --git a/src/infra/config/loaders/workflowResolver.ts b/src/infra/config/loaders/workflowResolver.ts deleted file mode 100644 index ce94818..0000000 --- a/src/infra/config/loaders/workflowResolver.ts +++ /dev/null @@ -1,304 +0,0 @@ -/** - * Workflow resolution — 3-layer lookup logic. - * - * Resolves workflow names and paths to concrete WorkflowConfig objects, - * using the priority chain: project-local → user → builtin. - */ - -import { existsSync, readdirSync, statSync } from 'node:fs'; -import { join, resolve, isAbsolute } from 'node:path'; -import { homedir } from 'node:os'; -import type { WorkflowConfig } from '../../../core/models/index.js'; -import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js'; -import { getLanguage, getDisabledBuiltins, getBuiltinWorkflowsEnabled } from '../global/globalConfig.js'; -import { createLogger, getErrorMessage } from '../../../shared/utils/index.js'; -import { loadWorkflowFromFile } from './workflowParser.js'; - -const log = createLogger('workflow-resolver'); - -export type WorkflowSource = 'builtin' | 'user' | 'project'; - -export interface WorkflowWithSource { - config: WorkflowConfig; - source: WorkflowSource; -} - -export function listBuiltinWorkflowNames(options?: { includeDisabled?: boolean }): string[] { - const lang = getLanguage(); - const dir = getBuiltinPiecesDir(lang); - const disabled = options?.includeDisabled ? undefined : getDisabledBuiltins(); - const names = new Set(); - for (const entry of iterateWorkflowDir(dir, 'builtin', disabled)) { - names.add(entry.name); - } - return Array.from(names); -} - -/** Get builtin workflow by name */ -export function getBuiltinWorkflow(name: string): WorkflowConfig | null { - if (!getBuiltinWorkflowsEnabled()) return null; - const lang = getLanguage(); - const disabled = getDisabledBuiltins(); - if (disabled.includes(name)) return null; - - const builtinDir = getBuiltinPiecesDir(lang); - const yamlPath = join(builtinDir, `${name}.yaml`); - if (existsSync(yamlPath)) { - return loadWorkflowFromFile(yamlPath); - } - return null; -} - -/** - * Resolve a path that may be relative, absolute, or home-directory-relative. - */ -function resolvePath(pathInput: string, basePath: string): string { - if (pathInput.startsWith('~')) { - const home = homedir(); - return resolve(home, pathInput.slice(1).replace(/^\//, '')); - } - if (isAbsolute(pathInput)) { - return pathInput; - } - return resolve(basePath, pathInput); -} - -/** - * Load workflow from a file path. - */ -function loadWorkflowFromPath( - filePath: string, - basePath: string, -): WorkflowConfig | null { - const resolvedPath = resolvePath(filePath, basePath); - if (!existsSync(resolvedPath)) { - return null; - } - return loadWorkflowFromFile(resolvedPath); -} - -/** - * Resolve a workflow YAML file path by trying both .yaml and .yml extensions. - * For category/name identifiers (e.g. "frontend/react"), resolves to - * {workflowsDir}/frontend/react.yaml (or .yml). - */ -function resolveWorkflowFile(workflowsDir: string, name: string): string | null { - for (const ext of ['.yaml', '.yml']) { - const filePath = join(workflowsDir, `${name}${ext}`); - if (existsSync(filePath)) return filePath; - } - return null; -} - -/** - * Load workflow by name (name-based loading only, no path detection). - * Supports category/name identifiers (e.g. "frontend/react"). - * - * Priority: - * 1. Project-local workflows → .takt/workflows/{name}.yaml - * 2. User workflows → ~/.takt/pieces/{name}.yaml - * 3. Builtin workflows → resources/global/{lang}/pieces/{name}.yaml - */ -export function loadWorkflow( - name: string, - projectCwd: string, -): WorkflowConfig | null { - const projectWorkflowsDir = join(getProjectConfigDir(projectCwd), 'workflows'); - const projectMatch = resolveWorkflowFile(projectWorkflowsDir, name); - if (projectMatch) { - return loadWorkflowFromFile(projectMatch); - } - - const globalPiecesDir = getGlobalPiecesDir(); - const globalMatch = resolveWorkflowFile(globalPiecesDir, name); - if (globalMatch) { - return loadWorkflowFromFile(globalMatch); - } - - return getBuiltinWorkflow(name); -} - -/** - * Check if a workflow identifier looks like a file path (vs a workflow name). - */ -export function isWorkflowPath(identifier: string): boolean { - return ( - identifier.startsWith('/') || - identifier.startsWith('~') || - identifier.startsWith('./') || - identifier.startsWith('../') || - identifier.endsWith('.yaml') || - identifier.endsWith('.yml') - ); -} - -/** - * Load workflow by identifier (auto-detects name vs path). - */ -export function loadWorkflowByIdentifier( - identifier: string, - projectCwd: string, -): WorkflowConfig | null { - if (isWorkflowPath(identifier)) { - return loadWorkflowFromPath(identifier, projectCwd); - } - return loadWorkflow(identifier, projectCwd); -} - -/** - * Get workflow description by identifier. - * Returns the workflow name and description (if available). - */ -export function getWorkflowDescription( - identifier: string, - projectCwd: string, -): { name: string; description: string } { - const workflow = loadWorkflowByIdentifier(identifier, projectCwd); - if (!workflow) { - return { name: identifier, description: '' }; - } - return { - name: workflow.name, - description: workflow.description ?? '', - }; -} - -/** Entry for a workflow file found in a directory */ -export interface WorkflowDirEntry { - /** Workflow name (e.g. "react") */ - name: string; - /** Full file path */ - path: string; - /** Category (subdirectory name), undefined for root-level workflows */ - category?: string; - /** Workflow source (builtin, user, project) */ - source: WorkflowSource; -} - -/** - * Iterate workflow YAML files in a directory, yielding name, path, and category. - * Scans root-level files (no category) and 1-level subdirectories (category = dir name). - * Shared by both loadAllWorkflows and listWorkflows to avoid DRY violation. - */ -function* iterateWorkflowDir( - dir: string, - source: WorkflowSource, - disabled?: string[], -): Generator { - if (!existsSync(dir)) return; - for (const entry of readdirSync(dir)) { - const entryPath = join(dir, entry); - const stat = statSync(entryPath); - - if (stat.isFile() && (entry.endsWith('.yaml') || entry.endsWith('.yml'))) { - const workflowName = entry.replace(/\.ya?ml$/, ''); - if (disabled?.includes(workflowName)) continue; - yield { name: workflowName, path: entryPath, source }; - continue; - } - - // 1-level subdirectory scan: directory name becomes the category - if (stat.isDirectory()) { - const category = entry; - for (const subEntry of readdirSync(entryPath)) { - if (!subEntry.endsWith('.yaml') && !subEntry.endsWith('.yml')) continue; - const subEntryPath = join(entryPath, subEntry); - if (!statSync(subEntryPath).isFile()) continue; - const workflowName = subEntry.replace(/\.ya?ml$/, ''); - const qualifiedName = `${category}/${workflowName}`; - if (disabled?.includes(qualifiedName)) continue; - yield { name: qualifiedName, path: subEntryPath, category, source }; - } - } - } -} - -/** Get the 3-layer directory list (builtin → user → project-local) */ -function getWorkflowDirs(cwd: string): { dir: string; source: WorkflowSource; disabled?: string[] }[] { - const disabled = getDisabledBuiltins(); - const lang = getLanguage(); - const dirs: { dir: string; source: WorkflowSource; disabled?: string[] }[] = []; - if (getBuiltinWorkflowsEnabled()) { - dirs.push({ dir: getBuiltinPiecesDir(lang), disabled, source: 'builtin' }); - } - dirs.push({ dir: getGlobalPiecesDir(), source: 'user' }); - dirs.push({ dir: join(getProjectConfigDir(cwd), 'workflows'), source: 'project' }); - return dirs; -} - -/** - * Load all workflows with source metadata. - * - * Priority (later entries override earlier): - * 1. Builtin workflows - * 2. User workflows (~/.takt/pieces/) - * 3. Project-local workflows (.takt/workflows/) - */ -export function loadAllWorkflowsWithSources(cwd: string): Map { - const workflows = new Map(); - - for (const { dir, source, disabled } of getWorkflowDirs(cwd)) { - for (const entry of iterateWorkflowDir(dir, source, disabled)) { - try { - workflows.set(entry.name, { config: loadWorkflowFromFile(entry.path), source: entry.source }); - } catch (err) { - log.debug('Skipping invalid workflow file', { path: entry.path, error: getErrorMessage(err) }); - } - } - } - - return workflows; -} - -/** - * Load all workflows with descriptions (for switch command). - * - * Priority (later entries override earlier): - * 1. Builtin workflows - * 2. User workflows (~/.takt/pieces/) - * 3. Project-local workflows (.takt/workflows/) - */ -export function loadAllWorkflows(cwd: string): Map { - const workflows = new Map(); - const withSources = loadAllWorkflowsWithSources(cwd); - for (const [name, entry] of withSources) { - workflows.set(name, entry.config); - } - return workflows; -} - -/** - * List available workflow names (builtin + user + project-local, excluding disabled). - * Category workflows use qualified names like "frontend/react". - */ -export function listWorkflows(cwd: string): string[] { - const workflows = new Set(); - - for (const { dir, source, disabled } of getWorkflowDirs(cwd)) { - for (const entry of iterateWorkflowDir(dir, source, disabled)) { - workflows.add(entry.name); - } - } - - return Array.from(workflows).sort(); -} - -/** - * List available workflows with category information for UI display. - * Returns entries grouped by category for 2-stage selection. - * - * Root-level workflows (no category) and category names are presented - * at the same level. Selecting a category drills into its workflows. - */ -export function listWorkflowEntries(cwd: string): WorkflowDirEntry[] { - // Later entries override earlier (project-local > user > builtin) - const workflows = new Map(); - - for (const { dir, source, disabled } of getWorkflowDirs(cwd)) { - for (const entry of iterateWorkflowDir(dir, source, disabled)) { - workflows.set(entry.name, entry); - } - } - - return Array.from(workflows.values()); -} diff --git a/src/infra/config/paths.ts b/src/infra/config/paths.ts index 9819ec1..a4392c3 100644 --- a/src/infra/config/paths.ts +++ b/src/infra/config/paths.ts @@ -90,8 +90,8 @@ export { loadProjectConfig, saveProjectConfig, updateProjectConfig, - getCurrentWorkflow, - setCurrentWorkflow, + getCurrentPiece, + setCurrentPiece, isVerboseMode, type ProjectLocalConfig, } from './project/projectConfig.js'; diff --git a/src/infra/config/project/index.ts b/src/infra/config/project/index.ts index 8d6a0f3..4ebc8cb 100644 --- a/src/infra/config/project/index.ts +++ b/src/infra/config/project/index.ts @@ -6,8 +6,8 @@ export { loadProjectConfig, saveProjectConfig, updateProjectConfig, - getCurrentWorkflow, - setCurrentWorkflow, + getCurrentPiece, + setCurrentPiece, isVerboseMode, type PermissionMode, type ProjectLocalConfig, diff --git a/src/infra/config/project/projectConfig.ts b/src/infra/config/project/projectConfig.ts index 6452819..10f23f6 100644 --- a/src/infra/config/project/projectConfig.ts +++ b/src/infra/config/project/projectConfig.ts @@ -14,7 +14,7 @@ export type { PermissionMode, ProjectLocalConfig }; /** Default project configuration */ const DEFAULT_PROJECT_CONFIG: ProjectLocalConfig = { - workflow: 'default', + piece: 'default', permissionMode: 'default', }; @@ -86,18 +86,18 @@ export function updateProjectConfig( } /** - * Get current workflow from project config + * Get current piece from project config */ -export function getCurrentWorkflow(projectDir: string): string { +export function getCurrentPiece(projectDir: string): string { const config = loadProjectConfig(projectDir); - return config.workflow || 'default'; + return config.piece || 'default'; } /** - * Set current workflow in project config + * Set current piece in project config */ -export function setCurrentWorkflow(projectDir: string, workflow: string): void { - updateProjectConfig(projectDir, 'workflow', workflow); +export function setCurrentPiece(projectDir: string, piece: string): void { + updateProjectConfig(projectDir, 'piece', piece); } /** diff --git a/src/infra/config/types.ts b/src/infra/config/types.ts index 095d426..47df673 100644 --- a/src/infra/config/types.ts +++ b/src/infra/config/types.ts @@ -2,7 +2,7 @@ * Config module type definitions */ -import type { WorkflowCategoryConfigNode } from '../../core/models/schemas.js'; +import type { PieceCategoryConfigNode } from '../../core/models/schemas.js'; /** Permission mode for the project * - default: Uses Agent SDK's acceptEdits mode (auto-accepts file edits, minimal prompts) @@ -14,17 +14,17 @@ export type PermissionMode = 'default' | 'sacrifice-my-pc'; /** Project configuration stored in .takt/config.yaml */ export interface ProjectLocalConfig { - /** Current workflow name */ - workflow?: string; + /** Current piece name */ + piece?: string; /** Provider selection for agent runtime */ provider?: 'claude' | 'codex'; /** Permission mode setting */ permissionMode?: PermissionMode; /** Verbose output mode */ verbose?: boolean; - /** Workflow categories (name -> workflow list) */ - workflow_categories?: Record; - /** Show uncategorized workflows under Others category */ + /** Piece categories (name -> piece list) */ + piece_categories?: Record; + /** Show uncategorized pieces under Others category */ show_others_category?: boolean; /** Display name for Others category */ others_category_name?: string; diff --git a/src/infra/fs/index.ts b/src/infra/fs/index.ts index f796155..ee350db 100644 --- a/src/infra/fs/index.ts +++ b/src/infra/fs/index.ts @@ -4,11 +4,11 @@ export type { SessionLog, - NdjsonWorkflowStart, + NdjsonPieceStart, NdjsonStepStart, NdjsonStepComplete, - NdjsonWorkflowComplete, - NdjsonWorkflowAbort, + NdjsonPieceComplete, + NdjsonPieceAbort, NdjsonPhaseStart, NdjsonPhaseComplete, NdjsonInteractiveStart, diff --git a/src/infra/fs/session.ts b/src/infra/fs/session.ts index e8ab886..2cea5f7 100644 --- a/src/infra/fs/session.ts +++ b/src/infra/fs/session.ts @@ -9,17 +9,17 @@ import { generateReportDir as buildReportDir } from '../../shared/utils/index.js import type { SessionLog, NdjsonRecord, - NdjsonWorkflowStart, + NdjsonPieceStart, LatestLogPointer, } from '../../shared/utils/index.js'; export type { SessionLog, - NdjsonWorkflowStart, + NdjsonPieceStart, NdjsonStepStart, NdjsonStepComplete, - NdjsonWorkflowComplete, - NdjsonWorkflowAbort, + NdjsonPieceComplete, + NdjsonPieceAbort, NdjsonPhaseStart, NdjsonPhaseComplete, NdjsonInteractiveStart, @@ -39,11 +39,11 @@ export class SessionManager { } - /** Initialize an NDJSON log file with the workflow_start record */ + /** Initialize an NDJSON log file with the piece_start record */ initNdjsonLog( sessionId: string, task: string, - workflowName: string, + pieceName: string, projectDir?: string, ): string { const logsDir = projectDir @@ -52,10 +52,10 @@ export class SessionManager { ensureDir(logsDir); const filepath = join(logsDir, `${sessionId}.jsonl`); - const record: NdjsonWorkflowStart = { - type: 'workflow_start', + const record: NdjsonPieceStart = { + type: 'piece_start', task, - workflowName, + pieceName, startTime: new Date().toISOString(), }; this.appendNdjsonLine(filepath, record); @@ -79,11 +79,11 @@ export class SessionManager { const record = JSON.parse(line) as NdjsonRecord; switch (record.type) { - case 'workflow_start': + case 'piece_start': sessionLog = { task: record.task, projectDir: '', - workflowName: record.workflowName, + pieceName: record.pieceName, iterations: 0, startTime: record.startTime, status: 'running', @@ -108,14 +108,14 @@ export class SessionManager { } break; - case 'workflow_complete': + case 'piece_complete': if (sessionLog) { sessionLog.status = 'completed'; sessionLog.endTime = record.endTime; } break; - case 'workflow_abort': + case 'piece_abort': if (sessionLog) { sessionLog.status = 'aborted'; sessionLog.endTime = record.endTime; @@ -149,12 +149,12 @@ export class SessionManager { createSessionLog( task: string, projectDir: string, - workflowName: string, + pieceName: string, ): SessionLog { return { task, projectDir, - workflowName, + pieceName, iterations: 0, startTime: new Date().toISOString(), status: 'running', @@ -227,7 +227,7 @@ export class SessionManager { sessionId, logFile: `${sessionId}.jsonl`, task: log.task, - workflowName: log.workflowName, + pieceName: log.pieceName, status: log.status, startTime: log.startTime, updatedAt: new Date().toISOString(), @@ -247,10 +247,10 @@ export function appendNdjsonLine(filepath: string, record: NdjsonRecord): void { export function initNdjsonLog( sessionId: string, task: string, - workflowName: string, + pieceName: string, projectDir?: string, ): string { - return defaultManager.initNdjsonLog(sessionId, task, workflowName, projectDir); + return defaultManager.initNdjsonLog(sessionId, task, pieceName, projectDir); } @@ -270,9 +270,9 @@ export function generateReportDir(task: string): string { export function createSessionLog( task: string, projectDir: string, - workflowName: string, + pieceName: string, ): SessionLog { - return defaultManager.createSessionLog(task, projectDir, workflowName); + return defaultManager.createSessionLog(task, projectDir, pieceName); } export function finalizeSessionLog( diff --git a/src/infra/github/issue.ts b/src/infra/github/issue.ts index 6d4cf82..a8cb65a 100644 --- a/src/infra/github/issue.ts +++ b/src/infra/github/issue.ts @@ -2,7 +2,7 @@ * GitHub Issue utilities * * Fetches issue content via `gh` CLI and formats it as task text - * for workflow execution or task creation. + * for piece execution or task creation. */ import { execFileSync } from 'node:child_process'; @@ -73,7 +73,7 @@ export function fetchIssue(issueNumber: number): GitHubIssue { } /** - * Format a GitHub issue into task text for workflow execution. + * Format a GitHub issue into task text for piece execution. * * Output format: * ``` diff --git a/src/infra/mock/client.ts b/src/infra/mock/client.ts index 5c7a7d3..974dec8 100644 --- a/src/infra/mock/client.ts +++ b/src/infra/mock/client.ts @@ -2,7 +2,7 @@ * Mock agent client for testing * * Returns immediate fixed responses without any API calls. - * Useful for testing workflows without incurring costs or latency. + * Useful for testing pieces without incurring costs or latency. */ import { randomUUID } from 'node:crypto'; diff --git a/src/infra/providers/types.ts b/src/infra/providers/types.ts index e5a6aad..85fcc26 100644 --- a/src/infra/providers/types.ts +++ b/src/infra/providers/types.ts @@ -14,7 +14,7 @@ export interface ProviderCallOptions { allowedTools?: string[]; /** Maximum number of agentic turns */ maxTurns?: number; - /** Permission mode for tool execution (from workflow step) */ + /** Permission mode for tool execution (from piece step) */ permissionMode?: PermissionMode; onStream?: StreamCallback; onPermissionRequest?: PermissionHandler; diff --git a/src/infra/resources/index.ts b/src/infra/resources/index.ts index 67f48bc..2815f6d 100644 --- a/src/infra/resources/index.ts +++ b/src/infra/resources/index.ts @@ -1,9 +1,9 @@ /** * Embedded resources for takt * - * Contains default workflow definitions and resource paths. + * Contains default piece definitions and resource paths. * Resources are organized into: - * - resources/global/{lang}/workflows/ - Builtin workflows (loaded via fallback) + * - resources/global/{lang}/pieces/ - Builtin pieces (loaded via fallback) * - resources/global/{lang}/agents/ - Builtin agents (loaded via fallback) * - resources/global/{lang}/prompts/ - Builtin prompt templates * - resources/project/ - Project-level template files (.gitignore) diff --git a/src/infra/task/autoCommit.ts b/src/infra/task/autoCommit.ts index d321059..1c0a9e9 100644 --- a/src/infra/task/autoCommit.ts +++ b/src/infra/task/autoCommit.ts @@ -1,7 +1,7 @@ /** * Auto-commit and push for clone tasks * - * After a successful workflow completion in a shared clone, + * After a successful piece completion in a shared clone, * automatically stages all changes, creates a commit, and * pushes to origin so the branch is reflected in the main repo. * No co-author trailer is added. diff --git a/src/infra/task/display.ts b/src/infra/task/display.ts index 7af6449..76e6751 100644 --- a/src/infra/task/display.ts +++ b/src/infra/task/display.ts @@ -48,8 +48,8 @@ export function showTaskList(runner: TaskRunner): void { if (task.data.branch) { extras.push(`branch: ${task.data.branch}`); } - if (task.data.workflow) { - extras.push(`workflow: ${task.data.workflow}`); + if (task.data.piece) { + extras.push(`piece: ${task.data.piece}`); } if (extras.length > 0) { console.log(chalk.dim(` [${extras.join(', ')}]`)); diff --git a/src/infra/task/schema.ts b/src/infra/task/schema.ts index c83169b..eeedf91 100644 --- a/src/infra/task/schema.ts +++ b/src/infra/task/schema.ts @@ -13,7 +13,7 @@ import { z } from 'zod/v4'; * task: "認証機能を追加する" * worktree: true # 共有クローンで隔離実行 * branch: "feat/add-auth" # オプション(省略時は自動生成) - * workflow: "default" # オプション(省略時はcurrent workflow) + * piece: "default" # オプション(省略時はcurrent piece) * * worktree patterns (uses git clone --shared internally): * - true: create shared clone in sibling dir or worktree_dir @@ -28,7 +28,7 @@ export const TaskFileSchema = z.object({ task: z.string().min(1), worktree: z.union([z.boolean(), z.string()]).optional(), branch: z.string().optional(), - workflow: z.string().optional(), + piece: z.string().optional(), issue: z.number().int().positive().optional(), }); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 33c7457..0a3c07f 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -5,8 +5,8 @@ /** Supported language codes (duplicated from core/models to avoid shared → core dependency) */ type Language = 'en' | 'ja'; -/** Default workflow name when none specified */ -export const DEFAULT_WORKFLOW_NAME = 'default'; +/** Default piece name when none specified */ +export const DEFAULT_PIECE_NAME = 'default'; /** Default language for new installations */ export const DEFAULT_LANGUAGE: Language = 'en'; diff --git a/src/shared/exitCodes.ts b/src/shared/exitCodes.ts index 723c8b4..eeee359 100644 --- a/src/shared/exitCodes.ts +++ b/src/shared/exitCodes.ts @@ -8,7 +8,7 @@ export const EXIT_SUCCESS = 0; export const EXIT_GENERAL_ERROR = 1; export const EXIT_ISSUE_FETCH_FAILED = 2; -export const EXIT_WORKFLOW_FAILED = 3; +export const EXIT_PIECE_FAILED = 3; export const EXIT_GIT_OPERATION_FAILED = 4; export const EXIT_PR_CREATION_FAILED = 5; export const EXIT_SIGINT = 130; // 128 + SIGINT(2), UNIX convention diff --git a/src/shared/i18n/labels_en.yaml b/src/shared/i18n/labels_en.yaml index d5e7872..fa44fdf 100644 --- a/src/shared/i18n/labels_en.yaml +++ b/src/shared/i18n/labels_en.yaml @@ -19,19 +19,19 @@ interactive: confirm: "Use this task instruction?" cancelled: "Cancelled" -# ===== Workflow Execution UI ===== -workflow: +# ===== Piece Execution UI ===== +piece: iterationLimit: - maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})" - currentMovement: "現在のムーブメント: {currentMovement}" - continueQuestion: "続行しますか?" - continueLabel: "続行する(追加イテレーション数を入力)" - continueDescription: "入力した回数だけ上限を増やします" - stopLabel: "終了する" - inputPrompt: "追加するイテレーション数を入力してください(1以上)" - invalidInput: "1以上の整数を入力してください。" - userInputPrompt: "追加の指示を入力してください(空で中止)" - notifyComplete: "ワークフロー完了 ({iteration} iterations)" - notifyAbort: "中断: {reason}" - sigintGraceful: "Ctrl+C: ワークフローを中断しています..." - sigintForce: "Ctrl+C: 強制終了します" + maxReached: "Reached max iterations ({currentIteration}/{maxIterations})" + currentMovement: "Current movement: {currentMovement}" + continueQuestion: "Continue?" + continueLabel: "Continue (enter additional iterations)" + continueDescription: "Increase the limit by the entered count" + stopLabel: "Stop" + inputPrompt: "Enter additional iterations (>= 1)" + invalidInput: "Please enter an integer of 1 or greater." + userInputPrompt: "Enter additional instructions (empty to cancel)" + notifyComplete: "Piece complete ({iteration} iterations)" + notifyAbort: "Aborted: {reason}" + sigintGraceful: "Ctrl+C: Aborting piece..." + sigintForce: "Ctrl+C: Force exit" diff --git a/src/shared/i18n/labels_ja.yaml b/src/shared/i18n/labels_ja.yaml index 939a11a..0d591d9 100644 --- a/src/shared/i18n/labels_ja.yaml +++ b/src/shared/i18n/labels_ja.yaml @@ -19,8 +19,8 @@ interactive: confirm: "このタスク指示で進めますか?" cancelled: "キャンセルしました" -# ===== Workflow Execution UI ===== -workflow: +# ===== Piece Execution UI ===== +piece: iterationLimit: maxReached: "最大イテレーションに到達しました ({currentIteration}/{maxIterations})" currentMovement: "現在のムーブメント: {currentMovement}" @@ -31,7 +31,7 @@ workflow: inputPrompt: "追加するイテレーション数を入力してください(1以上)" invalidInput: "1以上の整数を入力してください。" userInputPrompt: "追加の指示を入力してください(空で中止)" - notifyComplete: "ワークフロー完了 ({iteration} iterations)" + notifyComplete: "ピース完了 ({iteration} iterations)" notifyAbort: "中断: {reason}" - sigintGraceful: "Ctrl+C: ワークフローを中断しています..." + sigintGraceful: "Ctrl+C: ピースを中断しています..." sigintForce: "Ctrl+C: 強制終了します" diff --git a/src/shared/prompts/en/perform_builtin_agent_system_prompt.md b/src/shared/prompts/en/perform_builtin_agent_system_prompt.md index cfc88bd..197873b 100644 --- a/src/shared/prompts/en/perform_builtin_agent_system_prompt.md +++ b/src/shared/prompts/en/perform_builtin_agent_system_prompt.md @@ -4,4 +4,4 @@ vars: agentName caller: infra/claude/client.ts --> -You are the {{agentName}} agent. Follow the standard {{agentName}} workflow. +You are the {{agentName}} agent. Follow the standard {{agentName}} piece. diff --git a/src/shared/prompts/en/perform_phase1_message.md b/src/shared/prompts/en/perform_phase1_message.md index 24dbdc5..c14ac7f 100644 --- a/src/shared/prompts/en/perform_phase1_message.md +++ b/src/shared/prompts/en/perform_phase1_message.md @@ -1,7 +1,7 @@ You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process. @@ -9,19 +9,19 @@ You are a task planning assistant. You help the user clarify and refine task req ## Your role - Ask clarifying questions about ambiguous requirements - Clarify and refine the user's request into a clear task instruction -- Create concrete instructions for workflow agents to follow +- Create concrete instructions for piece agents to follow - Summarize your understanding when appropriate - Keep responses concise and focused -**Important**: Do NOT investigate the codebase, identify files, or make assumptions about implementation details. That is the job of the next workflow steps (plan/architect). +**Important**: Do NOT investigate the codebase, identify files, or make assumptions about implementation details. That is the job of the next piece steps (plan/architect). ## Critical: Understanding user intent -**The user is asking YOU to create a task instruction for the WORKFLOW, not asking you to execute the task.** +**The user is asking YOU to create a task instruction for the PIECE, not asking you to execute the task.** When the user says: -- "Review this code" → They want the WORKFLOW to review (you create the instruction) -- "Implement feature X" → They want the WORKFLOW to implement (you create the instruction) -- "Fix this bug" → They want the WORKFLOW to fix (you create the instruction) +- "Review this code" → They want the PIECE to review (you create the instruction) +- "Implement feature X" → They want the PIECE to implement (you create the instruction) +- "Fix this bug" → They want the PIECE to fix (you create the instruction) These are NOT requests for YOU to investigate. Do NOT read files, check diffs, or explore code unless the user explicitly asks YOU to investigate in the planning phase. @@ -32,10 +32,10 @@ Only investigate when the user explicitly asks YOU (the planning assistant) to c - "What does this project do?" ✓ ## When investigation is NOT appropriate (most cases) -Do NOT investigate when the user is describing a task for the workflow: -- "Review the changes" ✗ (workflow's job) -- "Fix the code" ✗ (workflow's job) -- "Implement X" ✗ (workflow's job) +Do NOT investigate when the user is describing a task for the piece: +- "Review the changes" ✗ (piece's job) +- "Fix the code" ✗ (piece's job) +- "Implement X" ✗ (piece's job) ## Strict constraints - You are ONLY refining requirements. Do NOT execute the task. @@ -43,11 +43,11 @@ Do NOT investigate when the user is describing a task for the workflow: - Do NOT use Read/Glob/Grep/Bash proactively. Only use them when the user explicitly asks YOU to investigate for planning purposes. - Do NOT mention or reference any slash commands. You have no knowledge of them. - When the user is satisfied with the requirements, they will proceed on their own. Do NOT instruct them on what to do next. -{{#if workflowInfo}} +{{#if pieceInfo}} ## Destination of Your Task Instruction -This task instruction will be passed to the "{{workflowName}}" workflow. -Workflow description: {{workflowDescription}} +This task instruction will be passed to the "{{pieceName}}" piece. +Piece description: {{pieceDescription}} -Create the instruction in the format expected by this workflow. +Create the instruction in the format expected by this piece. {{/if}} diff --git a/src/shared/prompts/en/score_summary_system_prompt.md b/src/shared/prompts/en/score_summary_system_prompt.md index 060fae3..74864e2 100644 --- a/src/shared/prompts/en/score_summary_system_prompt.md +++ b/src/shared/prompts/en/score_summary_system_prompt.md @@ -1,7 +1,7 @@ You are a task summarizer. Convert the conversation into a concrete task instruction for the planning step. @@ -13,13 +13,13 @@ Requirements: - If the source of a constraint is unclear, do not include it; add it to Open Questions if needed. - Do not include constraints proposed or inferred by the assistant. - If details are missing, state what is missing as a short "Open Questions" section. -{{#if workflowInfo}} +{{#if pieceInfo}} ## Destination of Your Task Instruction -This task instruction will be passed to the "{{workflowName}}" workflow. -Workflow description: {{workflowDescription}} +This task instruction will be passed to the "{{pieceName}}" piece. +Piece description: {{pieceDescription}} -Create the instruction in the format expected by this workflow. +Create the instruction in the format expected by this piece. {{/if}} {{#if conversation}} diff --git a/src/shared/prompts/ja/perform_builtin_agent_system_prompt.md b/src/shared/prompts/ja/perform_builtin_agent_system_prompt.md index cfc88bd..197873b 100644 --- a/src/shared/prompts/ja/perform_builtin_agent_system_prompt.md +++ b/src/shared/prompts/ja/perform_builtin_agent_system_prompt.md @@ -4,4 +4,4 @@ vars: agentName caller: infra/claude/client.ts --> -You are the {{agentName}} agent. Follow the standard {{agentName}} workflow. +You are the {{agentName}} agent. Follow the standard {{agentName}} piece. diff --git a/src/shared/prompts/ja/perform_phase1_message.md b/src/shared/prompts/ja/perform_phase1_message.md index 31f89e9..a13dc43 100644 --- a/src/shared/prompts/ja/perform_phase1_message.md +++ b/src/shared/prompts/ja/perform_phase1_message.md @@ -1,7 +1,7 @@ -あなたはTAKT(AIエージェントワークフローオーケストレーションツール)の対話モードを担当しています。 +あなたはTAKT(AIエージェントピースオーケストレーションツール)の対話モードを担当しています。 ## TAKTの仕組み -1. **対話モード(今ここ・あなたの役割)**: ユーザーと会話してタスクを整理し、ワークフロー実行用の具体的な指示書を作成する -2. **ワークフロー実行**: あなたが作成した指示書をワークフローに渡し、複数のAIエージェントが順次実行する(実装、レビュー、修正など) +1. **対話モード(今ここ・あなたの役割)**: ユーザーと会話してタスクを整理し、ピース実行用の具体的な指示書を作成する +2. **ピース実行**: あなたが作成した指示書をピースに渡し、複数のAIエージェントが順次実行する(実装、レビュー、修正など) -あなたは対話モードの担当です。作成する指示書は、次に実行されるワークフローの入力(タスク)となります。ワークフローの内容はワークフロー定義に依存し、必ずしも実装から始まるとは限りません(調査、計画、レビューなど様々)。 +あなたは対話モードの担当です。作成する指示書は、次に実行されるピースの入力(タスク)となります。ピースの内容はピース定義に依存し、必ずしも実装から始まるとは限りません(調査、計画、レビューなど様々)。 ## あなたの役割 - あいまいな要求に対して確認質問をする - ユーザーの要求を明確化し、指示書として洗練させる -- ワークフローのエージェントが迷わないよう具体的な指示書を作成する +- ピースのエージェントが迷わないよう具体的な指示書を作成する - 必要に応じて理解した内容を簡潔にまとめる - 返答は簡潔で要点のみ -**重要**: コードベース調査、前提把握、対象ファイル特定は行わない。これらは次のワークフロー(plan/architectステップ)の役割です。 +**重要**: コードベース調査、前提把握、対象ファイル特定は行わない。これらは次のピース(plan/architectステップ)の役割です。 ## 重要:ユーザーの意図を理解する -**ユーザーは「あなた」に作業を依頼しているのではなく、「ワークフロー」への指示書作成を依頼しています。** +**ユーザーは「あなた」に作業を依頼しているのではなく、「ピース」への指示書作成を依頼しています。** ユーザーが次のように言った場合: -- 「このコードをレビューして」→ ワークフローにレビューさせる(あなたは指示書を作成) -- 「機能Xを実装して」→ ワークフローに実装させる(あなたは指示書を作成) -- 「このバグを修正して」→ ワークフローに修正させる(あなたは指示書を作成) +- 「このコードをレビューして」→ ピースにレビューさせる(あなたは指示書を作成) +- 「機能Xを実装して」→ ピースに実装させる(あなたは指示書を作成) +- 「このバグを修正して」→ ピースに修正させる(あなたは指示書を作成) これらは「あなた」への調査依頼ではありません。ファイルを読んだり、差分を確認したり、コードを探索したりしないでください。ユーザーが明示的に「あなた(対話モード)」に調査を依頼した場合のみ調査してください。 @@ -38,22 +38,22 @@ - 「このプロジェクトは何をするもの?」✓ ## 調査が不適切な場合(ほとんどのケース) -ユーザーがワークフロー向けのタスクを説明している場合は調査しない: -- 「変更をレビューして」✗(ワークフローの仕事) -- 「コードを修正して」✗(ワークフローの仕事) -- 「Xを実装して」✗(ワークフローの仕事) +ユーザーがピース向けのタスクを説明している場合は調査しない: +- 「変更をレビューして」✗(ピースの仕事) +- 「コードを修正して」✗(ピースの仕事) +- 「Xを実装して」✗(ピースの仕事) ## 厳守事項 -- あなたは要求の明確化のみを行う。実際の作業(実装/調査/レビュー等)はワークフローのエージェントが行う +- あなたは要求の明確化のみを行う。実際の作業(実装/調査/レビュー等)はピースのエージェントが行う - ファイルの作成/編集/削除はしない(計画目的で明示的に依頼された場合を除く) - Read/Glob/Grep/Bash を勝手に使わない。ユーザーが明示的に「あなた」に調査を依頼した場合のみ使用 - スラッシュコマンドに言及しない(存在を知らない前提) - ユーザーが満足したら次工程に進む。次の指示はしない -{{#if workflowInfo}} +{{#if pieceInfo}} ## あなたが作成する指示書の行き先 -このタスク指示書は「{{workflowName}}」ワークフローに渡されます。 -ワークフローの内容: {{workflowDescription}} +このタスク指示書は「{{pieceName}}」ピースに渡されます。 +ピースの内容: {{pieceDescription}} -指示書は、このワークフローが期待する形式で作成してください。 +指示書は、このピースが期待する形式で作成してください。 {{/if}} diff --git a/src/shared/prompts/ja/score_summary_system_prompt.md b/src/shared/prompts/ja/score_summary_system_prompt.md index b5394e2..4113744 100644 --- a/src/shared/prompts/ja/score_summary_system_prompt.md +++ b/src/shared/prompts/ja/score_summary_system_prompt.md @@ -1,15 +1,15 @@ -あなたはTAKTの対話モードを担当しています。これまでの会話内容を、ワークフロー実行用の具体的なタスク指示書に変換してください。 +あなたはTAKTの対話モードを担当しています。これまでの会話内容を、ピース実行用の具体的なタスク指示書に変換してください。 ## 立ち位置 - あなた: 対話モード(タスク整理・指示書作成) -- 次のステップ: あなたが作成した指示書がワークフローに渡され、複数のAIエージェントが順次実行する -- あなたの成果物(指示書)が、ワークフロー全体の入力(タスク)になる +- 次のステップ: あなたが作成した指示書がピースに渡され、複数のAIエージェントが順次実行する +- あなたの成果物(指示書)が、ピース全体の入力(タスク)になる ## 要件 - 出力はタスク指示書のみ(前置き不要) @@ -20,13 +20,13 @@ - 制約の出所が不明な場合は保持せず、必要なら Open Questions に回す - アシスタントが提案・推測した制約は指示書に含めない - 情報不足があれば「Open Questions」セクションを短く付ける -{{#if workflowInfo}} +{{#if pieceInfo}} ## あなたが作成する指示書の行き先 -このタスク指示書は「{{workflowName}}」ワークフローに渡されます。 -ワークフローの内容: {{workflowDescription}} +このタスク指示書は「{{pieceName}}」ピースに渡されます。 +ピースの内容: {{pieceDescription}} -指示書は、このワークフローが期待する形式で作成してください。 +指示書は、このピースが期待する形式で作成してください。 {{/if}} {{#if conversation}} diff --git a/src/shared/ui/StreamDisplay.ts b/src/shared/ui/StreamDisplay.ts index 8b98484..d54532e 100644 --- a/src/shared/ui/StreamDisplay.ts +++ b/src/shared/ui/StreamDisplay.ts @@ -7,10 +7,10 @@ import chalk from 'chalk'; // NOTE: type-only import from core — acceptable because StreamDisplay is -// a UI renderer tightly coupled to the workflow event protocol. +// a UI renderer tightly coupled to the piece event protocol. // Moving StreamEvent/StreamCallback to shared would require relocating all // dependent event-data types, which is out of scope for this refactoring. -import type { StreamEvent, StreamCallback } from '../../core/workflow/index.js'; +import type { StreamEvent, StreamCallback } from '../../core/piece/index.js'; import { truncate } from './LogManager.js'; /** Stream display manager for real-time Claude output */ diff --git a/src/shared/utils/notification.ts b/src/shared/utils/notification.ts index 271b277..9bc0775 100644 --- a/src/shared/utils/notification.ts +++ b/src/shared/utils/notification.ts @@ -1,7 +1,7 @@ /** * Notification utilities for takt * - * Provides audio and visual notifications for workflow events. + * Provides audio and visual notifications for piece events. */ import { exec } from 'node:child_process'; diff --git a/src/shared/utils/types.ts b/src/shared/utils/types.ts index 8d27528..cdaba1f 100644 --- a/src/shared/utils/types.ts +++ b/src/shared/utils/types.ts @@ -9,7 +9,7 @@ export interface SessionLog { task: string; projectDir: string; - workflowName: string; + pieceName: string; iterations: number; startTime: string; endTime?: string; @@ -31,10 +31,10 @@ export interface SessionLog { // --- NDJSON log types --- -export interface NdjsonWorkflowStart { - type: 'workflow_start'; +export interface NdjsonPieceStart { + type: 'piece_start'; task: string; - workflowName: string; + pieceName: string; startTime: string; } @@ -60,14 +60,14 @@ export interface NdjsonStepComplete { timestamp: string; } -export interface NdjsonWorkflowComplete { - type: 'workflow_complete'; +export interface NdjsonPieceComplete { + type: 'piece_complete'; iterations: number; endTime: string; } -export interface NdjsonWorkflowAbort { - type: 'workflow_abort'; +export interface NdjsonPieceAbort { + type: 'piece_abort'; iterations: number; reason: string; endTime: string; @@ -106,11 +106,11 @@ export interface NdjsonInteractiveEnd { } export type NdjsonRecord = - | NdjsonWorkflowStart + | NdjsonPieceStart | NdjsonStepStart | NdjsonStepComplete - | NdjsonWorkflowComplete - | NdjsonWorkflowAbort + | NdjsonPieceComplete + | NdjsonPieceAbort | NdjsonPhaseStart | NdjsonPhaseComplete | NdjsonInteractiveStart @@ -123,7 +123,7 @@ export interface LatestLogPointer { sessionId: string; logFile: string; task: string; - workflowName: string; + pieceName: string; status: SessionLog['status']; startTime: string; updatedAt: string; diff --git a/tools/jsonl-viewer.html b/tools/jsonl-viewer.html index 21db6dd..bc9dd8c 100644 --- a/tools/jsonl-viewer.html +++ b/tools/jsonl-viewer.html @@ -157,7 +157,7 @@ border-left: 4px solid #007acc; } - .record.workflow_start { + .record.piece_start { border-left-color: #4ec9b0; } @@ -169,11 +169,11 @@ border-left-color: #608b4e; } - .record.workflow_complete { + .record.piece_complete { border-left-color: #4ec9b0; } - .record.workflow_abort { + .record.piece_abort { border-left-color: #f48771; } @@ -611,8 +611,8 @@ function formatRecordContent(record) { const fields = []; - if (record.workflow) { - fields.push(`
Workflow:${escapeHtml(record.workflow)}
`); + if (record.piece) { + fields.push(`
Piece:${escapeHtml(record.piece)}
`); } if (record.step) { fields.push(`
Step:${escapeHtml(record.step)}
`);